STM32入门教程:SPI通信外设详解——从框图到时序,一文吃透硬件SPI

📌 适用平台:STM32F103C8T6(标准库)
一、前言
学过I2C的同学应该知道,I2C虽然只用两根线,但协议复杂、速度也不快。那有没有一种通信方式,线稍微多一点,但速度快、协议简单、全双工呢?有!就是今天的主角——SPI(Serial Peripheral Interface,串行外设接口)。
SPI是STM32芯片内部的一个硬件外设,用来和外部设备进行高速串行通信。常见的应用场景比如:W25Q64闪存、OLED显示屏、SD卡、各种传感器等等。
我的理解:SPI就像一条"高速公路",I2C像一条"国道"。高速公路上没有红绿灯(不需要应答),车道还多(全双工),所以车速(波特率)能飙得老高。但代价是——占用的引脚多了点,需要4根线。
二、SPI的"四根线"——先搞清楚引脚
| 引脚名 | 全称 | 方向(主设备视角) | 作用 |
|---|---|---|---|
| SCK | Serial Clock | 输出 | 时钟线,主设备产生,用来同步数据传输 |
| MOSI | Master Out Slave In | 输出 | 主设备发送,从设备接收 |
| MISO | Master In Slave Out | 输入 | 主设备接收,从设备发送 |
| NSS | Slave Select | 输出 | 片选线,低电平有效,选中哪个从设备 |
我的理解:NSS就像老师点名,老师(主设备)喊"小明你回答"(拉低对应NSS),小明(从设备)才开始说话。同一时间只能有一个从设备被选中,不然会"抢麦"。
⚠️ 注意:NSS可以硬件控制(SPI外设自动管理),也可以软件控制(用普通GPIO手动拉低拉高)。实际项目中,软件NSS更常用,因为一根NSS只能选一个从机,多个从机时需要多个GPIO。
三、STM32的SPI外设框图——硬件是怎么干活的?
3.1 官方参考手册框图

这张图来自《STM32F10xxx参考手册》,看起来有点复杂,我们拆分来看:
核心部件:
-
发送缓冲区(TDR) + 接收缓冲区(RDR):软件读写数据的地方
-
移位寄存器:真正负责"一位一位移出去/移进来"的干活单元
-
波特率发生器:产生SCK时钟频率,由主控制电路管理
-
主控制电路:协调整个SPI外设的工作
-
通信电路:处理各种状态标志(TXE、RXNE、BSY等)
关键寄存器:
-
SPI_CR1:控制寄存器1,配置主从模式、波特率、CPOL/CPHA、数据帧格式等 -
SPI_CR2:控制寄存器2,配置中断使能、DMA等 -
SPI_SR:状态寄存器,存放TXE、RXNE、BSY等标志位
3.2 江科大简化版框图

江科大这个简化图更直观,我们顺着数据流走一遍:
发送流程(写数据):
软件写入 → 发送数据寄存器(TDR) → 移位寄存器 → GPIO → MOSI引脚 → 外部设备
接收流程(读数据):
外部设备 → MISO引脚 → GPIO → 移位寄存器 → 接收数据寄存器(RDR) → 软件读出
我的理解:移位寄存器就像一个小型"传送带",一次只能运8位(或16位)数据。TDR和RDR相当于"前台",移位寄存器相当于"后台"。软件只跟前台打交道,后台的搬运由硬件自动完成。这样做的好处是——软件写入TDR后,硬件会自动搬到移位寄存器去发送,软件不用死等,可以去干别的事。
四、SPI的工作模式——CPOL和CPHA是个啥?
SPI有四种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)两个位决定:
| 模式 | CPOL | CPHA | SCK空闲状态 | 数据采样时刻 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 第一个边沿(上升沿) |
| 1 | 0 | 1 | 低电平 | 第二个边沿(下降沿) |
| 2 | 1 | 0 | 高电平 | 第一个边沿(下降沿) |
| 3 | 1 | 1 | 高电平 | 第二个边沿(上升沿) |
人话解释:
-
CPOL=0:SCK不干活的时候(空闲)是低电平;CPOL=1则相反。
-
CPHA=0:数据在第一个时钟边沿被"采样"(读取);CPHA=1则在第二个边沿采样。
我的理解:CPOL决定的是"休息姿势",CPHA决定的是"什么时候拍照"。两个设备通信时,必须约定好同一个"姿势"和"拍照时机",不然一个准备拍正面,一个只给背影,那就对不上号了。实际使用中,模式0和模式3最常见。
五、主模式下的数据传输——连续 vs 非连续
这是SPI编程中最容易搞混的地方!STM32的SPI主模式下,有两种传输方式:
5.1 连续传输(全双工)

