STM32硬件SPI主模式(硬件NSS,SPI Master)
現実逃避と
2019年08月29日 00:01

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_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-NSS-HARD-TX

上图是刚刚的代码运行的效果,用逻辑分析仪抓取的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;}

}

  **效果测试**

SPI-NSS-SOFT-TX

软件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-NSS-SOFT-TX2

可以看到,程序似乎没有按照预想顺序进行,延时函数好像没有在SPI_I2S_ReceiveData()结束后才执行。

这也是使用硬件SPI常犯的错误,因为在使用硬件SPI时,外设在进行数据读写的时候是不占用内核时间的,内核把数据丢给外设寄存器就完事了,内核只需要等待外设返回结束标志位,刚才屏蔽掉的两个函数就是在等待外设返回结束标志,而这个等待时间其实也可以做很多事情。

经过测试,内核对外设的操作只用了大概0.4uS。

若不注意这点,可能会出现SPI正在读取芯片数据过程中,芯片的CS线被拉高,芯片可能就停止发送数据了,导致最后读取的数据异常。