SPI通信
SPI全称串行外设接口(Serial Peripheral Interface),是一种同步的串行通信方式,由于接线简单,速率高被广泛地用于MCU和外设之间的通信, 就连SD卡都有SPI的工作模式。
在串口通信一文中,我们提到所谓的串行通信是指数据在连线上一位一位的传送, 由接收方根据数据层协议拼接成字节。而同步通信则是指伴随着数据的传输还有一个时钟信号,用于协调发送和接收方的频率和相位。
1. 物理层连接
SPI的接线形式很简单,只有四根线,如下图1所示,分别是时钟线(CLK)、主设备发送从设备接收线(MOSI)、从设备发送主设备接收线(MISO)、片选信号线(SS,低电平有效)。 SPI是一种主从的通信方式,从图中也可以看到其中涉及了Master和Slave两个设备。这些信号线只有MISO是从设备控制的,其余信号线都是由主设备控制。
图1 SPI物理连接 |
片选信号的存在,配合上主从的通信模式,我们可以实现一主多从的通信方式。只需要在一组SPI信号线上同时连接多个设备,但要保证给每个从设备的片选信号线都是不一致的。 主设备需要与哪个从设备通信,就把对应的片选信号拉低就可以了。但是需要强调的是,这种一主多从的通信方式是不存在类似广播的形式的, 也就是说主设备同一时间只能与一个从设备通信。
通常SPI通信都是芯片与芯片之间的直连,也就是所谓的TTL或者CMOS电平。 这种电平信号抗干扰能力很弱,所以通常通信距离都不会很长。 一般都是在同一块电路板上用导体相连,当然也可以用导线接触来,只是随着距离的增加,通信频率或者CLK时钟的频率需要响应的降低,才可以保证通信过程的稳定。
2. 数据层逻辑
相比于串口这样的异步通信机制,SPI的数据层逻辑要相对复杂一点点,多了针对通信时钟的操作。根据总线闲时CLK的电平,以及是在上升沿还是下降沿采集数据, SPI通信又有四种不同的工作方式。下图2是在四种不同工作模式下的时序图,是从STM32的参考手册上直接抠下来的图。
我们知道时钟的一个周期有两个沿,为了保证每个时钟周期都能交换一次数据,SPI协议规定主从设备都在一个时钟沿准备数据,在另一个时钟沿读取数据。 图2中的CPHA表示的是在哪个时钟沿采样数据或者说是读取数据。CPHA = 0时表示在第一个时钟沿读取数据,如图2(a)所示。 图2(b)描述的是CPHA = 1时在第二个时钟沿读取数据的情形。
CPOL则描述了总线闲时时钟线CLK的电平。CPOL = 0为低电平,CPOL = 1为高电平。
图2(a) SPI物理连接——第一个时钟沿采集数据 | 图2(b) SPI物理连接——第二个时钟沿采集数据 |
如果我们按照[CPOL, CPHA]的顺序总结SPI的四种工作模式,就可以得到下表1的结论:
表1 SPI的四种工作模式
工作模式 | [CPOL, CPHA] | 说明 |
---|---|---|
Mode 0 | 00b | 总线闲时CLK为低电平; 在第一个时钟沿(上升沿)采样数据,第二个时钟沿(下降沿)准备数据。 |
Mode 1 | 01b | 总线闲时CLK为低电平; 在第二个时钟沿(下降沿)采样数据,第一个时钟沿(上升沿)准备数据。 |
Mode 2 | 10b | 总线闲时CLK为高电平; 在第一个时钟沿(下降沿)采样数据,第二个时钟沿(上升沿)准备数据。 |
Mode 3 | 11b | 总线闲时CLK为高电平; 在第二个时钟沿(上升沿)采样数据,第一个时钟沿(下降沿)准备数据。 |
3. STM32的SPI通信实现
本质上,SPI的实现可以看做是一个循环移位寄存器,如下图3所示。一般SPI通信都是先发送高位的,即第一个发送的是MSB,最后再发送LSB, 这就构成了图3中显示的一个循环右移的寄存器。但在STM32中,通信过程是先发送LSB还是MSB,是可以通过修改配置寄存器CR1的LSBFIRST位进行配置的,该位默认为0。
图3 SPI结构示意图 |
SPI的工作模式则是通过修改CR1中的CPHA和CPOL位进行配置,这两位的意义与本文的第2节中的内容一致。在CR1寄存器中还有BR位用于对SPI外设的驱动时钟进行分频, 以获得期望的总线时钟,即CLK信号线上的时钟频率。STM32的SPI还可以配置循环寄存器的长度是16位还是8位的,也就是一次完整传输的数据长度, 不过大多数的SPI设备都是以字节为单位进行传送的。
关于片选信号,我们需要特别注意一下。如果我们不打算通过软件控制NSS引脚,那么需要保证NSS引脚接入的是一个高电平信号,否则会产生模式错误,状态寄存器SR的MODF将置位, 同时关闭SPI。不管NSS引脚连接的是什么,我都比较习惯用GPIO控制片选信号。
之所以习惯用GPIO控制片选信号,是因为这样可以有更好的适应性,我可以根据需要合理的安排引脚功能。 为了防止产生模式错误,置位MODF,需要配置CR1寄存器的SSM位和SSI位为1,具体可以参见Reference Manual的28.3.10一节的介绍。 虽然,SSM = 1时引脚NSS的电平将受到SSI控制,但我一般不会将NSS引脚配置为复用功能,NSS引脚的电平也就不再由SSI决定。
下面是一段对SPI外设进行配置的代码,这里是一个驱动开发板上FLASH存储W25Q128的的SPI例程,在这个例程我们完成了对SPI1的初始化操作,并读取了FLASH存储的设备ID通过串口1输出。 关于W25Q128的使用方法,读者可以参考其开发手册。
SPI1->CR1.bits.SPE = 0; // 关闭SPI外设
SPI1->CR1.bits.MSTR = 1;
SPI1->CR1.bits.DFF = SPI_Data_Size_8b; // 8位
SPI1->CR1.bits.CPHA = 0;
SPI1->CR1.bits.CPOL = 0;
SPI1->CR1.bits.BR = SPI_Prescaler_2;
SPI1->CR1.bits.LSBFIRST = 0;
SPI1->CR1.bits.SSM = 1;
SPI1->CR1.bits.SSI = 1;
SPI1->CR1.bits.SPE = 1; // 开启SPI外设
在完成了配置之后,我们就可以进行SPI通信了。下面给出了交换一个字节的函数spi_exchange(),这个函数有两个参数, spi一个指向片上SPI外设的指针,data则是需要通过SPI下发的数据,该函数将把由SPI外设收到的数据返回。 在函数中我们首先检测SR寄存器的TXE位判定是否可以发送数据,然后把待发数据写到DR寄存器中。等待一个字节发送完毕后, 我们从DR寄存器中取出接收到的数据返回。
uint8 spi_exchange(spi_regs_t *spi, uint8 data) {
while (0 == spi->SR.bits.TXE);
spi->DR = data;
while (0 == spi->SR.bits.RXNE);
return spi->DR;
}
此外在例程中, 我们还提供了用于一次交换多个字节的函数spi_exchange_bytes()和发送多个字节的函数spi_send_bytes()。
4. 总结
SPI是一种高速的、全双工、同步、串行的通信方式。它只需要四根信号线既可以实现通信,一般是TTL电平可以在芯片之间直接用引脚互联。 它是一种主从结构的通信形式,支持一主多从的连接方式,但同一时间主设备只能与一个从设备通信。具体与哪个从设备通信需要由片选信号来决定。
根据总线时钟的闲时电平,以及数据准备和采样方式,SPI又有四种工作方式。 一个时钟周期一定有两个时钟沿,SPI规定在一个时钟沿采样数据,就要在另一个时钟沿准备数据。
在使用STM32的SPI外设时,需要注意片选引脚NSS的电平,和控制方式。为了防止产生模式错误置位MODF,我们需要保证NSS引脚接入高电平,或者配置CR1寄存器的SSI位为1。