这张图信息量巨大,我们一步一步看:
时序流程解析:
-
软件写入0xF1到SPI_DR → TXE由1变0(发送缓冲区非空),BSY由0变1(开始忙)
-
数据从TDR转移到移位寄存器 → TXE由硬件置1(发送缓冲区空了,可以写入下一个数据)
-
软件检测到TXE=1,写入0xF2 → 同时移位寄存器在SCK驱动下发送数据1
-
数据1发送完成,数据2进入移位寄存器 → 接收到的数据1(0xA1)从移位寄存器转入RDR → RXNE置1
-
软件检测到RXNE=1,读出0xA1 → RXNE由软件清0
-
重复上述过程...
关键标志位:
-
TXE(Transmit Buffer Empty):发送缓冲区空。=1时表示可以往SPI_DR写新数据
-
RXNE(Receive Buffer Not Empty):接收缓冲区非空。=1时表示可以从SPI_DR读出接收到的数据
-
BSY(Busy):忙标志。=1时表示SPI正在通信中
我的理解:连续传输的精髓在于"流水线作业"。当第一个数据还在移位寄存器里往外发的时候,第二个数据就已经可以写入TDR排队了。发送和接收是同时进行的(全双工),发完8个时钟的同时,也收完了8个时钟的数据。
5.2 非连续传输(只发不收)

非连续传输的特点是:发完一个字节,等忙完了再发下一个。
时序流程解析:
-
软件写入0xF1 → BSY置1,TXE变0
-
等待TXE=1(数据从TDR移到移位寄存器)→ 但软件"较晚"写入0xF2
-
第一个字节发完,SCK出现间隙 → 因为软件写入晚了,移位寄存器没及时拿到下一个数据,SCK停了一小会儿
-
BSY变0再变1 → 表示中间有空闲
-
最后一个字节发完后,软件等待BSY=0 → 确保全部发送完毕
我的理解:非连续传输就像"单线程"——发一个,等发完,再发下一个。中间会有SCK的"停顿间隙"。这种方式简单、不容易出错,但效率比连续传输低。对于不要求极限速度的场景,非连续传输完全够用,代码也更简单。
💡 编程提示:如果只想发送不关心接收,可以只判断TXE和BSY;如果全双工收发,必须同时处理TXE(写)和RXNE(读),否则RDR满了会溢出(OVR错误)!
六、软件SPI vs 硬件SPI——波形对比

