文章目录
前言
前面介绍了IIC协议的通讯,这一节介绍一下比较简单的SPI通讯协议,spi使用的地方也是很多的,而且也很简单,所以这一章就来介绍一下。
一、SPI协议
1.什么是SPI协议
SPI是由摩托罗拉公司开发的一种通用的数据总线。有四根信号线,分别是SCK
时序控制线、SS
片选线、MOSI
主机发送从机接收信号线和MISO
主机接收从机发送信号线。
是一个同步、全双工的通讯协议,支持一主一从和一主多从。
2.SPI连接方法
所有设备的MOSI
要连接到一起,MISO
要连接到一起,SCK
也是一样的,从机的SS
要连接到主机的指定引脚上,就如下图一样:
在配置时如果是软件模拟SPI协议,那需要将除去MISO
的所有引脚都配置为推挽输出,而MISO
配置为上拉输入。
如果是硬件SPI时,需要把除去MISO
的所有引脚配置为复用推挽输出,MISO
还是配置为上拉输入。
3.SPI的工作方式
SPI协议和IIC是不一样的,在IIC中是半双工,在发送时不能接收,接收时不能发送,并且只有一个数据寄存器来接收和发送数据。
但在SPI中是全双工的,可以边发送数据边接收数据,在SPI中也有一个寄存器,而这个寄存器是移位寄存器,如下:
在规定的时钟信号下,移位寄存器会根据设定的方式先把高位或者低位数据转移到MOSI
信号线上发送给从机,并从MISO
信号线中接收从机发送过来的数据到移位寄存器中,这就是SPI的一个工作过程。
我们要用软件模拟SPI协议就要搞清楚这个协议的工作方法和起始和结束信号。
4.SPI的起始和结束信号
起始和结束信号并不是依靠于发送一个特定的波形信号,而是只用给SS
片选引脚一个使能或者失能信号即可。
要选中哪个从机,就给对应的从机SS
引脚发送一个低电平即可选中,如果不想和该从机进行通讯,就给对应从机的SS
引脚发送一个高电平即可取消选中。
所以这里就明白了,起始信号就是给从机的SS
引脚发送低电平,结束信号就是给从机的SS
发送高电平。
5.SPI工作时序
在SPI中有四种工作时序,都可以进行使用,只不过就是在每个时许执行的内容不一样而已。
5.1 方式0
在方式0中,CPOL = 0,CPHA = 0,SCK默认空闲为低电平,SS被拉为低电平后就会将数据通过MOSI
发送给从机,在SCK
信号上升沿后将MISO
信号线中的数据转移到移位寄存器中。
也就是说在下降沿的时候将移位寄存器中的数据通过MOSI
信号线发送到从机,在上升沿时主机会从MISO
信号线中读取从机发送过来的数据到移位寄存器中。
5.2 方式1
在方式1中,CPOL = 0,CPHA = 1,SCK默认空闲为低电平,在SCK上升沿的时候将数据通过MOSI
发送给从机,在下降沿的时候将MISO
信号线中的数据转移到移位寄存器中。
5.3 方式2
在方式2中,CPOL = 1,CPHA = 0,SCK默认空闲为高电平,该模式是和方式0是反过来的,方式0是高电平采集,而方式2是低电平采集,发送也是一样的。
5.4 方式3
在方式3中,CPOL = 1,CPHA = 1,SCK默认为高电平,该模式是和方式1是发过来的,方式1是高电平发送,方式3是高电平接收。
二、软件模拟SPI协议
前面介绍了一下SPI的各种东西,这里就开始代码的实现,其实实现的方法很简单,首先配置GPIO口,起始信号和结束信号,发送接收时序的拼接,然后就结束了。
1.配置GPIO口
因为这里我们使用的是软件来模拟SPI协议,所以需要配置一下GPIO口,对于GPIO口的模式配置前面也是说过的,如果是软件模拟则需要配置为推挽输出的模式,这里我使用GPIOB下的引脚,下面是宏定义:
#define SPI_CS GPIO_Pin_4
#define SPI_MISO GPIO_Pin_6
#define SPI_MOSI GPIO_Pin_7
#define SPI_CLK GPIO_Pin_5
#define SPI_GPIO GPIOB
#define SPI_RCC RCC_APB2Periph_GPIOB
然后开始配置GPIO口:
GPIO_InitTypeDef GPIO_InitStruct = {0};
RCC_APB2PeriphClockCmd(SPI_RCC, ENABLE);
GPIO_InitStruct.GPIO_Pin = SPI_CS | SPI_MOSI | SPI_CLK;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = SPI_MISO;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
MISO引脚主要是作为输入的,这里需要配置为上拉输入。
然后封装一下输入和输出吧,这里我将所有的引脚都封装了一下:
void SPI_MOSI_Set(uint8_t set)
{
GPIO_WriteBit(SPI_GPIO, SPI_MOSI, (BitAction)set);
}
uint8_t SPI_MISO_Read(void)
{
return GPIO_ReadInputDataBit(SPI_GPIO, SPI_MISO);
}
void SPI_CLK_Set(uint8_t set)
{
GPIO_WriteBit(SPI_GPIO, SPI_CLK, (BitAction)set);
}
void SPI_CS_Set(uint8_t set)
{
GPIO_WriteBit(SPI_GPIO, SPI_CS, (BitAction)set);
}
前面IIC协议也是这样封装的,但是没有在那里着重说明。
然后就还需要在初始化中添加两句代码,第一个是将SS引脚初始化为高电平,也就是不选中,这样在初始化完成后就默认不选中,需要使用的话就调用一下SPI_CS_Set(0)
进行选中。
然后再确定一下模式,如果要使用模式0或者模式1则需要将SCK的电平拉低,默认低电平;如果使用模式2或者模式3则信息要将SCK的电平拉高,完整的初始化代码如下:
GPIO_InitTypeDef GPIO_InitStruct = {0};
RCC_APB2PeriphClockCmd(SPI_RCC, ENABLE);
GPIO_InitStruct.GPIO_Pin = SPI_CS | SPI_MOSI | SPI_CLK;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = SPI_MISO;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
SPI_CS_Set(1);
SPI_CLK_Set(0); // 这里选择的是模式0或者模式1
2.起始和结束信号
前面说了,起始信号和结束信号起始就是拉低和拉高SS引脚即可,所以这里的写法很简单,代码如下:
void SPI_Start(void)
{
SPI_CS_Set(0);
}
void SPI_Stop(void)
{
SPI_CS_Set(1);
}
3.时序编写
这里也是比较简单的,按照上面的时序图来进行代码的编写即可,这里每个都会讲一个例子来说明。
3.1 方式0
方式0是在SS拉低后就开始数据的传输,所以这里可以直接先把数据通过MOSI
进行发送,然后拉高SCK后再通过MISO
获取接收的数据,最后再拉低SCK为下一次传输做准备,代码如下:
uint8_t SPI_ReadWrite(uint8_t date)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
SPI_MOSI_Set(date & 0x80);
date <<= 1; // 将高位已经传输的数据位移出去
SPI_CLK_Set(1);
if (SPI_MISO_Read() == 1)
date |= 0x01; // 移入的数据放在最后
SPI_CLK_Set(0);
}
return date;
}
3.2 方式1
uint8_t SPI_ReadWrite(uint8_t date)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
SPI_CLK_Set(1);
SPI_MOSI_Set(date & 0x80);
date <<= 1;
SPI_CLK_Set(0);
if (SPI_MISO_Read() == 1)
date |= 0x01;
}
return date;
}
在模式1中,在SCK的上升沿时发送数据,在下降沿时接收数据。
3.3 方式2
uint8_t SPI_ReadWrite(uint8_t date)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
SPI_MOSI_Set(date & 0x80);
date <<= 1; // 将高位已经传输的数据位移出去
SPI_CLK_Set(0);
if (SPI_MISO_Read() == 1)
date |= 0x01; // 移入的数据放在最后
SPI_CLK_Set(1);
}
return date;
}
本质上方式2只是在方式0的基础上的电平翻转了一下,并且在初始化的那里需要将SCK的电平默认为高电平即可。
3.4 方式3
uint8_t SPI_ReadWrite(uint8_t date)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
SPI_CLK_Set(0);
SPI_MOSI_Set(date & 0x80);
date <<= 1;
SPI_CLK_Set(1);
if (SPI_MISO_Read() == 1)
date |= 0x01;
}
return date;
}
方式3和方式1一样,只不过电平需要翻转,并且SCK的默认值得为高电平。
4.时序编写
时序的编写起始很简单,之前就已经写好了的,就是发送一个起始信号,然后发送内容接收内容最后再发送一个结束信号即可。
三、硬件SPI协议
熟悉了软件模拟SPI协议后应该知道SPI协议的时序图和方式,硬件的SPI使用的是硬件来进行实现SPI协议,STM32F103中有两个SPI组件,分别是SPI1和SPI2。
这两个组件在的总线是不一样的,SPI1的总线是在APB2上,而SPI2的总线是在APB1上的,两个SPI的频率是不一样的,需要使用高频率的就使用SPI1,低频率的就所用SPI2。
1.SPI内部结构
下图就是硬件SPI的内部结构:
可以看到MISO和MOSI引脚都连接到了一个移位寄存器,可以从移位寄存器中读出数据到接收缓冲区中,然后再到数据总线上,也可以通过数据总线向发送缓冲区中写入需要发送的数据,发送寄存器再发送给移位寄存器中。
SCK是由波特率发生器来进行控制的,通过APB2总线或者APB1总线上的频率来进行分频后得到一个输出频率,这个输出频率就是SCK的时钟频率。
NSS是一个主从控制引脚,这个一般都是使用GPIO口直接控制SS引脚来选中从机,所以这个引脚一般不进行使用。
硬件SPI其实和硬件IIC一样,需要通过判断一些事件来知道是否发送或者接收到一些内容,只不过在硬件SPI中是使用的是标志位来进行读取。
这里使用的比较多的是TXD发送缓冲区标志位、TXNE接收。
2.软件实现
了解了内部结构后我们就可以开始来书写代码了,书写的逻辑就是配置GPIO口,配置SPI,使能SPI,使用SPI发送数据,这样就是一个完整的配置和使用过程。
2.1配置GPIO口
这里配置就和软件不一样了,这里的配置需要使用复用推挽输出,因为SPI是属于GPIO口的复用功能,所以需要使用复用的输出,而输入就不用了,直接使用上拉即可,所以这里的代码就如下:
GPIO_InitTypeDef GPIO_InitStruct = {0};RCC_APB2PeriphClockCmd(SPI_RCC, ENABLE);GPIO_InitStruct.GPIO_Pin = SPI_CS;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = SPI_MOSI | SPI_CLK;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = SPI_MISO;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
这里的SS不需要使用复用模式,因为是直接输出内容的。
2.2 配置SPI
这里我是使用的是SPI1进行配置,上面的那些宏定义把GPIOB换成GPIOA就可以了,这里配置的方法其实也是使用一个结构体来进行配置,对应的宏定义如下:
typedef struct
{
uint16_t SPI_Direction; /*!< 指定 SPI 单向或双向数据模式。
此参数的值可以是 @ref SPI_data_direction */
uint16_t SPI_Mode; /*!< 指定 SPI 操作模式。
此参数的值可以是 @ref SPI_mode */
uint16_t SPI_DataSize; /*!< 指定 SPI 数据大小。
此参数的值可以是 @ref SPI_data_size */
uint16_t SPI_CPOL; /*!< 指定串行时钟稳定状态。
此参数的值可以是 @ref SPI_Clock_Polarity */
uint16_t SPI_CPHA; /*!< 指定位捕获的时钟活动边沿。
此参数的值可以是 @ref SPI_Clock_Phase */
uint16_t SPI_NSS; /*!< 指定 NSS 信号是否由
硬件(NSS 引脚)或使用 SSI 位的软件。
此参数的值可以是 @ref SPI_Slave_Select_management */
uint16_t SPI_BaudRatePrescaler; /*!< 指定波特率预分频器值,该值将为
用于配置发送和接收 SCK 时钟。
此参数的值可以是 @ref SPI_BaudRate_Prescaler。
@note 通信时钟派生自主机时钟。从时钟无需设置。 */
uint16_t SPI_FirstBit; /*!< 指定数据传输是从 MSB 位还是 LSB 位开始。
此参数的值可以是 @ref SPI_MSB_LSB_transmission */
uint16_t SPI_CRCPolynomial; /*!< 指定用于 CRC 计算的多项式。 */
}SPI_InitTypeDef;
只用看这些介绍即可了,下面就开始进行配置:
SPI_InitTypeDef SPI_InitStruct = {0};RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; // 波特率分频128
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // cpha = 0
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // cpol = 0
SPI_InitStruct.SPI_CRCPolynomial = 7; // 默认为7
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; // 数据长度
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // spi两根线全双工模式
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先行
SPI_InitStruct.SPI_Mode = SPI_Mode_Master; // 作为主机
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 软件控制SS
SPI_Init(SPI1, &SPI_InitStruct);
这样就配置完成了,接下来就使能SPI:
SPI_Cmd(SPI1, ENABLE);
别忘记给SCK一个默认值就可以了,这里选择的是方式0,所以默认的是低电平。
2.3 发送数据
发送也是比较简单,就是判断发送缓冲区中的是否为空,如果为空发送数据,判断接收寄存器是否为空,如果为空接收接收寄存器中的值,这样依次执行即可,所以代码如下:
uint8_t SPI_ReadWrite(uint8_t date)
{
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 判断输入缓冲区是否为空
SPI_I2S_SendData(SPI1, date); // 发送数据
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); // 判断接收缓冲区是否为空
return SPI_I2S_ReceiveData(SPI1); // 接收数据
}
如果接收缓冲区中有数据,那TXE
中的值为RESET也就是0,当为空了才置为1,同理接收寄存器也是一样得到,如果接收寄存器中有数据就为1,没有数据就为0。
总结
SPI协议还是比IIC协议简单,但是就是SPI需要的线比IIC的多,对于SPI来说也是需要多去练习才可以学会使用,这里只是讲一下理论,如果需要使用SPI和其它设备进行通讯的话等后面我搞一个专题专门来讲一些驱动的开发。