嵌入式学习笔记——SPI通信

前言

之前已经介绍了STM32的ADC、DMA、EXTI、TIME、NVIC、USART以及普通IO模式,此系列笔者还打算写最后三个大的内容,分别是SPI通信、IIC通信以及看门狗,后面就看大家的需求了,需要什么可以留在评论区,本文首先来介绍SPI的有关知识。

SPI总线概述

在这里插入图片描述
在通信协议分类的介绍中,提到过SPI,它是一种同步 串行 全双工(也可半双工)通信协议,是最常用的板级通信总线。为什么要加总线作为它的定语呢,原因就是这个协议可以实现一主多从的通信,多个从机和主机通过SPI所需的信号线连接在一起,就拿之前的串口通信来说,串口通信是一主一从的通信方式,主机TX接从机的RX,主机的RX接从机的TX,除了共地以外还需要两个通信线;而SPI通信除了共地以外需要四个信号线进行传输,根据它同步串行全双工的特点,可以分析出它必然具有同步时钟线,以及两个串行数据线来实现全双工通信,至于第四个信号线,是用来判断具体与哪个从机进行通信的片选线。

名称 功能
MOSI 主输出从输入
MISO 主输入从输出
SCLK 同步时钟线
CS 片选线

SPI通信拓扑图

上面提到过,SPI是一种通信总线,这也意味着在一组信号线上可能存在多个从机,那么具体的连接方式拓扑图是怎么样的呢
1.一主一从
在这里插入图片描述
在这里插入图片描述
注意上图中的MOSI和MOSI分别用了DO和DI来表示,这里需要了解一下生产厂商常用的别称,如下所示:
MISO:SIMO、DOUT、DO、SDO或SO(在主机端);

MOSI:SOMI、DIN、DI、SDI或SI(在主机端);

CS:CE、NSS、CE或SSEL;

SCLK也可以是SCK;
2.一主多从:
如下图所示,就是一个主机与多个主机连接的一种方式,主机与各个从机直接共同连接在SCLK、MOSI、MISO上,除此之外,每个从机还单独有一个SS片选与主机连接,通信时,主机通过拉低对应从机的SS来选择通信对象。
在这里插入图片描述
除此之外还有一个菊花链的拓扑连接方式,这个大家可以去下面两篇博客中查看,关于SPI的详细介绍也可以查看这两篇大佬的分析,很透彻。

  1. SPI协议详解(图文并茂+超详细)http://t.csdn.cn/qaS7h
  2. 一文搞懂SPI通信协议http://t.csdn.cn/gTKQq

STM32的SPI通信

关于STM32的SPI通信,也有两种方案,这个在串口通信的时候我们也提到过,
方案一就是使用STM32集成好的SPI控制器,与之前使用USART一样,按照自己需求配置好对应的寄存器以及通信模式后就可以直接通过SPI控制器来实现收发,不需要编程实现底层的具体发送方式;
方案二使用IO口模拟SPI的时序,参照SPI通信的时序图,操作GPIO口实现数据的收发,模拟SPI的好处在于不必拘束于固定的管脚,随便一组管脚都可以,只是需要自己编写底层的发送函数,实现01010之类的发送。本文笔者会将两种方式都介绍一下。
首先,还是参照之前的模式,先介绍使用控制器实现的过程。

SPI的特性

首先还是来过一下STM32的SPI的特性,
第一,基于三条线的全双工同步传输,也就是常说的四线SPI,这里的三条线是不包括NSS片选线的;注意,还有一种三线SPI,这里的三线是缺少MOSI或者是MISO其中的一根,双方通过一个数据线进行数据交换,同一时间只能主发从收或者主收从发,这种三线SPI是一种半双工模式,有些类似后面会介绍的IIC的通信方式;
第二,STM32 SPI的数据帧可以是八位或者是16位数据,也就是说SPI控制器可以一次性发送16位数据或者8位数据,而我们之前的USART是只能发送8位或者9位数据;
第三,SPI控制器具有主从模式可以设置,也就是说,STM32上集成的SPI既可以做主机也可以作为从机,在使用过程中需要我们进行配置;
第四,SPI的最大通信频率是40MHZ,而fPCLK是84MHZ(由所挂接的时钟线决定),所以最大的通信速率是fPCLK/2,40MHZ的传输速率已经比IIC和USART快很多了。而且SPI控制器还可以使用快速模式的通信方式,将两个数据线同时作为传输,一般用不上,做个了解即可。
第五,SPI控制器有可编程的时钟极性与时钟相位,关于时钟极性与时钟相位,它们两个分别有两个状态,时钟极性有0与1两个状态,决定时钟是高有效还是低有效;而时钟相位决定的是第一个时钟边沿有效还是第二个时钟边沿有效,它们两两组合,构成了SPI的四种模式,
时钟极性CPOL
当时钟极性为0的时候,时钟线空闲状态是低电平状态
当时钟极性为1的时候,时钟线空闲状态是高电平状态
时钟相位CPHA
当时钟相位为0的时候,数据在第一个跳变沿被采样(数据采集)
当时钟相位为1的时候,数据在第二个跳变沿被采样
在这里插入图片描述

