STM32学习笔记(15)——SPI协议

Posted Mount256

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32学习笔记(15)——SPI协议相关的知识,希望对你有一定的参考价值。

一、SPI协议简介

SPI(Serial Peripheral Interface)即串行外设总线接口,是由摩托罗拉公司提出的一种高速通讯协议,采用全双工、同步通信方式。其通讯协议较 I2C 简单,常常在单片机系统和一些要求通讯速率较高的场合中应用。

1. 物理层

在这里插入图片描述

SPI的物理层由一个主机和一个或多个从机组成。主机和每台从机之间均有 4 条线组成,其中 SCK、MISO、MOSI 这三条线是共用的,而 SS 线是每台从机与主机单独连接。下面来一一介绍这 4 条线(信号):

  • SS/NSS/CS(片选信号):每个从机都有一条单独的 SS 线与主机相连。当主机要选中某台从机进行通讯时,需要发送低电平信号给从机的 SS,从机的片选信号有效,即从机被选中,接着主机开始与从机进行 SPI 通讯。当 SS 信号为高电平时,从机片选信号为禁止,此时通讯结束。因此,SPI 通讯以 SS 为低电平信号开始,以高电平信号结束。

  • SCK/SCLK(时钟信号):主机产生时钟信号,用于通讯的同步。它决定了通讯的速率,两个设备进行通讯时,通讯速率会受限于低速设备。需要注意的是,只有主机控制时钟信号,从机的时钟完全由主机提供,当时钟信号发生跳变时,从机才会采集数据;当没有发生跳变时,从机不会采集数据。另外,SPI 协议并没有硬性规定时钟的频率范围是多少,这也是 SPI 协议的通讯速率快的原因。通讯状态下 SCK 处于互补推挽输出状态;空闲状态下处于高阻态

  • MOSI(Master Output & Slave Input,主设备输出/从设备输入):主机的数据从 MOSI 线输出后,从机从 MOSI 线读入数据,此时的通讯方向为从主机到从机。通讯状态下 MOSI 处于互补推挽输出状态;空闲状态下处于高阻态

  • MISO(Master Input & Slave Output,主设备输入/从设备输出):从机的数据从 MISO 线输出后,主机从 MISO 线读入数据,此时的通讯方向为从从机到主机。对于主机而言,MISO 总是处于高阻输入状态

2. 协议层

下图为 SPI 协议通讯图,为方便说明已在图中标出红色数字:

在这里插入图片描述

(1) 通讯的开始与停止

标号 1 处是NSS信号由高电平跳变为低电平,说明SPI通讯开始。当从机检测到 NSS 信号变为低电平后,就知道自己被主机选中了,开始与主机进行通讯。

标号 6 处是NSS信号由低电平跳变为高电平,说明SPI通讯结束。当从机检测到 NSS 信号变为高电平后,就知道自己的选中状态被取消了,结束与主机进行通讯。

(2)时钟极性CPOL、时钟相位CPHA

SPI 传输协议中一个很重要的问题是:传输的数据是什么时候被采集呢?是上升沿还是下降沿呢?这个与时钟极性和时钟相位有关,首先来介绍一下两者的功能。

  • 时钟极性CPOL:SCLK 在空闲状态下是一直为高电平或低电平的,通过时钟极性 CPOL 可以控制空闲时 SCLK 的极性,进而决定了起始信号与停止信号的触发方式。当 CPOL = 0 时,空闲状态下的 SCLK 处于低电平,起始信号由 SCLK 的上升沿触发,停止信号由 SCLK 的下降沿触发;当 CPOL = 1 时,空闲状态下的 SCLK 处于高电平,起始信号由 SCLK 的下降沿触发,停止信号由 SCLK 的上升沿触发。

  • 时钟相位CPHA:数据的采集可以在 SCLK 的上升沿触发,也可以在下降沿触发,通过时钟相位 CPHA 可以控制 MISO 和 MOSI 数据采样的触发方式。当 CPHA = 0 时,数据采样发生在 SCLK 的奇数边沿,偶数边沿则切换到下一位数据;当 CPHA = 1 时,数据采样发生在 SCLK的偶数边沿,奇数边沿则切换到下一位数据。

按照这种方式,CPHA 和 CPOL 共同决定了数据采样时的触发边沿。下面我们来看看两者是如何影响数据采样的触发方式的:

在这里插入图片描述

如上图所示,当 CPHA = 0,CPOL = 0 时,数据采样发生在上升沿,切换数据位则发生在下降沿;当 CPHA = 0,CPOL = 1 时,数据采样发生在下降沿,切换数据位则发生在上升沿。空闲状态时 SCLK 始终为低电平。

在这里插入图片描述

如上图所示,当 CPHA = 1,CPOL = 0 时,数据采样发生在下降沿,切换数据位则发生在上升沿;当 CPHA = 1,CPOL = 1 时,数据采样发生在上升沿,切换数据位则发生在下降沿。空闲状态时 SCLK 始终为高电平。

