🚀 我的STM32学习专栏:[点击直达 | 持续更新中]

📌 适用平台:STM32F103C8T6(标准库)


一、前言

学过I2C的同学应该知道,I2C虽然只用两根线,但协议复杂、速度也不快。那有没有一种通信方式,线稍微多一点,但速度快、协议简单、全双工呢?有!就是今天的主角——SPI(Serial Peripheral Interface,串行外设接口)

SPI是STM32芯片内部的一个硬件外设,用来和外部设备进行高速串行通信。常见的应用场景比如:W25Q64闪存、OLED显示屏、SD卡、各种传感器等等。

我的理解:SPI就像一条"高速公路",I2C像一条"国道"。高速公路上没有红绿灯(不需要应答),车道还多(全双工),所以车速(波特率)能飙得老高。但代价是——占用的引脚多了点,需要4根线。


二、SPI的"四根线"——先搞清楚引脚

引脚名全称方向(主设备视角)作用
SCKSerial Clock输出时钟线,主设备产生,用来同步数据传输
MOSIMaster Out Slave In输出主设备发送,从设备接收
MISOMaster In Slave Out输入主设备接收,从设备发送
NSSSlave Select输出片选线,低电平有效,选中哪个从设备

我的理解:NSS就像老师点名,老师(主设备)喊"小明你回答"(拉低对应NSS),小明(从设备)才开始说话。同一时间只能有一个从设备被选中,不然会"抢麦"。

⚠️ 注意:NSS可以硬件控制(SPI外设自动管理),也可以软件控制(用普通GPIO手动拉低拉高)。实际项目中,软件NSS更常用,因为一根NSS只能选一个从机,多个从机时需要多个GPIO。


三、STM32的SPI外设框图——硬件是怎么干活的?

3.1 官方参考手册框图

这张图来自《STM32F10xxx参考手册》,看起来有点复杂,我们拆分来看:

核心部件

  1. 发送缓冲区(TDR) + 接收缓冲区(RDR):软件读写数据的地方

  2. 移位寄存器:真正负责"一位一位移出去/移进来"的干活单元

  3. 波特率发生器:产生SCK时钟频率,由主控制电路管理

  4. 主控制电路:协调整个SPI外设的工作

  5.  通信电路:处理各种状态标志(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(时钟相位)两个位决定:

模式CPOLCPHASCK空闲状态数据采样时刻
000低电平第一个边沿(上升沿)
101低电平第二个边沿(下降沿)
210高电平第一个边沿(下降沿)
311高电平第二个边沿(上升沿)

人话解释

  • CPOL=0:SCK不干活的时候(空闲)是低电平;CPOL=1则相反。

  • CPHA=0:数据在第一个时钟边沿被"采样"(读取);CPHA=1则在第二个边沿采样。

我的理解:CPOL决定的是"休息姿势",CPHA决定的是"什么时候拍照"。两个设备通信时,必须约定好同一个"姿势"和"拍照时机",不然一个准备拍正面,一个只给背影,那就对不上号了。实际使用中,模式0和模式3最常见


五、主模式下的数据传输——连续 vs 非连续

这是SPI编程中最容易搞混的地方!STM32的SPI主模式下,有两种传输方式:

5.1 连续传输(全双工)

这张图信息量巨大,我们一步一步看:

时序流程解析

  1. 软件写入0xF1到SPI_DR → TXE由1变0(发送缓冲区非空),BSY由0变1(开始忙)

  2. 数据从TDR转移到移位寄存器 → TXE由硬件置1(发送缓冲区空了,可以写入下一个数据)

  3. 软件检测到TXE=1,写入0xF2 → 同时移位寄存器在SCK驱动下发送数据1

  4. 数据1发送完成,数据2进入移位寄存器 → 接收到的数据1(0xA1)从移位寄存器转入RDR → RXNE置1

  5. 软件检测到RXNE=1,读出0xA1 → RXNE由软件清0

  6. 重复上述过程...

关键标志位

  • TXE(Transmit Buffer Empty):发送缓冲区空。=1时表示可以往SPI_DR写新数据

  • RXNE(Receive Buffer Not Empty):接收缓冲区非空。=1时表示可以从SPI_DR读出接收到的数据

  • BSY(Busy):忙标志。=1时表示SPI正在通信中

我的理解:连续传输的精髓在于"流水线作业"。当第一个数据还在移位寄存器里往外发的时候,第二个数据就已经可以写入TDR排队了。发送和接收是同时进行的(全双工),发完8个时钟的同时,也收完了8个时钟的数据。

5.2 非连续传输(只发不收)

非连续传输的特点是:发完一个字节,等忙完了再发下一个

时序流程解析

  1. 软件写入0xF1 → BSY置1,TXE变0

  2. 等待TXE=1(数据从TDR移到移位寄存器)→ 但软件"较晚"写入0xF2

  3. 第一个字节发完,SCK出现间隙 → 因为软件写入晚了,移位寄存器没及时拿到下一个数据,SCK停了一小会儿

  4. BSY变0再变1 → 表示中间有空闲

  5. 最后一个字节发完后,软件等待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校验:一般不用,关闭即可


八、📌 个人理解 / 心得

(以下为博主个人学习心得,仅供参考)

  1. TXE和RXNE的判断时机:刚开始学的时候,我一度纠结是先判断TXE还是先判断RXNE。后来对着参考手册的时序图看了好几遍才明白——写数据前判TXE,读数据前判RXNE,两者是"生产者-消费者"的关系,不能搞混。如果写得太快不读,RDR溢出就会报OVR错误。

  2. 波特率的选择:SPI1在APB2上,最高72MHz。预分频最小是2,所以理论最高速36MHz。但实际不要一上来就拉满,先选大分频(低速)调通,再逐步提高速度。特别是用杜邦线连接时,信号干扰大,高速很容易出问题。

  3. "交换"的概念要刻进DNA里:SPI是全双工的,不存在"只发送"或"只接收"的纯操作。你每发一个字节,必然同时收一个字节;你想收一个字节,也必须先发一个Dummy字节去维持SCK时钟。很多新手(包括我)一开始会在这个点上绕很久。


九、⚠️ 踩坑记录

(以下为博主实际踩坑记录,血泪教训)

坑号现象原因解决方案
坑1SPI初始化后,MOSI和SCK始终没有波形忘了调用SPI_Cmd()使能SPI外设初始化最后一定要使能SPI!
坑2读取的数据全是0xFF或0x00NSS片选没拉低,从设备根本没响应;或者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通信协议详解——从移位寄存器到时序模式

如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ !有任何问题欢迎在评论区交流,一起学习进步!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