模式 CPOL(时钟极性) CPHA(时钟相位) 数据收发
模式0 0 0 空闲时时钟线为低电平,第一个时钟边沿采集数据
模式1 0 1 空闲时时钟线为低电平,第二个时钟边沿采集数据
模式2 1 0 空闲时时钟线为高电平,第一个时钟边沿采集数据
模式3 1 1 空闲时时钟线为高电平,第二个时钟边沿采集数据

常用的是模式3与模式0,而且一般来说支持模式0的器件也支持模式3的通信方式,支持模式1的也支持模式2的通信方式。
第六,SPI控制器也有对应的状态位可以产生发送完成和接收完成的标志,而且其传输是高位先发还是低位先发都可以进行编程控制。
在这里插入图片描述

SPI控制器的框图

在了解了SPI控制器的相关特性后,接下来就是它的框图介绍了,其整体框图如下图所示,不难发现,这个框图的整体不是很复杂,我们来稍作拆分介绍一下。
在这里插入图片描述

引脚

首先是左边的四个GPIO口,它们各自的功能如下图所示:
在这里插入图片描述
这里注意的NSS片选,我们在使用过程过程中一般禁止此处的NSS,用内部的软件管理来屏蔽这个NSS,用另外一个GPIO来专门操作控制从机,在需要通信时拉低对应的管脚即可。
在这里插入图片描述

数据收发过程

当使用四线SPI实现收发时,主机数据发送过程如下图中绿色线的流程,首先由MCU将数据写入发送缓冲区,然后再将数据由发送缓冲区并行转移到移位寄存器,根据LSB的设置,看是地位先发还是高位先发,然后再由移位寄存器一位一位的将数据发送到MOSI管脚上
主机数据接收过程,如下图红色线所示,有MISO输入数据,然后进入移位寄存器,接收完毕后经过移位寄存器并行转移到接收缓冲区,然后CPU在接收缓冲区中读取数据。
注:

  1. 下图中蓝色框里面还有两个箭头,一个是从MOSI指向MISO的,另一个是从MISO指向MOSI的,而且看起来是受到主控制逻辑的管理的,这里的两个箭头有两个作用,一是使用三线SPI时,会舍弃一个数据传输脚,使用MOSI或者MISO中的任意一个,此时这一个数据脚既要发送又要接收,就需要使用到蓝色框内的箭头;另外一个作用是使用更快的传输模式时,会使用两个数据脚同时由主机向从机或者由从机向主机写数据,这个时候也需要使用到蓝色框内的箭头。
  2. 橙色框的NSS片选本来是SPI控制器内部集成的片选脚,但是我们实际使用过程一般不用,所以在配置过程中会使用软件管理来屏蔽掉它,让它作为一个普通的GPIO口;而实际的片选脚会根据硬件连接的管脚进行配置。
    在这里插入图片描述
    类似下图,假设此时使用三线SPI的半双工模式,只有MOSI一个脚,此时数据数据输入时就需要使用到上图蓝色框内的箭头,编程下图橙色线的流程。而输出方式不变,同样的,只留下MISO亦是如此。
    在这里插入图片描述

时钟以及控制部分

首先来看时钟部分,作为主机使用时,STM32的SPI控制器需要提供Sclk时钟信号,其产生方式就是下图的波特率发生器,对于这个波特率发生器在框图中可以看出留给我们操作的只有CR1寄存器的BR[2:0]共三位,按照前面的介绍以及以前的经验,这三位肯定是用来空时分频系数的,也就是决定通信速率的,在特性那里我们提到了,SPI的最大通信速率是40Mhz也就是说,此处的分频估计最少也是一个2分频。
在这里插入图片描述
然后是主控制逻辑,主控制逻辑部分就是关于模式,单个数据线还是两个数据线,主模式还是从机模式、是否只读这些进行选择。具体的在编程手册查看寄存器SPI_CR1寄存器介绍。
最后是通信控制,通信控制部分首先是最右边橙色框内有一个数据选择器,其中一路来自上面提到得NSS,另外的一路输入来自SSI,控制选择的是SSM。这里我们选用的是使用软件从器件管理。
在这里插入图片描述
然后通讯控制还有输出上方蓝色框的各种标志位,发送完成以及接收完成这些,主要用于中断或者DMA关于这个详细的在SPI_CR2寄存器的介绍。这里仅需要知道有这个东西即可。