因此,SPI 实际上被分为了四种不同的工作状态,我们以表格形式总结一下:

SPI模式CPHA(时钟相位)CPOL(时钟极性)数据采样发生在SCLK空闲时的SCLK
000上升沿低电平
101下降沿高电平
210上升沿低电平
311下降沿高电平

通常情况下,模式 0 和模式 3 用的较多。只有主机和从机采用相同的模式才能够正常通讯。

二、STM32的SPI外设

STM32 有多个 SPI 外设,STM32F10x 系列就拥有 3 个 SPI。SPI 的功能框图如下图所示:

在这里插入图片描述

1. 通讯引脚

SPI 一共有 4 个引脚,不同 SPI 对应的引脚也不同:

引脚SPI1SPI2SPI3
NSSPA4PB12PA15下载口的TDI
CLKPA5PB13PB3下载口的TDO
MISOPA6PB14PB4下载口的NTRST
MOSIPA7PB15PB5

实际应用中,一般不使用 SPI 外设的标准 NSS 信号线,而是更简单地使用普通的 GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。

注意 SPI3 中有些引脚与 SWD 下载引脚重合了,这对我们使用和学习有影响,因此在做 SPI 实验时应避免使用 SPI3。STM32中文参考手册也对此给出了提示:

警告:由于SPI3/I2S3的部分引脚与JTAG引脚共享(SPI3_NSS/I2S3_WS与JTDI,SPI3_SCK/I2S3_CK与JTDO),因此这些引脚不受IO 控制器控制,他们(在每次复位后)被默认保留为被默认保留为JTAG用途。如果用户想把引脚配置给SPI3/I2S3必须(在调试时)关闭JTAG并切换至SWD接口,或者(在标准应用时)同时关闭JTAG和SWD接口。

2. 时钟控制逻辑

之前说过,时钟信号由 SCK 提供,而 SCK 又是由波特率发生器提供,它是由控制寄存器SPI_CR1BR[2:0] 控制。可以得知:波特率与PCLK(即APB)的频率有关。

在这里插入图片描述

那么如何确定 PCLK 的值呢?我们可以查看总线架构图:

在这里插入图片描述

如图所示,SPI1 挂载到 APB1(即PCLK1),SPI2、SPI3挂载到 APB2(即PCLK2)。对于 SPI1,因为 APB1 最高为 36MHz,所以 SPI1 时钟最高为36MHz / 2 = 18MHz;对于SPI2、SPI3,因为 APB2 最高为 72MHz,所以两者时钟最高为72MHz / 2 = 36MHz

3. 数据控制逻辑

SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的数据来源来源于接收缓冲区及发送缓冲区。

数据寄存器SPI_DR 对应两个缓冲区:一个用于写(发送缓冲);另外一个用于读(接受缓冲)。写操作将数据写到发送缓冲区;读操作将返回接收缓冲区里的数据。

我们可以通过写数据寄存器SPI_DR 把数据填充到发送缓冲区中;通过读数据寄存器SPI_DR,可以获取接收缓冲区中的内容。

数据帧长度可以通过控制寄存器SPI_CR1DFF配置成8位及16位模式;而 LSBFIRST可配置移位寄存器的发送方向:选择 MSB(高位数据)发送还是 LSB(低位数据)先发送。

4. 整体控制逻辑

控制逻辑根据外设的工作状态修改状态寄存器SPI_SR,只要读取状态寄存器相关的寄存器位,就可以了解 SPI 的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生 SPI 中断信号、DMA 请求及控制 NSS 信号线。

5. STM32的SPI通讯过程

当 STM32 作为主机时,与从机的通讯过程如下图:

在这里插入图片描述

首先需要知道一个大概的过程:主机输出的数据都要通过缓冲区(对应数据寄存器SPI_DR)才能进入移位寄存器,最后再发送;主机读入的数据都要通过移位寄存器,再移入缓冲区。因此,缓冲区和移位寄存器中的数据在同一时刻是不一样的,它们正好相差了一次通信时间

(1)从主机发送数据到从机的详细过程(以 CPHA=1、CPOL=1 为例)

在这里插入图片描述

  • 往发送缓冲区(即数据寄存器SPI_DR)写入一个数据(比如图中的 0XF1),此时触发 SCK 信号进入忙碌状态。此时状态寄存器SPI_SRBSY(忙标志,Busy flag)位为 1,表示 SPI 开始通信;TXE(发送缓冲为空,Transmit buffer empty)位为 0,表示发送缓冲非空
  • 由于移位寄存器还没有数据,因此发送缓冲区很快将数据 0xF1 转移到移位寄存器中。此时TXE位为 1,表示发送缓冲为空。移位寄存器将数据一位一位传到 MOSI 线。
  • 等待至TXE位为 1后,软件往发送缓冲区写入第二个数据(比如 0xF2),TXE位变为 0,表示发送缓冲非空。由于移位寄存器还没将前一个数据 0xF1 全部发送出去,因此新数据 0xF2 暂时存到缓冲区。
  • 前一个数据发送完毕后,新数据 0xF2 转移到移位寄存器中,此时TXE位为 1,表示发送缓冲为空。移位寄存器将数据一位一位传到 MOSI 线。
  • 如此往复:等待TXE位由0变为 1,软件往发送缓冲区写入新数据,前一个数据发送完,新数据就往移位寄存器补,并将其一位一位发出去。发送出去的数据总比新写入缓冲区的数据少一次发送周期。

(2)从从机接收数据到主机的详细过程(以 CPHA=1、CPOL=1 为例)

在这里插入图片描述

  • 必须先使 SPI 启动(即让 SCK 处于忙碌状态),才能接收数据。因此,需要先往发送缓冲随意写入一个数据,以触发 SCK 信号进入忙碌状态。写入数据后,状态寄存器SPI_SRBSY位为 1,表示 SPI 开始通信;RXNE(接收缓冲非空,Receive buffer not empty)位为 0,表示接收缓冲为空
  • 移位寄存器一位位接收到从 MISO 来的数据 0xA1,将数据转移到接收缓冲区中,然后进行下一次数据接收;此时RXNE位为 1,表示接收缓冲非空,表明软件可以开始读取数据 0xA1。
  • 待数据读取完后,RXNE位为 0,表示接收缓冲为空。此时移位寄存器将新数据 0xA2 转移到接收缓冲区中,RXNE位为 1,表示接收缓冲非空,表明软件可以开始读取数据 0xA2
  • 如此往复:移位寄存器一位位接收数据,然后将数据转移到接收缓冲区,然后继续接收下一次数据;软件等待RXNE位由 0 变为 1 后,从接收缓冲区读取数据。接收到的数据总比新读取缓冲区的数据多一个发送周期。

三、SPI的初始化结构体和库函数

1. SPI的初始化结构体

以下为 SPI 的结构体定义:

typedef struct
{
  uint16_t SPI_Direction;           /*!< 配置 SPI 的单/双向模式 */

  uint16_t SPI_Mode;                /*!< 配置 SPI 的主/从机模式 */

  uint16_t SPI_DataSize;            /*!< 配置 SPI 的数据帧长度(8/16位) */

  uint16_t SPI_CPOL;                /*!< 配置 SPI 的时钟极性(高/低电平) */

  uint16_t SPI_CPHA;                /*!< 配置 SPI 的时钟相位(奇/偶边沿采样) */

  uint16_t SPI_NSS;                 /*!< 配置 SPI 的 NSS 引脚由软件控制还是硬件控制 */
 
  uint16_t SPI_BaudRatePrescaler;   /*!< 配置 SPI 的时钟分频因子 */

  uint16_t SPI_FirstBit;            /*!< 配置数据是高位先行(MSB)或低位先行(LSB) */
  
  uint16_t SPI_CRCPolynomial;       /*!< 配置 CRC 校验表达式 */
}SPI_InitTypeDef;
  • SPI_Direction(单/双向模式):可配置为双线全双工、双线只接收、单线只接收、单线只发送模式。
  • SPI_Mode(主/从机模式):若配置为主机模式,则 STM32 将提供 SCLK 时序信号;若配置为从机模式,则 STM32 将接收外来的 SCLK 信号。
  • SPI_DataSize(数据帧长度):可配置 8 位数据帧或 16 位数据帧。
  • SPI_CPOL(时钟极性)和SPI_CPHA(时钟相位):可参考 SPI 相关头文件和本文章的相关内容,不再赘述。
  • SPI_NSS(NSS引脚使用模式):若选择硬件模式,则 SPI 片选信号由 SPI 硬件自动产生;若软件模式,则需要手动把相应的 GPIO 端口置 1 或置 0 以产生禁止片选或允许片选信号。
  • SPI_BaudRatePrescaler(时钟分频因子):可设置为 PCLK 的 2、4、6、8、16、32、64、128、256分频。
  • SPI_FirstBit(数据先行模式):不再赘述。
  • SPI_CRCPolynomial(CRC校验):这是 SPI 的 CRC 校验中的多项式,若我们使用 CRC 校验时,就使用这个成员的参数(多项式),来计算 CRC 的值。

2. SPI的常用库函数

// 与初始化相关:
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

// 与接收、发送数据相关:
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

// 与标志位相关:
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

// 与中断标志位相关:
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

未完待续···

以上是关于STM32学习笔记(15)——SPI协议的主要内容,如果未能解决你的问题,请参考以下文章

STM32学习笔记(16)——(SPI续)读写串行Flash

stm32学习笔记-第一天

stm32学习笔记-第一天

stm32学习笔记-第一天

STM32——SPI接口

笔记之STM32F0芯片SPI_DMA的使用(HAL库)