B站账号:小光学嵌入式


  • 大家好哇!我是小光,嵌入式爱好者,一个想要成为系统架构师的大二学生。
  • 最近开始系统性补习STM32基础知识,规划有:串口通信,Github,Ucos等等。
  • 今天总结一下串口通信之stm32-SPI。
  • 感谢你的阅读,不对的地方欢迎指正。

一.原理讲解

请跳转->串口通信————UART、I2C、SPI详解(总结篇
从上面的文章中,我们知道SPI的通信方式是:全双工、同步、串口通信。
接线有四根:
MOSI———主机发送从机接收
MISO———主机接收从机发送
SCL————时钟信号
CS/SS————片选信号
传输步骤
1.主机输出时钟信号

2.主机拉低SS/CS引脚,激活从机

3.主机通过MOSI将数据发送给从机

4.如果需要相应,则从机通过MISO将数据返回给从机

二.驱动编写

接口定义与初始化(固件库)

程序配置步骤:

  1. 配置相关引脚的复用功能,使能SPIx时钟
  2. 初始化SPIx,设置SPIx的工作模式
  3. 使能SPIx
  4. SPI传输数据,查看SPI传输状态

1 . 配置相关引脚的复用功能,使能SPIx时钟

RCC_APB2PeriphClockCmd(    RCC_APB2Periph_GPIOA, ENABLE );//PORTB时钟使能 
RCC_APB1PeriphClockCmd(    RCC_APB2Periph_SPI1,  ENABLE );//SPI2时钟使能

首先使能SPI的IO口时钟和SPI1的时钟,从下面的SPI 1重映射表可以看出来,我们使用PA4-7引脚配置SPI1,所以使能对应的引脚,这里没有使用重映射,所以就不用调用void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);这个函数。
SPI1是APB2时钟,SPI2和SPI3是APB1时钟。

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //PA5/6/7复用推挽输出 
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOB
     GPIO_SetBits(GPIOA,GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7);  //PA5/6/7上拉

设置对应的引脚,然后根据下表把引脚设置为复用推挽输出,为社么只有三根线呢?
当然是少了SPI_NSS,这个呢就要在使用多个从机时去不同的配置它,硬件SPI也就是通过配置多个NSS引脚连接多个从机,哪一个拉低,就选择哪一个从机进行传输数据。

主从机连接方式,这样我们使用多个IO作为片选信号,从而链接多个从机:

2. 初始化SPIx,设置SPIx的工作模式

首先我们看一下SPI结构体的定义:

typedef struct
{
  uint16_t SPI_Direction;          //设置SPI单双向

  uint16_t SPI_Mode;               //设置SPI主/从机模式

  uint16_t SPI_DataSize;            //设置SPI数据帧的长度 8/16帧
  uint16_t SPI_CPOL;               //设置时钟的极性

  uint16_t SPI_CPHA;                //设置时钟的相位,可选奇/偶边沿采样

  uint16_t SPI_NSS;                //硬件控制还是软件控制

  uint16_t SPI_BaudRatePrescaler;   //设置时钟分频因子

  uint16_t SPI_FirstBit;           //设施LSB/MSB先行,MSB就是二进制第一位,LSB就是最后一位 

  uint16_t SPI_CRCPolynomial;       //设置CRC校验表达式
}SPI_InitTypeDef;

其中根据配置SPI有四种模式:
时钟极性(CPOL)定义了时钟空闲状态电平
CPOL=0,表示当SCLK=0时处于空闲态,所以有效状态就是SCLK处于高电平时
CPOL=1,表示当SCLK=1时处于空闲态,所以有效状态就是SCLK处于低电平时
时钟相位(CPHA)定义数据的采集时间。
CPHA=0,在时钟的第一个跳变沿(上升沿或下降沿)进行数据采样。,在第2个边沿发送数据
CPHA=1,在时钟的第二个跳变沿(上升沿或下降沿)进行数据采样。,在第1个边沿发送数据
就可以配置四种模式。
我们的SPI模式配置为:

    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;        //设置SPI工作模式:设置为主SPI
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;        //设置SPI的数据大小:SPI发送接收8位帧结构
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;        //串行同步时钟的空闲状态为高电平
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;    //串行同步时钟的第二个跳变沿(上升或下降)数据被采样
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;        //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;        //定义波特率预分频的值:波特率预分频值为256
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;    //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
    SPI_InitStructure.SPI_CRCPolynomial = 7;    //CRC值计算的多项式
    SPI_Init(SPI2, &SPI_InitStructure);  //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器

3. 使能SPIx

SPI_Cmd(SPI1, ENABLE); //使能SPI外设

所以SPI初始化为:

void SPI1_Init(void)
{
     GPIO_InitTypeDef GPIO_InitStructure;
  SPI_InitTypeDef  SPI_InitStructure;

    RCC_APB2PeriphClockCmd(    RCC_APB2Periph_GPIOA, ENABLE );//PORTB时钟使能 
    RCC_APB1PeriphClockCmd(    RCC_APB2Periph_SPI1,  ENABLE );//SPI2时钟使能     

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //PA5/6/7复用推挽输出 
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOB

     GPIO_SetBits(GPIOA,GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7);  //PA5/6/7上拉

    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;        //设置SPI工作模式:设置为主SPI
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;        //设置SPI的数据大小:SPI发送接收8位帧结构
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;        //串行同步时钟的空闲状态为高电平
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;    //串行同步时钟的第二个跳变沿(上升或下降)数据被采样
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;        //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;        //定义波特率预分频的值:波特率预分频值为256
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;    //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
    SPI_InitStructure.SPI_CRCPolynomial = 7;    //CRC值计算的多项式
    SPI_Init(SPI1, &SPI_InitStructure);  //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器

    SPI_Cmd(SPI1, ENABLE); //使能SPI外设

    SPI1_ReadWriteByte(0xff);//启动传输         
}

4. SPI传输数据

SPI通信时,主机和从机传输数据就是一次数据交换,会将双方移位寄存器的数据交换,我们如果只是发送数据,那就不用管接收数据就可以了。如果要接收一个字节数据,那就必须先发送一个字节的数据。

//SPIx 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
u8 SPI1_ReadWriteByte(u8 TxData)
{        
    u8 retry=0;                     
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) //检查指定的SPI标志位设置与否:发送缓存空标志位
        {
        retry++;
        if(retry>200)return 0;
        }              
    SPI_I2S_SendData(SPI1, TxData); //通过外设SPIx发送一个数据
    retry=0;

    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET) //检查指定的SPI标志位设置与否:接受缓存非空标志位
        {
        retry++;
        if(retry>200)return 0;
        }                                  
    return SPI_I2S_ReceiveData(SPI1); //返回通过SPIx最近接收的数据                        
}

总结

这次简单介绍了SPI的原理,以及使用固件库如何驱动SPI,初始化和SPI的读写。串口通信就到这里了,据我了解还有CAN和IIS的通信方式,之后可能会去学习。
我们一般在用到SPI的时候是有很多传感器或者其他器件使用的是SPI的协议去传输数据,所以SPI还是需要去掌握的。感谢阅读!