SPI寄存器简介

1.SPI 控制寄存器 1 (SPI_CR1)(不用于 I2S 模式)
写法:SPIX->CR1
位 0 CPHA:时钟相位 (Clock phase)
位1 CPOL:时钟极性 (Clock polarity)
位 2 MSTR:主模式选择 (Master selection) 选择主模式
位 5:3 BR[2:0]:波特率控制 (Baud rate control) 选2分频
位 6 SPE:SPI 使能 (SPI enable) 放在最后
位 7 LSBFIRST:帧格式 (Frame format) 一般选择高位先发
位8 SSI: 内部从器件选择
位 9 SSM:软件从器件管理 (Software slave management)
位9:8 设置软件管理。什么是软件管理?
NSS引脚属于硬件SPI 片选引脚。把NSS引脚设置为软件管理之后,这个引脚相当于当作普通IO口使用。为什么要把它设置成普通IO口使用。因为当SPI总线外接多个从机时,是通过片选引脚进行选择。把NSS引脚设置为软件管理之后,片选引脚就可以接任意一个IO口都可以,这就方便很多了。
把位9 和 位8 都 设置为1,就是把NSS设置为软件管理。

位 10 RXONLY:只接收 (Receive only) 全双工
位 11 DFF:数据帧格式 (Data frame format 数据位
位 15 BIDIMODE:双向通信数据模式使能 (Bidirectional data mode enable)
选择3线制SPI 还是4线制的SPI 结合硬件
2.SPI 控制寄存器 2 (SPI_CR2)
写法:SPIx->CR2
主要是中断使能和DMA使能

3.SPI 状态寄存器 (SPI_SR)
写法:SPIx->SR
位 0 RXNE:接收缓冲区非空 (Receive buffer not empty)
位 1 TXE:发送缓冲区为空 (Transmit buffer empty)

4.SPI 数据寄存器 (SPI_DR)
写法:SPIx->DR
发送数据SPI1->DR=data
接收数据data = SPI1->DR
如果是8位数据位,则数据寄存器的低8位为有效位
如果使用16为数据为,则整个DR为有效位

SPI初始化代码流程

根据上面的介绍即可总结出SPI控制器的初始化流程
伪代码:

SPI初始化
{
    
    
  /*IO口控制器配置*/
	//端口时钟使能
	//端口模式配置
	//具体复用功能配置
 	//输出类型配置
    //输出速度配置
	/*SPI1控制器配置*/
	//SPI1模块时钟使能
	//CR1
	//双向单向 全双工
	//8位数据帧
	//全双工
	//软件从器件管理//
	//主机模式
	//先发高位
	//4分频    
	//主配置
	//0.0模式
	//CR2
	//MOT模式
	//CFGR
	//SPI模式
	//SPI使能
}

SPI数据收发函数
{
    
    
  //等待发送

  发送数据
 
  //等待接收
  接收数据
}

SPI初始化代码

/*******************************************
*函数名    :spi1_init
*函数功能  :SPI1初始化配置
*函数参数  :无
*函数返回值:无
*函数描述  :
SCK------PB3   //复用输出 
MISO-----PB4   //复用输出
MOSI-----PB5   //复用输出
*********************************************/
void spi1_init(void)
{
    
    
	/*IO口控制器配置*/
	//端口时钟使能
	RCC->AHB1ENR |= (1<<1);   //B组
	//端口模式配置
	GPIOB->MODER &= ~((3<<6)|(3<<8)|(3<<10));
	GPIOB->MODER |= ((2<<6) |(2<<8)| (2<<10));
	//具体复用功能配置
	GPIOB->AFR[0] &= ~((15<<12)|  (15<<16) |  (15<<20));
	GPIOB->AFR[0] |= ((5<<12)|  (5<<16)|(5<<20));
  //输出类型配置
	GPIOB->OTYPER &= ~((1<<3) | (1<<4) | (1<<5));
  //输出速度配置
	GPIOB->OSPEEDR &= ~((3<<6)| (3<<8) | (3<<10));
	GPIOB->OSPEEDR |= ((2<<6)| (2<<8) | (2<<10));   //50M
	
	/*SPI1控制器配置*/
	//SPI1模块时钟使能
	RCC->APB2ENR |= (1<<12);
	//CR1
//	SPI1->CR1 &= ~(1<<15);    //单线双向
	SPI1->CR1 |= (1<<15);    //双线双向
	SPI1->CR1 &= ~(1<<11);    //8位数据帧
	SPI1->CR1 &= ~(1<<10);    //全双工
	SPI1->CR1 |= (1<<9);      //软件从器件管理//
	SPI1->CR1 |= (1<<8);      //主机模式
	SPI1->CR1 &= ~(1<<7);     //先发高位
	SPI1->CR1 |= (1<<3);      //4分频    
	SPI1->CR1 |= (1<<2);      //主配置
	SPI1->CR1 &= ~(3<<0);     //0.0模式
	//CR2
	SPI1->CR2 &= ~(1<<4);     //MOT模式
	//CFGR
	SPI1->I2SCFGR &= ~(1<<11);  //SPI模式
	
	//SPI使能
	SPI1->CR1 |= (1<<6);
}


/*******************************************
*函数名    :spi1_byte
*函数功能  :SPI1传输一个字节函数
*函数参数  :u8 data
*函数返回值:u8
*函数描述  :
						发送数据时候只需要关注参数
            接收数据的时候,关注返回值,
								参数随便传一个数值
*********************************************/
u8 spi1_byte(u8 data)
{
    
    
	u8 val;
	
	/*发送*/
	//等待之前的数据发送完成
	while(!(SPI1->SR & (1<<1)));
	//将要发送的数据赋值给DR
	SPI1->DR = data;

	/*接收*/
	//等待有数据就接收
	while(!(SPI1->SR & (1<<0)));
	//将DR数据赋值给变量
	val = SPI1->DR;
	
	return val;
}

SPI使用IO模拟的代码思路

找到对应的硬件设备接口
LCD_SPI2_MOSI   ---- 发送数据  PB15
LCD_SPI2_SCLK                 PB13
LCD_SPI2_MISO--------并不是屏幕上面没有这根线,而是没有使用到所以没有接
//注意使用的是IO口模拟来实现的功能,所以GPIO口需要配置为推挽输出。
SPI的初始化
{
    
    
  //打开时钟   PB
  //配置IO控制器
  //PB13   PB15通用推挽输出
}

SPI数据收发函数
{
    
    
  u8 buff;
  时钟线拉低
  数据线输出拉高
  
  for(循环八次)
  {
    
    
     //如何发送
时钟线拉低;   //控制发送
     If(data & 0x80>>i)
     {
    
    
       数据线输出拉高
     }
     Else
     {
    
    
        数据输出拉低
}
 时钟线拉高;   //接收     
}
}

由于笔者使用的屏幕不需要给主机反馈数据,所以这里少配置了一个MISO的管脚,且这里的GPIO初始化使用了GPIO的库函数来实现。


/*******************************************
*函数名    :Spi_Gpio_Init
*函数功能  :GPIO模拟SPI的初始化配置
*函数参数  :无
*函数返回值:无
*函数描述  :将GPIO配置为通用推挽输出,模拟产生SPI的时序
*********************************************/
void Spi_Gpio_Init(void)
{
    
    
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
	
	//IO控制器
	GPIO_InitTypeDef gpio_InitTypeDef; //定义了一个结构体变量
	
	gpio_InitTypeDef.GPIO_Mode = GPIO_Mode_OUT;           //通用输出模式
	gpio_InitTypeDef.GPIO_OType = GPIO_OType_PP;          //推挽输出
	gpio_InitTypeDef.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;  //一起使用的前提条件 必须是同一个端口
	gpio_InitTypeDef.GPIO_PuPd = GPIO_PuPd_NOPULL;        //无上下拉
	gpio_InitTypeDef.GPIO_Speed = GPIO_Medium_Speed;      //中速
	
	GPIO_Init(GPIOB,&gpio_InitTypeDef);
	

}

/*******************************************
*函数名    :LCD_GSend_Byte
*函数功能  :使用模拟SPI发送一个八位的数据到LCD
*函数参数  :无
*函数返回值:无
*函数描述  :模式0或者模式3
*********************************************/
void LCD_GSend_Byte(u8 data)
{
    
    
	u8 i;
	
	//空闲状态
	LCD_SCL_L;
	LCD_MOSI_H;

	//具体的发送过程
	for(i=0;i<8;i++)
	{
    
    
		LCD_SCL_L;   //发送数据
		if(data & (0x80>>i))
		{
    
    
			LCD_MOSI_H;  //发送数据1
		}
		else
		{
    
    
			LCD_MOSI_L;
		}
		LCD_SCL_H;   //拉高接收数据
	}
	//开始读取返回数据
}

总结

关于SPI的控制器实现以及GPIO,模拟实现的介绍就记录到这里,具体的实际使用,笔者抽空再单开一片,应该是LCD的显示或者是对W25Q64烧录GB2312的字库。想要那个大家可以私信或者留在评论区。然后文中如有不足欢迎批评指正。

猜你喜欢

转载自blog.csdn.net/qq_41954556/article/details/130019145
今日推荐