文章目录
概述
在上一章节 《SPI 相关概念》 中,我们已经从通信协议本身出发,系统讲解了 SPI 通信在时序结构上的灵活可配置性。
包括:
- 比特序的选择。
- 时钟极性(CPOL),也就是通信空闲状态的选择。
- 时钟相位(CPHA),也就是采用时机的选择。
- 四种时钟模式选择。
至此,我们已经大体了解了SPI通信的基本规则。
结合我们学习I2C通信的经验,距离我们实现SPI通信,还有一个很重要的问题需要解决:
我们实现软件SPI,还是实现硬件SPI呢?
事实上,和 I2C 通信类似,SPI 通信同样可以采用两种实现方式:
- 软件 SPI:通过普通 GPIO 引脚,由软件控制引脚电平变化,手动模拟 SPI 的时序过程。
- 硬件 SPI:由单片机内部集成的 SPI 外设模块,在硬件层面自动完成时序控制和数据移位。
在实现I2C时,两种方式我们都实现过了。对于I2C而言,它们的差异在于:
- 软件I2C实现,更灵活简单,但通信效率低,时序控制不稳定。
- 硬件I2C实现,通信效率更高,时序控制稳定。但相对配置多更繁琐,引脚选择也不灵活。
由于I2C本身就不是高速通信协议(快速模式时钟频率不过400kHz),不以性能著称,主打性价比,所以软I2C是应用更广泛的实现。
SPI以性能著称,通信速率快是其最核心的优势,采用软件SPI则会放弃这最大优势,而且软件SPI实现起来会很繁琐。
所以在实际工程应用中,硬件SPI是决定的主流。
在后续的SPI章节内容中,我们所讲的SPI实现都是基于STM32的SPI外设实现的,也就是硬件SPI,后续内容不再单独强调这一点。
在本章节内容中,我们会讲解单片机SPI外设的核心工作原理,并且还会带着大家完成SPL库相关代码的编写。
我们就分两个大部分来讲解这一章节:
- SPI通信的移位机制。
- STM32的SPI外设框图。
主要讲解SPI框图,来看一下STM32的SPI外设是如何工作的。
SPI通信的移位机制(以bit为单位)
早在SPI通信讲解的开头,我们就提出关于SPI通信的重要特性:
SPI 通信本质上是在时钟信号驱动下,主从双方移位寄存器进行同步移位操作,从而完成串行数据的交换。
SPI通信的过程是数据交换的过程!
那么SPI通信究竟如何完成这个数据交换的过程呢?
在单主单从的前提下,《参考手册》的SPI章节为我们提供了一个简易的、移位机制示意图。
如下所示:

我们可以把图中无关的内容去掉,然后在移位寄存器中增添数据,一个简化的SPI移位机制示意图。
如下所示:

解释一下这张图:
- 主机控制SCK时钟信号线,主机掌握时序,SPI是严格的主从通信模型。
- 主机每移位发送1个bit数据,也同时移位接收1个bit数据。
- 从机每移位接收1个bit数据,也同时移位发送1个bit数据。
在这张示意图中,主机发送1个字节数据给从机,从机接收这1个字节的过程,也会发送1个字节给主机。
总之,SPI通信的过程,实际上就是主从双方移位寄存器移位,进而实现数据交换的过程。
上面这张图,双方完成移位的过程,以及最终结果如下所示:



现在,你应当可以理解,什么叫做**"数据交换"**了吧?
当然,这个图中还涉及一个小问题:
如果按照图中的移位寄存器,向左移位的方向,逐bit收发数据,这是什么比特序呢?
当然是MSB First。
如果移位的方向反过来,则又变成了另一种比特序------LSB First。
如此,关于SPI的移位机制,我们就搞懂了。
所以 SPI 通信的本质是:
- 主从双方各自拥有一个移位寄存器。
- 在时钟信号的驱动下,每来一个时钟沿,双方同时移出一位、移入一位
- SPI通信本质上是一次"数据交换",而不是单向发送。
这种设计,把通信简化为一个最纯粹的动作------在时钟驱动下的、互相持续移位。
只要主机产生时钟,数据就必须流动。
每一个时钟周期都必须完成一次 bit 交换,既不会浪费时钟资源,也无需额外的方向控制逻辑。
因此,SPI 的核心思想不是"发送"或"接收",而是"同步移位交换"。
SPI通信的高速,就是这种结构高效、简单所带来的自然结果。
下面我们要进一步来看一下,单片机SPI外设是如何工作的。
我们来研究一下SPI外设框图。
SPI外设框图
《参考手册》的SPI章节,给出了单片机SPI的外设框图,如下:

我们学STM32已经有一段时间了,再加上我们前面对SPI相关知识的讲解,相信已经能够看懂这张图。
我们应对这种框图,最好的办法还是按照功能进行切割分区,然后每个分区单独进行理解。
其中寄存器部分不进行单独讲解,在其它功能部分中用到了,我们就会提。
这个框图,我把它分解成三个功能区域来进行讲解。
第一部分:数据通路
首先,我们先来看一下这个图左上角部分,也就是SPI外设实现移位通信过程。
把图进行一下裁剪,得到如下图内容:



释说明一下这个图:
- STM32的SPI外设,其移位寄存器的位数是可以配置的。
- 实际上是通过配置SPI_CR1寄存器的DFF(Data frame format)位来实现的。
- 有两种选择,可以配置成8位,也可以配置成16位。
- 也就是说,基于SPI外设实现通信时,可以一次性交换1个字节或2个字节数据。
- 大多数情况下,程序员处理数据都是以字节为单位的,所以8位移位寄存器是最常见的选择!
- 如果选择8位移位寄存器长度,那么SPI通信一次性移位、收发数据的基本长度就是1个字节。
- 配置SPI外设时,可以通过配置**"SPI_CR1寄存器的LSBFIRST位"**,来确定SPI通信的比特序。
- 图中的"发送缓冲区",其实就是我们之前讲通信时的发送数据寄存器,它是一个真实存在于SPI外设当中的寄存器。
- 图中的"接收缓冲区",其实就是我们之前讲通信时的接收数据寄存器,它是一个真实存在于SPI外设当中的寄存器。
- 图中展示的核心机制就是上一小节的中的,以bit为单位的"数据移位"机制,这里不再赘述。
SPI通信的数据帧格式
数据帧格式是我们学习串口和I2C通信时,最最核心的概念,没有之一。
下面两张图展示了串口通信和I2C通信的数据帧格式:




- 很显然串口通信的数据帧格式最固定,固定长度,且每1个bit位做什么也都是固定的。
- I2C通信的数据帧长度虽然不固定,但也仍然具有起始位、停止位、寻址字节等这些相对比较固定的格式
那么关于SPI,SPI通信的数据帧格式,到底是什么样的呢?有多长啊?起始位停止位有没有?
为了回答这个问题,我们不妨先思考下面的问题:
就在上一章节《SPI相关概念》中,我们看过下面的时序图:

当时我们还得到了这样的一个结论:
SPI通信是非常灵活的,在NSS拉低有效期间,主从双方在1个时钟周期内交换1bit数据,有多少个时钟周期就能交换多少个bit数据。
SPI通信不受固定的数据帧格式限制,一次通信可以交换不固定数量个bit数据,而且也不需要是必须是8整数倍个bit数据。
在上一章节中,我们讲过这是SPI的一个优势,数据帧的格式不受限,比较灵活。
但是到了这一章节,我们发现:SPI外设的移位寄存器是8位或16位的,这也就意味着:
使用单片机的SPI外设,进行数据处理交换时,以字节为基本单元,或以半字(2个字节)为基本单元。
这样灵活性不就丧失了吗?
这是什么情况?
之所以会产生这样的疑问,主要的原因是混淆了SPI协议和SPI外设硬件实现上的差异:
- 从SPI通信协议上来讲,NSS拉低通信时间内,产生多少个时钟周期就能够收发多少bit数据。从协议上确实非常灵活,没有任何限制。
- 但到了具体硬件实现时,移位寄存器总得确定一个长度,那么8位1个字节,16位2个字节就是最常见的选择了。
也就是说:
- SPI协议层面对于数据帧长度的限制是没有的。
- 但到了单片机SPI外设硬件实现时,处理数据的基本单元是1个字节或2个字节。
- 所以有些人会认为配置移位寄存器的长度就是配置数据帧格式的长度,这也有一定道理。
- 把移位寄存器的长度,叫做"1帧",这在很多官方文档、官方库代码中都能够见到。
上面聊完了,SPI数据帧的长度,那么其它格式限制有吗?
答:完全没有。为什么一直没讲,就是因为没有限制。
至此,关于SPI通信的数据帧格式,我们做以下总结:
- 从SPI通信协议层面讲:SPI没有数据帧格式的概念,只要符合通信的时序,可以随意进行通信收发交换数据。
- 从SPI硬件外设实现层面讲:移位寄存器的长度经常被设置为8位1个字节,所以很多人会认定SPI通信的"1帧"是一个字节。
- 具体到通信的从机设备,不同从机设备在通信时,有不同的指令、数据以及行为约束,但这本身和SPI协议没有关系。
举一个通俗的例子就是:
全球任何国家都是有货币的,你花钱的时候,可以花1元、8元、2.5元、10.8元......
但具体到这个国家的货币体系,印纸币时,总不能所有金额都印一种纸币,所以就选择一些常用金额来印刷纸币。
不同的国家有不同货币,美国总不能花人民币,这就是从机不同,通信方式行为有些许区别,但不是根本区别,因为大家都使用货币。
这是通信的软件协议和硬件实现上的差异性。
扩展/思考
其实讲完SPI数据帧的问题,SPI为什么快,就可以总结第一个核心因素了:
- SPI通信中收发的数据几乎100%都是有效数据,没有起始和停止位,没有应答,更没有什么寻址等乱七八糟的内容。
- SPI通信本身就是交换数据,通信一直都是全双工的,也没有切换通信方向的消耗。
SPI外设移位机制(以字节为单位)
第一次讲移位机制时,我们是以bit为单位的。
站在整个SPI外设的角度,处理数据是以"整个移位寄存器"为基本单位的:可以一次性处理1个字节,也可以一次性处理2个字节。
一次性处理1个字节是最常见的选择,接下来,以字节为单位,看一下SPI外设的移位机制,是如何运行的。

