1. STM32 SPI
1.1 STM32的SPI接口
SPI可以设置为主、从两种模式,并且支持全双工模式,而配置为主、从模式或软件、硬件NSS,在操作上有很大的区别。由于一个项目需求,笔者对STM32的硬件模式和主从模式进行了一些研究,走了很多弯路,也查询了很多资料,现在终于调通了,因此写一篇文章记录调试心得,以及很多需要注意的地方。
以下是STM32 SPI接口的介绍:
-
3线全双工同步传输;
-
8或16位传输帧格式选择;
-
主或从操作,支持多主模式;
-
主模式和从模式下均可以由软件或硬件进行NSS管理:主/从操作模式的动态改变;
-
可编程的时钟极性和相位;
-
可编程的数据顺序,MSB在前或LSB在前;
-
可触发中断的专用发送和接收标志;
-
SPI总线忙状态标志;
-
支持可靠通信的硬件CRC;
-
可触发中断的主模式故障、过载以及CRC错误标志;
-
支持DMA功能的1字节发送和接收缓冲器:产生发送和接受请求。
本文主要探讨主模式和从模式NSS硬件和软件管理。
2. SPI Master 初始化及测试
2.1 硬件NSS模式
以下是初始化代码
void SPI1_Configuration(void)
{
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
/************打开时钟*************/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );
RCC_APB2PeriphClockCmd( RCC_APB2Periph_AFIO, ENABLE );
/************引脚配置*************/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; //SPI_NSS
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_4);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //SPI_SCK
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //SPI_MISO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //SPI_MOSI
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/************SPI初始化配置*************/
SPI_Cmd(SPI1, DISABLE); //必须先禁用,才能改变MODE
SPI_InitStructure.SPI_Direction =SPI_Direction_2Lines_FullDuplex; //两线全双工
SPI_InitStructure.SPI_Mode =SPI_Mode_Master; //主
SPI_InitStructure.SPI_DataSize =SPI_DataSize_8b; //8位
SPI_InitStructure.SPI_CPOL =SPI_CPOL_Low; //CPOL=0
SPI_InitStructure.SPI_CPHA =SPI_CPHA_1Edge; //CPHA=0
SPI_InitStructure.SPI_NSS =SPI_NSS_Hard; //硬件NSS
SPI_InitStructure.SPI_BaudRatePrescaler =SPI_BaudRatePrescaler_64; //64分频
SPI_InitStructure.SPI_FirstBit =SPI_FirstBit_MSB; //高位在前
SPI_InitStructure.SPI_CRCPolynomial =7; //CRC7
SPI_Init(SPI1,&SPI_InitStructure);
// SPI_Cmd(SPI1, ENABLE); //先不打开SPI
SPI_SSOutputCmd(SPI1, ENABLE); //SPI的NSS引脚控制开启
}
SPI配置为主模式,采用硬件NSS有几点需要注意,若采用硬件NSS,一定要把NSS引脚输出设置为GPIO_Mode_AF_PP,否则程序无法正确控制。
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
2.2 软件NSS模式
软件NSS的初始化步骤大同小异,有几个地方不一样,需要注意一下
①NSS引脚修改为GPIO_Mode_Out_PP
/************引脚配置*************/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; //SPI_NSS
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_4);
②将NSS配置改为SPI_NSS_Soft,然后打开SPI,屏蔽SPI_SSOutputCmd()这个函数。
/************SPI初始化配置*************/
SPI_Cmd(SPI1, DISABLE); //必须先禁用,才能改变MODE
SPI_InitStructure.SPI_Direction =SPI_Direction_2Lines_FullDuplex; //两线全双工
SPI_InitStructure.SPI_Mode =SPI_Mode_Master; //主
SPI_InitStructure.SPI_DataSize =SPI_DataSize_8b; //8位
SPI_InitStructure.SPI_CPOL =SPI_CPOL_Low; //CPOL=0
SPI_InitStructure.SPI_CPHA =SPI_CPHA_1Edge; //CPHA=0
SPI_InitStructure.SPI_NSS =SPI_NSS_Soft; //软件NSS
SPI_InitStructure.SPI_BaudRatePrescaler =SPI_BaudRatePrescaler_64; //64分频
SPI_InitStructure.SPI_FirstBit =SPI_FirstBit_MSB; //高位在前
SPI_InitStructure.SPI_CRCPolynomial =7; //CRC7
SPI_Init(SPI1,&SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE); //打开SPI
//SPI_SSOutputCmd(SPI1, ENABLE); //SPI的NSS引脚控制开启
2.3 主函数
2.3.1 硬件NSS模式
硬件NSS模式的操作步骤和软件NSS模式可谓天差地别,首先初始化的时候不需要打开SPI,而且需要单独配置打开NSS引脚,否则NSS引脚会一直输出低电平,无法控制。
其次是操作步骤,硬件NSS模式下,每一次数据读写都需要先打开SPI,操作完成后再关闭SPI,必须要按照这个步骤来,否则NSS引脚一直会是低电平。
这也许就是很多人认为硬件NSS有BUG,无法正确选通,而我也在网上查了很多资料,发现很少有人用硬件NSS。
int main()
{
u8 SPI_TX=10,SPI_RX=0; //
SPI1_Configuration(); //spi初始化
while(1)
{
SPI_Cmd(SPI1,ENABLE); //启动SPI
/**************向从设备发送一个字节*************/
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI1,SPI_TX);
/**************保存将接收到的数据*************/
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET);
SPI_RX = SPI_I2S_ReceiveData(SPI1);
SPI_Cmd(SPI1,DISABLE); //关闭SPI
SPI_TX++; //发送的值+1
delay_100ms(10); //延时1秒
if(SPI_TX>50) {SPI_TX=10;}
}
}
- 效果测试
上图是刚刚的代码运行的效果,用逻辑分析仪抓取的SPI波形。
从设备同样是STM32,设置为硬件从模式,采用硬件NSS控制,设置方法后文会讲。
可以看到NSS能正确拉低,主设备能正确收到从设备返回的数据。
从设备设置的是收到什么就会返回什么,由于SPI的特性是在发送的同时接收数据,且从设备不能主动向主设备发送数据,因此在发送新数据的时候才能收到上此次发送的旧数据。
2.3.2 软件NSS模式
由于代码差不多,就只贴while(1)的函数
while(1)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_4); //拉低CS
/**************向从设备发送一个字节*************/
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI1,SPI_TX);
/**************保存将接收到的数据*************/
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET);
SPI_RX = SPI_I2S_ReceiveData(SPI1);
GPIO_SetBits(GPIOA,GPIO_Pin_4); //拉高CS
SPI_TX++; //发送的值+1
delay_100ms(10); //延时1秒
if(SPI_TX>50) {SPI_TX=10;}
}
- 效果测试
软件NSS模式下,SPI是常开的,读写数据的时候需要人为控制CS引脚
可以看到CS线在最后一个CLK脉冲发送完成后就立即拉高了,和硬件模式有一点区别
如果去掉这两个函数会怎样呢?
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET);
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET);
主函数改为:
while(1)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_4); //拉低CS
/**************向从设备发送一个字节*************/
SPI_I2S_SendData(SPI1,SPI_TX);
/**************保存将接收到的数据*************/
SPI_RX = SPI_I2S_ReceiveData(SPI1);
delay_us(2); //延时2微秒
GPIO_SetBits(GPIOA,GPIO_Pin_4); //拉高CS
SPI_TX++; //发送的值+1
delay_100ms(10); //延时1秒
if(SPI_TX>50) {SPI_TX=10;}
}
- 效果测试
可以看到,程序似乎没有按照预想顺序进行,延时函数好像没有在SPI_I2S_ReceiveData()结束后才执行。
这也是使用硬件SPI常犯的错误,因为在使用硬件SPI时,外设在进行数据读写的时候是不占用内核时间的,内核把数据丢给外设寄存器就完事了,内核只需要等待外设返回结束标志位,刚才屏蔽掉的两个函数就是在等待外设返回结束标志,而这个等待时间其实也可以做很多事情。
经过测试,内核对外设的操作只用了大概0.4uS。
若不注意这点,可能会出现SPI正在读取芯片数据过程中,芯片的CS线被拉高,芯片可能就停止发送数据了,导致最后读取的数据异常。
以上就是SPI Master模式中的硬件NSS和软件NSS的设置和控制,接下来是SPI Slave模式的设置步骤。
3. SPI Slave 初始化及测试
3.1 硬件NSS模式
以下是初始化代码
void SPI1_Config_Slave(void)
{
/************打开时钟*************/
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_SPI1 , ENABLE );
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );
RCC_APB2PeriphClockCmd( RCC_APB2Periph_AFIO , ENABLE );
/************引脚配置*************/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; //SPI_NSS
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
// GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_4);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //SPI_SCK
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
// GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //SPI_MISO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; //复用开漏输出(多从机)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //SPI_MOSI
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
// GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/************SPI初始化配置*************/
SPI_Cmd(SPI1, DISABLE); //必须先禁能,才能改变MODE
SPI_InitStructure.SPI_Direction =SPI_Direction_2Lines_FullDuplex; //两线全双工
SPI_InitStructure.SPI_Mode =SPI_Mode_Slave; //从机
SPI_InitStructure.SPI_DataSize =SPI_DataSize_16b; //8位
SPI_InitStructure.SPI_CPOL =SPI_CPOL_Low; //CPOL=1时钟悬空高
SPI_InitStructure.SPI_CPHA =SPI_CPHA_1Edge; //CPHA=1 数据捕获第2个
SPI_InitStructure.SPI_NSS =SPI_NSS_Hard; //硬件NSS
SPI_InitStructure.SPI_BaudRatePrescaler =SPI_BaudRatePrescaler_32; //32分频(从机没用)
SPI_InitStructure.SPI_FirstBit =SPI_FirstBit_MSB; //高位在前
SPI_InitStructure.SPI_CRCPolynomial =7; //CRC7
SPI_Init(SPI1,&SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
从机硬件NSS初始化和软件NSS初始化大同小异,有几个注意的地方是:
不同于主机,从机很多IO口都要改为输入模式。
然后在SPI_MISO引脚的配置上,笔者这里用的是开漏输出,因为一个主机上挂了很多从机,如果用上拉输出的话可能会造成很多问题,在这条总线上用的是外部电阻上拉。
但是如果是单从机的话,如果主机设置的是浮空输入,那就很有可能导致主机无法收到数据,因此要根据自己的实际使用情况灵活设置