这张图对比了软件模拟SPI和硬件SPI的波形:
| 对比项 | 软件SPI | 硬件SPI |
|---|---|---|
| 实现方式 | GPIO位带操作/库函数模拟时序 | 配置寄存器,硬件自动完成 |
| CPU占用 | 高,发送时CPU全程参与 | 低,CPU只负责读写DR寄存器 |
| 速度 | 慢,受限于CPU指令速度 | 快,最高可达72M/2=36MHz(F103) |
| 波形质量 | 可能有抖动,时序不精确 | 波形规整,时序由硬件保证 |
| 代码复杂度 | 需要自己写时序逻辑 | 配置好寄存器即可 |
我的理解:软件SPI就像手写快递单,每一张都要自己填;硬件SPI就像用打印机,设置好模板后自动批量打印。从图里也能看出来,硬件SPI的波形边缘更陡峭、时序更均匀,而软件SPI由于CPU执行指令有先后,边沿可能会有微小抖动。所以能开硬件SPI,尽量开硬件SPI!
七、SPI的初始化配置要点——需要设置哪些参数?
虽然不讲具体代码,但配置SPI时需要关注的关键参数必须搞清楚,不然代码写出来也是懵的:
7.1 时钟使能
SPI外设和对应的GPIO端口都需要开启时钟。SPI1在APB2总线上,SPI2在APB1总线上,别搞混了。
7.2 GPIO模式配置
| 引脚 | 模式 | 原因 |
|---|---|---|
| SCK | 复用推挽输出 | 由SPI硬件控制时钟输出 |
| MOSI | 复用推挽输出 | 由SPI硬件控制数据发送 |
| MISO | 上拉输入 | 读取从设备返回的数据 |
| NSS | 通用推挽输出(软件控制时) | 由软件手动拉高/拉低片选 |
⚠️ 注意:MISO千万别配成输出模式,不然从设备发来的数据你根本读不到!
7.3 SPI参数配置清单
-
方向:双线全双工(最常用)/ 单线半双工 / 单线只发 / 单线只收
-
主从模式:一般STM32做主设备(Master)
-
数据大小:8位(1字节)或16位,绝大多数情况用8位
-
CPOL/CPHA:根据从设备 datasheet 选择,常用Mode 0
-
NSS管理:软件NSS(推荐,灵活控制多从机)或硬件NSS
-
波特率预分频:SPI1时钟源72MHz,分频值越大速度越慢。建议先选大分频调通,再逐步提速
-
数据帧格式:高位先行(MSB First,最常用)或低位先行(LSB First)
-
CRC校验:一般不用,关闭即可
八、📌 个人理解 / 心得
(以下为博主个人学习心得,仅供参考)
-
TXE和RXNE的判断时机:刚开始学的时候,我一度纠结是先判断TXE还是先判断RXNE。后来对着参考手册的时序图看了好几遍才明白——写数据前判TXE,读数据前判RXNE,两者是"生产者-消费者"的关系,不能搞混。如果写得太快不读,RDR溢出就会报OVR错误。
-
波特率的选择:SPI1在APB2上,最高72MHz。预分频最小是2,所以理论最高速36MHz。但实际不要一上来就拉满,先选大分频(低速)调通,再逐步提高速度。特别是用杜邦线连接时,信号干扰大,高速很容易出问题。
-
"交换"的概念要刻进DNA里:SPI是全双工的,不存在"只发送"或"只接收"的纯操作。你每发一个字节,必然同时收一个字节;你想收一个字节,也必须先发一个Dummy字节去维持SCK时钟。很多新手(包括我)一开始会在这个点上绕很久。
九、⚠️ 踩坑记录
(以下为博主实际踩坑记录,血泪教训)
| 坑号 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| 坑1 | SPI初始化后,MOSI和SCK始终没有波形 | 忘了调用SPI_Cmd()使能SPI外设 | 初始化最后一定要使能SPI! |
| 坑2 | 读取的数据全是0xFF或0x00 | NSS片选没拉低,从设备根本没响应;或者CPOL/CPHA模式不匹配 | 用示波器抓波形,确认NSS拉低时机和SCK空闲电平 |
| 坑3 | 连续读取多个字节时,第一个字节对,后面的全错 | 每次交换字节后没等RXNE就读了,或者读完后没及时清标志 | 严格按"等TXE→写→等RXNE→读"的顺序,不要省等待! |
| 坑4 | 波特率设得太高,数据偶尔错乱 | 杜邦线太长,信号完整性差,高频下波形畸变 | 降低波特率预分频,或者改用短排线/PCB走线 |
十、📋 总结表格
| 知识点 | 核心内容 |
|---|---|
| SPI引脚 | SCK(时钟)、MOSI(主发从收)、MISO(主收从发)、NSS(片选) |
| 工作模式 | 由CPOL和CPHA组合成4种模式,常用Mode 0和Mode 3 |
| 数据流向 | 写→TDR→移位寄存器→MOSI;MISO→移位寄存器→RDR→读 |
| 关键标志 | TXE(发送缓冲空)、RXNE(接收缓冲非空)、BSY(忙) |
| 传输方式 | 连续传输(效率高,流水线)/ 非连续传输(简单可靠,有间隙) |
| NSS控制 | 硬件NSS(自动,单从机)/ 软件NSS(GPIO手动,多从机推荐) |
| 软件vs硬件 | 硬件SPI速度快、CPU占用低、波形好;软件SPI灵活但慢 |
| 编程要点 | 全双工交换数据,发必收、收必发;严格等待标志位;先低速调通 |
| GPIO配置 | SCK/MOSI复用推挽输出,MISO上拉输入,NSS通用推挽输出(软件模式) |
| 初始化顺序 | 开时钟→配GPIO→设SPI参数→使能SPI |
写这篇文章的时候,我把江科大的视频又回顾了一遍,同时对着《STM32F10xxx参考手册》的SPI章节啃了很久。特别是那张连续传输的时序图,我一开始看得一头雾水,后来拿笔画了画TXE、RXNE、BSY三条线跟SCK的对应关系,才恍然大悟——原来"连续"的精髓是TDR和移位寄存器的双重缓冲。
SPI相比I2C,协议确实简单很多,没有起始位、停止位、应答位的概念,就是纯粹的"你发我收、我发你收"。但简单不代表没有细节,CPOL/CPHA的模式匹配、TXE/RXNE的等待时机、NSS的片选控制,这些都是实打实的坑。希望这篇文章能帮到你,少走点弯路。
📌 开发环境:Keil5 + STM32F103C8T6最小系统板 + ST-Link
📌 参考资料:江科大STM32入门教程、《STM32F10xxx参考手册》
📌 前置文章:江科大STM32入门:SPI通信协议详解——从移位寄存器到时序模式
如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ !有任何问题欢迎在评论区交流,一起学习进步!

更多推荐

所有评论(0)