这一次分析,我们要带上SR寄存器中的标志位,TXE和RXNE。
这两个标志位大家都很熟悉了,这里不再赘述。
SPI外设收发处理一个字节数据的过程是这样的:
- 主机判断和等待TXE标志位置位,也就是等待发送数据寄存器为空。
- 在 TXE = 1 的前提下,主机将 1 个字节写入发送数据寄存器(TDR)
- 该1个字节的数据进入主机移位寄存器,被逐bit移除发送。
- 与此同时,从机也会逐bit发送数据,主机的移位寄存器也会同步接收这些bit数据。
- 等待主机接收数据寄存器非空,当主机接收数据寄存器非空时:
- 意味着主机已经将1个字节数据完全发给从机。
- 意味着从机已经将1个字节数据完全发给主机。
- 主机读接收数据寄存器。
如此一轮操作下来,主机就完成了发送1个字节数据,同时也收到1个字节数据。
第二部分:主机时钟生成器
SPI外设框图的第二核心区域是,SPI外设主机时钟生成器,如下图所示:

SCK 引脚输出时钟信号的时钟频率,是由 APB 外设总线时钟频率,经过"波特率发生器"分频后得到的。
解释一下这句话。
第一,APB外设总线的时钟频率是多少?
这个概念早在串口通信讲波特率时就已经详细讲过了,APB外设总线的时钟频率在默认情况下:
- APB2外设总线的时钟频率是72MHz。
- APB1外设总线的时钟频率是36MHz。
参考下列STM32系统结构框图:

我们所使用的STM32F103C8T6单片机,一共有两个SPI外设,即SPI1和SPI2。
其中SPI1挂载在APB2外设总线上,SPI2挂载在APB1外设总线上。
综上所述:
- 如果你使用SPI1外设,那么默认情况下,通信的时钟频率是分频72MHz得到的。
- 如果你使用SPI2外设,那么默认情况下,通信的时钟频率是分频36MHz得到的。
第二,波特率发生器是什么意思?SPI还有波特率的概念?
首先,SPI一定没有波特率这个概念。
在串口通信中,由于是异步通信,没有时钟信号统一时序,所以才需要通信双方约定波特率来确定每bit数据的时长。
但SPI是同步通信,由时钟信号严格控制时序,完全没必要使用波特率。
那这里的波特率发生器是什么呢?
这里实际上只是一个名称的沿用,就像马路上没有马,波特率发生器也不是生成波特率的。
波特率发生器,更贴近其功能的叫法应该是"时钟生成器",它通过对APB外设总线频率进行分频,最终得到SPI通信的时钟频率。
第三,波特率生成器的分频系数如何设定呢?
图上已经展示的非常清楚了,SPI外设的CR1寄存器的三位BR0、BR1、BR2用于确定分频系数。
具体来说,如下图所示:

从1/2分频,到1/4分频,最高还可以做到1/256分频。
举一个例子:
使用SPI1外设实现SPI通信时,波特率生成器采用1/4分频,那么此时通信中的时钟频率是:72 / 4 = 18MHz
那么通信的时钟频率有什么用呢?
SPI通信时钟频率与传输速率
I2C和SPI都是同步通信,通信的时序由时钟信号完全控制,通信的速率则由时钟线的时钟频率决定。
对于SPI通信来说,假设SPI时钟频率是18MHz:
- 这意味着,每秒钟有18 000 000个时钟周期。
- 这意味着,在连续传输的情况下,理论上每秒钟能传输有效数据18 000 000 bit。
所以数据的传输速率是:18 000 000 / 8 / 1000 / 1000 = 2.25 MB/s
这是18MHz的SPI通信,在协议层面上的理论最大传输速率。
在实际工程中,由于各种软硬件因素影响,18MHz时钟频率下,SPI通信通常也能稳定在2MB/s左右的数据传输速率。
这对于串行通信而言,已经是一个非常快的传输速率了。
相比较而言:
- 串口通信,比较常见的通信速率是115200波特率,折算下来通信速率大约是11KB/s
- I2C通信的快速模式,时钟频率是400kHz,连续数据传输,折算下来通信速率大约是44KB/s
可以看到,在中低速通信场景中,SPI 的数据传输能力相比 UART 和 I2C 具有数量级上的优势。
扩展/了解:
那这是为什么呢?为什么SPI通信的速度优势这么明显呢?
首先对于串口通信这种异步通信,双方没有统一时钟信号来确定时序,这种设计先天就不可能是高速通信。
串口通信只能靠双方约定波特率后,依靠起始位对齐时间,然后再逐bit的进行过采样判决结果。
某种程度上来说,串口通信的采样过程是一种"盲人摸象"式的采样,本身出错的风险就是存在的。
如果波特率设置过大,那么采样难度就会显著提升,采样的准确度就很难以保证了,那样通信就会完全失败。
而对于I2C来说,它的硬件电路设计天然就是有短板的。
信号线电平的上拉依赖于上拉电阻,这个过程注定是很慢的。
所以I2C通信一旦将时钟频率设置过高,就很容易出现上拉过慢,时序异常的问题。
最后回到SPI通信,它在通信时是完全的一对一,有确定时钟信号统一时序,电平上拉下拉也都是直接完成的。
所以SPI通信的时钟频率可以设置得很高,通信速率非常快。
如果用通俗的语言来描述,那这三种通信:
- 串口就是写信读信,而且还要靠掐表来完成,注定不可能快。
- I2C就像普通的马路,从设计上来说,就不支持让你飙车。
- SPI则像高速公路,而且还是一对一的专线高速公路,设计的目的就是跑得快。
第三部分:主从控制与NSS管理电路
在前面的内容中,我们已经分别讲清楚了 SPI 外设中的数据通路,以及主机模式下的时钟生成电路。
到这里为止,SPI 通信中"数据怎么走""时钟从哪来"这两件最核心的事情,其实已经解决了。
接下来这部分电路,我们统一称之为 SPI 主从控制与 NSS 管理电路。
主要解决的都是一个核心问题:此SPI外设在通信中,究竟做主机还是做从机。
当然我们早就已经确定了,STM32作为通信中的主机,所以我们需要把此SPI外设设置为主机模式。
这部分电路内容稍多一些,简化后如下图所示:

首先,在 STM32 的 SPI 外设中,由 SPI_CR1 寄存器中的 MSTR 位决定其主从身份:
- 当 MSTR 被配置为 1 时,SPI 外设工作在主机模式。
- 当 MSTR 为 0 时,SPI 外设工作在从机模式。
这张图中,我还把上面讲过的**"时钟发生器"**电路放进来了,这是因为:
- 主机模式下 SPI 会启用内部的波特率发生器产生 SCK 时钟,对外输出时钟信号。
- 从机模式下 SPI 则完全依赖外部 SCK 引脚输入的时钟信号,这时候"波特率发生器"就不工作了。
然后我们重点来讲一下,NSS引脚及其相关电路。
首先,在前面的内容中,我们几乎没有刻意提及单片机的 NSS 引脚。
原因很简单,NSS引脚是从机片选使用的,STM32做主机,看起来用不到NSS引脚。
是这样的吗?
当然不是,这是很明显的误解。
SPI通信是一种:
完全没有总线仲裁、严格一主机、主机不切换的同步总线通信协议,这固然带来了通信的简洁性和便利性。
但也不可避免带来了一个问题:
如果总线上,确实出现了多台设备竞争主机,怎么办?
从SPI通信协议上来说,不用考虑这个问题,你不符合协议要求,通信失败就是了。
但从硬件设计和工程实用性的角度出发,这样不管不顾就不合理了。
至少需要有一种方式,能够检测并暴露这种非法状态,从而提醒使用者及时发现并解决问题。
STM32的SPI外设,其NSS引脚以及相关电路,就是被设计用于确认:当前SPI外设作为主机,其身份是否合法。
也就是说:
从SPI软件协议上来说,不关心什么主机从机冲突问题,但具体到硬件实现时,从实用性出发必须防范这种问题。
那么具体怎么做呢?
要想理解NSS相关电路,首先需要理解右上角的梯形电路符号,这是一个双路选择器:
- 如果SSM标志位是0,则表示选择上面的0,也就是把NSS引脚的输入电平,送入SPI主控制电路。这种方式,叫做"硬件NSS"。
- 如果SSM标志位是1,则表示选择下面的1,也就是把SSI标志位确定的内部电平信号,送入SPI主控制电路。这种方式,叫做"软件NSS"。
下面展开讲解NSS的两种模式------硬件NSS和软件NSS。
硬件NSS
在CR1寄存器,SSM标志位设置为0的情况下:
此时单片机的 NSS 引脚输入的电平会被直接送入 SPI 主控制电路,用于裁决判断当前STM32的主机身份是否合法。
通俗的讲,此时SPI外设会这么来进行判断:
- 如果我被配置为主机(MSTR = 1)
- 那么 NSS 引脚的输入电平必须持续保持为高电平。
- 而如果发现,NSS引脚的输入电平变成了低电平,则说明总线上"有其它设备试图做主机,选我做从机"
- 如果出现NSS引脚输入低电平,就表示出现了主机竞争冲突。
- 此时SPI外设会自动置位SR状态寄存器中的MODF(模式错误)位,也就是说NSS设置与实际情况不符合。
- 最终SPI外设会自动清除 MSTR 位,强制退出主机模式。
需要注意的是:
使用硬件NSS必须选择引脚定义表中的固定NSS引脚,不能选择随意引脚。
比如:
| 引脚号 | 引脚名称 | 类型 | I/O 口电平 | 主功能 | 默认复用功能 | 重定义功能 |
|---|---|---|---|---|---|---|
| 14 | PA4 | I/O | / | PA4 | SPI1_NSS/USART2_CK/ADC12_IN4 | / |
软件NSS
在CR1寄存器,SSM标志位设置为1的情况下,此时SSI标志位的值决定主控制电路的输入电平:
- 如果SSI标志位设置为0(复位),那么主控制电路的输入电平就固定是低电平。
- 如果SSI标志位设置为1(置位),那么主控制电路的输入电平就固定是高电平。
所以:
在MSTR = 1,单片机SPI外设被设置为主机时,只需要设置SSM = 1,SSI = 1,就是告诉SPI外设:
我一直都是主机,你别操心我的主机身份了,我是神圣且合法的主机。 当然:
在MSTR = 1,单片机SPI外设被设置为主机,且SSM = 1选择软件NSS的前提下,如果SSI = 0,则依然表示模式错误。
硬件NSS和软件NSS如何选择?
现在我们的需求是:STM32始终做主机不切换。
所以如果选择使用硬件NSS,会有以下麻烦:需要将固定的NSS引脚,接入3.3V,表示NSS引脚始终输入高电平,从而固定STM32主机身份。
但如果选择软件NSS,则只需要设置三个标志位就行了:
设置MSTR = 1,SSM = 1,SSI = 1
从编程角度来说,只需要一个函数调用即可解决,无需任何额外操作和接线。
很显然,在我们当前的需求中,直接用软件NSS是最合适的。
总结
总得来说,在STM32做主机时,建议大家选择软件NSS,直接固定自身主机身份。
如果需要STM32做从机,则可以选择硬件NSS。
如下图所示:
