SPI(Serial Peripheral Interface)即串行外设总线接口,是由摩托罗拉公司提出的一种高速通讯协议,采用全双工、同步通信方式。其通讯协议较 I2C 简单,常常在单片机系统和一些要求通讯速率较高的场合中应用。
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 总是处于高阻输入状态。
下图为 SPI 协议通讯图,为方便说明已在图中标出红色数字:
标号 1 处是NSS信号由高电平跳变为低电平,说明SPI通讯开始。当从机检测到 NSS 信号变为低电平后,就知道自己被主机选中了,开始与主机进行通讯。
标号 6 处是NSS信号由低电平跳变为高电平,说明SPI通讯结束。当从机检测到 NSS 信号变为高电平后,就知道自己的选中状态被取消了,结束与主机进行通讯。
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 |
---|---|---|---|---|
0 | 0 | 0 | 上升沿 | 低电平 |
1 | 0 | 1 | 下降沿 | 高电平 |
2 | 1 | 0 | 上升沿 | 低电平 |
3 | 1 | 1 | 下降沿 | 高电平 |
通常情况下,模式 0 和模式 3 用的较多。只有主机和从机采用相同的模式才能够正常通讯。
STM32 有多个 SPI 外设,STM32F10x 系列就拥有 3 个 SPI。SPI 的功能框图如下图所示:
SPI 一共有 4 个引脚,不同 SPI 对应的引脚也不同:
引脚 | SPI1 | SPI2 | SPI3 |
---|---|---|---|
NSS | PA4 | PB12 | PA15下载口的TDI |
CLK | PA5 | PB13 | PB3下载口的TDO |
MISO | PA6 | PB14 | PB4下载口的NTRST |
MOSI | PA7 | PB15 | PB5 |
实际应用中,一般不使用 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接口。
之前说过,时钟信号由 SCK 提供,而 SCK 又是由波特率发生器提供,它是由控制寄存器SPI_CR1
的BR[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
。
SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的数据来源来源于接收缓冲区及发送缓冲区。
数据寄存器SPI_DR
对应两个缓冲区:一个用于写(发送缓冲);另外一个用于读(接受缓冲)。写操作将数据写到发送缓冲区;读操作将返回接收缓冲区里的数据。
我们可以通过写数据寄存器SPI_DR
把数据填充到发送缓冲区中;通过读数据寄存器SPI_DR
,可以获取接收缓冲区中的内容。
数据帧长度可以通过控制寄存器SPI_CR1
的DFF
位配置成8位及16位模式;而 LSBFIRST
位可配置移位寄存器的发送方向:选择 MSB(高位数据)发送还是 LSB(低位数据)先发送。
控制逻辑根据外设的工作状态修改状态寄存器SPI_SR
,只要读取状态寄存器相关的寄存器位,就可以了解 SPI 的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生 SPI 中断信号、DMA 请求及控制 NSS 信号线。
当 STM32 作为主机时,与从机的通讯过程如下图:
首先需要知道一个大概的过程:主机输出的数据都要通过缓冲区(对应数据寄存器SPI_DR
)才能进入移位寄存器,最后再发送;主机读入的数据都要通过移位寄存器,再移入缓冲区。因此,缓冲区和移位寄存器中的数据在同一时刻是不一样的,它们正好相差了一次通信时间。
SPI_DR
)写入一个数据(比如图中的 0XF1),此时触发 SCK 信号进入忙碌状态。此时状态寄存器SPI_SR
的BSY(忙标志,Busy flag)
位为 1,表示 SPI 开始通信;TXE(发送缓冲为空,Transmit buffer empty)
位为 0,表示发送缓冲非空。TXE
位为 1,表示发送缓冲为空。移位寄存器将数据一位一位传到 MOSI 线。TXE
位为 1后,软件往发送缓冲区写入第二个数据(比如 0xF2),TXE
位变为 0,表示发送缓冲非空。由于移位寄存器还没将前一个数据 0xF1 全部发送出去,因此新数据 0xF2 暂时存到缓冲区。TXE
位为 1,表示发送缓冲为空。移位寄存器将数据一位一位传到 MOSI 线。TXE
位由0变为 1,软件往发送缓冲区写入新数据,前一个数据发送完,新数据就往移位寄存器补,并将其一位一位发出去。发送出去的数据总比新写入缓冲区的数据少一次发送周期。SPI_SR
的BSY
位为 1,表示 SPI 开始通信;RXNE(接收缓冲非空,Receive buffer not empty)
位为 0,表示接收缓冲为空。RXNE
位为 1,表示接收缓冲非空,表明软件可以开始读取数据 0xA1。RXNE
位为 0,表示接收缓冲为空。此时移位寄存器将新数据 0xA2 转移到接收缓冲区中,RXNE
位为 1,表示接收缓冲非空,表明软件可以开始读取数据 0xA2RXNE
位由 0 变为 1 后,从接收缓冲区读取数据。接收到的数据总比新读取缓冲区的数据多一个发送周期。以下为 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;
// 与初始化相关:
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);
未完待续···