STM32F1 SPI通信协议详解:从原理到驱动开发实战
1. 项目概述从“点灯”到“对话”玩过STM32的朋友第一步都是从GPIO点灯开始的。当LED灯在你的控制下闪烁时那种成就感是实实在在的。但很快你会发现这个世界不只是“开”和“关”。当你想驱动一块OLED屏幕显示波形或者连接一个温湿度传感器读取数据又或者与一个无线模块通信时GPIO那简单的“高电平”和“低电平”就显得力不从心了。这时你就需要一种更高效、更结构化的“对话”方式——这就是通信协议而SPI正是其中最直接、最快速的“方言”之一。SPI全称Serial Peripheral Interface即串行外设接口。它不像UART那样需要事先约定好波特率也不像I2C那样需要复杂的起始、停止信号和地址寻址。SPI的哲学很简单我有一个时钟我告诉你什么时候该发送数据什么时候该接收数据我们同步进行。这种全双工、同步的通信方式让它在需要高速数据交换的场景下比如显示屏刷新、SD卡读写、音频编解码器大放异彩。STM32F1系列作为经典的入门级ARM Cortex-M3内核MCU其内置的SPI模块功能完善是学习并掌握这一通信协议的绝佳平台。这篇文章我将以一个在嵌入式领域摸爬滚打多年的工程师视角为你彻底拆解STM32F1的SPI模块。我不会只给你罗列寄存器手册里的字段而是会结合我实际项目中驱动各种外设如W25Qxx FLASH、NRF24L01、OLED屏驱动IC等的经验告诉你SPI协议的核心思想是什么STM32如何实现它配置时有哪些关键的“坑点”以及如何编写稳定可靠的驱动程序。无论你是刚接触SPI的新手还是想深入理解其工作机制的开发者相信都能从中获得实用的干货。2. SPI协议核心思想四线制下的同步“舞蹈”要理解STM32的SPI模块必须先吃透SPI协议本身。你可以把它想象成两个人主机Master和从机Slave之间跳一支精心编排的双人舞。这支舞需要四条线来协调步伐缺一不可。2.1 四条通信生命线SCK (Serial Clock) - 时钟线由主机产生并输出给所有从机。这是整支舞的节拍器每一个时钟脉冲的边沿都定义了一个数据位的采样或输出时刻。没有统一的时钟主从双方就会“踩错拍子”。MOSI (Master Output, Slave Input) - 主机输出线数据从主机流向从机的通道。主机通过这条线将指令或数据一位一位地“送”给从机。MISO (Master Input, Slave Output) - 主机输入线数据从从机流向主机的通道。从机通过这条线将其内部的数据如传感器读数、状态寄存器值一位一位地“回”给主机。注意当有多个从机时在非选中状态下从机必须将其MISO引脚置为高阻态以避免总线冲突。NSS (Slave Select) - 从机选择线这条线决定了哪个从机有资格参与这场“舞蹈”。通常为低电平有效。主机通过将某个从机的NSS线拉低来“选中”它。在同一时刻一条SPI总线上只能有一个从机被选中。这条线有时也被称为CS (Chip Select) 或 SS (Slave Select)。注意SPI协议标准本身并没有严格规定NSS是硬件管理还是软件管理。硬件NSS由SPI模块自动控制方便但灵活性稍差软件NSS则使用一个普通的GPIO来模拟在复杂的多从机系统中更为常用。在STM32中你需要根据实际情况选择。2.2 时钟极性与相位舞蹈的起拍规则这是SPI配置中最容易让人混淆也最关键的两个参数CPOL (Clock Polarity) 和 CPHA (Clock Phase)。它们共同定义了数据在时钟的哪个边沿被采样和输出。CPOL (时钟极性)定义SCK线在空闲状态即两次传输之间时的电平。CPOL0SCK空闲时为低电平。CPOL1SCK空闲时为高电平。CPHA (时钟相位)定义数据是在时钟的第几个边沿被采样。CPHA0数据在时钟的第一个边沿对于CPOL0是上升沿对于CPOL1是下降沿被采样在下一个边沿切换。CPHA1数据在时钟的第二个边沿被采样在第一个边沿切换。CPOL和CPHA组合起来就构成了SPI的四种工作模式Mode 0, 1, 2, 3。绝大多数SPI外设的数据手册都会明确要求使用哪种模式你必须严格按照外设的要求来配置STM32否则通信必然失败。模式CPOLCPHA空闲时SCK电平数据采样时刻数据切换时刻常见外设举例Mode 000低电平第一个时钟边沿上升沿下降沿W25Q系列FLASHMode 101低电平第二个时钟边沿下降沿上升沿较少见Mode 210高电平第一个时钟边沿下降沿上升沿较少见Mode 311高电平第二个时钟边沿上升沿下降沿SD卡SPI模式、NRF24L01实操心得我习惯用示波器或逻辑分析仪来抓取SPI波形这是验证配置是否正确最直观的方法。抓取时同时查看SCK、MOSI、MISO和NSS四条线。首先看NSS确认在通信期间是否为有效电平通常为低然后看SCK的空闲电平确定CPOL最后看MOSI/MISO的数据位是在SCK的哪个边沿稳定下来的采样边沿哪个边沿发生变化切换边沿从而确定CPHA。图形化的比对比死记硬背模式号要可靠得多。2.3 数据帧格式与传输顺序SPI以数据帧为单位进行传输一帧通常是8位或16位STM32F1支持。这里又涉及两个重要概念数据大小 (Data Size)即一帧包含多少位。STM32F1的SPI模块可以通过寄存器配置为8位或16位。必须与外设支持的数据宽度一致。位顺序 (Bit Order)即数据是从最高有效位MSB开始发送还是从最低有效位LSB开始发送。绝大多数SPI设备都采用MSB First这也是STM32 SPI的默认配置。除非外设数据手册特别说明否则不要轻易改动。一次完整的SPI数据传输过程以8位数据、MSB First为例主机拉低对应从机的NSS线选中从机。主机产生SCK时钟。在SCK的每个时钟边沿由CPHA决定主机将一位数据从MOSI线移出从MSB开始同时从MISO线读入一位数据。8个时钟周期后一帧数据传输完毕。数据寄存器中会同时包含刚刚发送出去的数据和接收到的数据。主机可以拉高NSS线结束本次通信或者保持NSS有效继续发送下一帧数据。3. STM32F1 SPI模块架构与配置详解理解了协议我们再看STM32F1如何实现它。STM32F1的SPI模块是一个全双工、同步的串行接口它既可以作为主机也可以作为从机。其核心是一个移位寄存器和一个数据寄存器(DR)。3.1 模块内部工作流程当你向数据寄存器(DR)写入一个数据时如果发送缓冲区为空这个数据会被立即转移到发送移位寄存器中。在SCK时钟的驱动下发送移位寄存器中的数据从MOSI引脚一位一位地移出。同时MISO引脚上的数据也被一位一位地移入接收移位寄存器。当一整帧数据8或16位传输完成后接收移位寄存器中的内容会被自动转移到数据寄存器(DR)中并置位RXNE (接收缓冲区非空)标志位告诉你数据已就绪可以读取。如果此时你又写入了新的数据到DR则会置位TXE (发送缓冲区空)标志位。这个过程是硬件自动完成的你的软件只需要关注两件事1. 在TXE置位时或之前准备好要发送的数据2. 在RXNE置位时及时读取接收到的数据。3.2 关键寄存器配置指南配置SPI本质上是配置一组寄存器。我们使用标准外设库SPL或HAL库来操作但理解背后的寄存器至关重要。SPI_CR1 (控制寄存器1) - 核心配置CPOL 和 CPHA位于位1和位0直接设置SPI模式。BR[2:0] (波特率控制)位5:3。设置SPI的时钟分频系数。SCK频率 APB1/APB2时钟频率 / 分频系数。注意STM32F1的SPI1挂载在APB2总线最高72MHzSPI2挂载在APB1总线最高36MHz。要根据实际总线频率计算。对于低速外设如FLASH分频大一些无妨对于高速外设如显示屏则需要计算好最高支持速率。MSTR (主/从模式选择)位2。1为主机模式0为从机模式。DFF (数据帧格式)位11。0为8位数据帧1为16位数据帧。这个位必须在SPI禁用SPE0时修改。LSBFIRST (帧格式)位7。0为MSB先行默认1为LSB先行。SSM 和 SSI (软件从机管理)位9和位8。这是一对组合。当SSM1时启用软件NSS管理此时NSS引脚的功能由SSI位的值决定内部拉高或拉低。当你使用软件控制GPIO作为NSS时必须设置SSM1SSI1。这样硬件NSS引脚就可以释放为普通GPIO或其他功能。SPI_CR2 (控制寄存器2) - 中断与DMARXNEIE 和 TXEIE位6和位7。分别使能RXNE接收非空和TXE发送空中断。当使用中断方式收发数据时需要开启它们。ERRIE位5。使能错误中断如过载错误、模式错误等用于错误处理。TXDMAEN 和 RXDMAEN位1和位0。使能发送和接收DMA。在需要高速、连续传输大量数据时如刷新显存必须使用DMA来解放CPU。SPI_SR (状态寄存器) - 查询状态RXNE位0。为1时表示接收缓冲区有数据可读。TXE位1。为1时表示发送缓冲区空可以写入新数据。BSY位7。为1时表示SPI正忙正在通信。在修改某些配置如DFF或关闭SPI前必须等待BSY位为0。3.3 初始化配置步骤以主机、软件NSS为例下面是一个典型的SPI主机初始化步骤假设使用SPI1模式08位数据MSB First时钟分频256低速约281.25KHz适用于W25Q FLASH。// 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // 2. 初始化GPIO // PA5 - SPI1_SCK, PA6 - SPI1_MISO, PA7 - SPI1_MOSI // 配置为复用推挽输出SCK MOSI浮空输入MISO GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 初始化软件NSS引脚例如PA4 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 通用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); SPI_CS_HIGH(); // 宏定义将PA4置高默认不选中从机 // 4. 配置并初始化SPI SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; // 全双工 SPI_InitStructure.SPI_Mode SPI_Mode_Master; // 主机模式 SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; // 8位数据 SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // CPOL 0 SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; // CPHA 0 (对应库中的1Edge) SPI_InitStructure.SPI_NSS SPI_NSS_Soft; // 软件NSS管理 SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_256; // 分频系数 SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; // MSB先行 SPI_InitStructure.SPI_CRCPolynomial 7; // CRC多项式默认7不使用CRC时可忽略 SPI_Init(SPI1, SPI_InitStructure); // 5. 使能SPI SPI_Cmd(SPI1, ENABLE);注意事项模式匹配SPI_CPHA_1Edge在标准外设库中对应CPHA0在第一个边沿采样。务必对照数据手册理解。NSS配置SPI_NSS_Soft是关键它使得硬件NSS引脚PA4不再控制片选我们可以用软件控制GPIO这里用了PA4来模拟片选。分频系数SPI_BaudRatePrescaler_256是在APB2时钟72MHz下的分频实际SCK 72MHz / 256 281.25KHz。确保从机设备能支持这个速率。4. SPI数据收发实战与驱动编写配置好SPI后核心就是数据的收发了。收发方式有三种阻塞式查询、中断和DMA。我们将从最简单的查询方式开始逐步深入。4.1 阻塞式查询收发函数这是最基础、最常用的方式适用于单次、非频繁的数据交换。/** * brief 通过SPI发送并接收一个字节阻塞式 * param data: 要发送的字节 * retval 接收到的字节 */ uint8_t SPI1_ReadWriteByte(uint8_t data) { // 等待发送缓冲区为空可以写入新数据 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); // 写入数据启动传输 SPI_I2S_SendData(SPI1, data); // 等待接收缓冲区非空数据已收到 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); // 读取接收到的数据并返回 return SPI_I2S_ReceiveData(SPI1); }这个函数封装了一次完整的全双工SPI传输。你发送一个data出去函数会返回同时接收到的数据。这里有一个非常重要的细节对于很多只读或只写的操作你仍然需要调用这个函数来产生SCK时钟。例如读取FLASH芯片的ID你需要先发送读ID的命令字然后接着发送几个 dummy byte比如0xFF这些dummy byte产生的时钟边沿才会让FLASH把ID数据通过MISO线送出来。实操心得片选信号NSS的时机控制片选信号的控制是软件驱动中极易出错的一环。基本原则是在开始发送命令/数据前拉低在本次完整通信序列结束后拉高。// 读取W25Q128 FLASH的制造商和设备ID uint16_t W25Q_ReadID(void) { uint16_t id 0; W25Q_CS_LOW(); // 1. 通信开始拉低片选 SPI1_ReadWriteByte(0x90); // 发送读ID命令 SPI1_ReadWriteByte(0x00); // 发送24位地址的高8位对于读ID命令通常发3个0x00 SPI1_ReadWriteByte(0x00); // 地址中8位 SPI1_ReadWriteByte(0x00); // 地址低8位 id | (SPI1_ReadWriteByte(0xFF) 8); // 读制造商ID同时发dummy clock id | SPI1_ReadWriteByte(0xFF); // 读设备ID W25Q_CS_HIGH(); // 2. 通信结束拉高片选 return id; }注意整个命令序列命令地址读数据必须在一次连续的片选有效期内完成。如果在发送命令后错误地拉高了片选再从机看来这次通信就结束了后续的读操作会无效。4.2 中断与DMA方式的应用场景当数据量较大或通信频率很高时阻塞查询会长时间占用CPU影响系统实时性。中断方式适用于中等数据量、非连续的传输。在TXE或RXNE中断服务程序里进行数据搬运。配置稍复杂但能提高CPU利用率。DMA方式这是处理大批量、高速SPI数据传输的“终极武器”。例如通过SPI向OLED屏的显存发送一整屏的图像数据几百到几千字节。配置DMA通道将内存中的数组自动搬运到SPI的DR寄存器发送出去整个过程无需CPU干预。CPU只需启动DMA传输然后等待DMA传输完成中断即可。这能极大解放CPU实现高效刷屏。DMA配置关键点使能SPI的TXDMAEN和/或RXDMAEN。配置DMA通道的外设地址SPI-DR、内存地址、数据方向、数据宽度需与SPI的DFF设置匹配、传输数量。设置传输模式单次/循环、优先级。使能DMA通道并在SPI使能后启动DMA传输。4.3 编写稳健的SPI外设驱动框架一个健壮的SPI设备驱动不应只包含收发函数还应考虑以下方面初始化与反初始化提供xxx_Init()和xxx_DeInit()函数集中管理GPIO、SPI模块、DMA的初始化和复位。设备检测像上面的W25Q_ReadID()函数可用于上电时检测设备是否存在、型号是否匹配。错误处理在中断或DMA回调函数中检查SPI的错误标志位如OVR过载错误并进行相应的恢复操作如清除标志、重新初始化。超时机制在查询标志位如等待TXE、RXNE、BSY时一定要加入超时判断防止因硬件故障导致程序死锁。uint32_t timeout 0xFFFF; while ((SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) RESET) (timeout--)); if(timeout 0) { // 超时处理例如记录错误日志复位SPI等 return ERROR; }临界区保护如果你的SPI驱动会在中断和主循环中被同时调用需要考虑使用关中断或信号量等机制来保护共享资源如SPI总线防止访问冲突。5. 常见问题排查与调试技巧实录即使理解了原理配置了代码第一次调通SPI也常常会遇到问题。下面是我在多年调试中总结的“排坑”清单。5.1 通信完全无反应检查电源和地线最基础也最容易被忽视。确保MCU和外设供电正常共地良好。检查引脚连接与配置用万用表或查看原理图确认SCK、MOSI、MISO、NSS四根线是否正确连接。确认GPIO模式配置正确SCK、MOSI为复用推挽输出MISO为浮空/上拉输入NSS若软件控制则为推挽输出。检查时钟使能确认RCC_APBxPeriphClockCmd已经使能了对应的SPI和GPIO时钟。检查片选信号用逻辑分析仪或示波器看NSS/CS引脚。通信期间是否被正确拉低拉低和拉高的时机是否正确一个常见错误是硬件NSS模式配置错误导致该引脚无法输出有效电平。我强烈建议初学者使用软件控制GPIO作为片选更直观可控。检查SPI是否使能确认SPI_Cmd(SPIx, ENABLE)已被执行。5.2 能发送但接收数据全为0或0xFF检查MISO线连接与配置确保MISO线已正确连接且GPIO模式配置为输入GPIO_Mode_IN_FLOATING或GPIO_Mode_IPU。检查从机是否被正确选中确保片选信号有效。检查从机是否支持全双工有些外设在特定命令下只接收或只发送。确认你发送的命令序列是否符合从机数据手册的要求。对于只读操作你发送的数据dummy byte是什么不重要但必须发送以产生时钟。检查CPOL和CPHA这是导致SPI通信失败的最高频原因务必使用逻辑分析仪抓取波形将SCK、MOSI、MISO的时序图与外设数据手册中的时序图进行严格比对。确认空闲电平、数据采样边沿完全一致。5.3 数据错位或字节顺序错误检查数据帧格式(DFF)确认主机和从机设置的数据位宽一致都是8位或都是16位。检查位顺序(LSBFIRST)99%的情况使用MSB First。除非外设手册明确要求LSB First否则不要改动。检查软件收发逻辑在连续读写多字节时注意字节序大端/小端。例如读一个32位的寄存器从机可能先发高字节你的接收代码需要按顺序组合。5.4 高速传输时数据出错降低时钟频率先尝试大幅降低SPI的波特率分频系数看通信是否恢复正常。如果恢复则可能是布线过长、干扰大、从机最高时钟频率限制等原因。检查PCB布局与走线SPI的SCK是高速时钟线应尽可能短并远离其他高频或模拟信号线。在长距离或干扰环境可以考虑在信号线上串联小电阻如22Ω~100Ω来抑制过冲和振铃。启用DMA并优化内存如果使用DMA确保源/目标内存地址是对齐的并且是非缓存Cache区域如果涉及Cortex-M7等带Cache的芯片或者使用__attribute__((aligned(4)))和__attribute__((section(.ram_dma)))等指令将DMA缓冲区放在合适的RAM中。检查中断优先级如果SPI通信被更高优先级的中断频繁打断可能导致数据丢失。适当调整SPI中断或DMA中断的优先级。5.5 逻辑分析仪你的“眼睛”没有逻辑分析仪调试SPI就像蒙着眼睛走迷宫。一个哪怕是最基础的逻辑分析仪如Saleae Logic 8克隆版也能极大提升调试效率。连接好线设置正确的采样率和协议解码器SPI你就能清晰地看到NSS、SCK、MOSI、MISO四路信号的实时波形。数据字节的解析结果十六进制或二进制显示。时序参数时钟频率、数据建立/保持时间是否满足从机要求。通过对比实际波形和数据手册的理想波形绝大部分通信问题都能被迅速定位。我个人在调试任何新的SPI外设时第一件事就是用逻辑分析仪抓取上电后的初始通信波形。这不仅能验证硬件连接和基础配置还能直观地理解外设的通信流程比反复烧录代码试错要高效十倍。最后STM32的SPI是一个看似简单但细节丰富的模块。从理解四线舞蹈的规则CPOL/CPHA到精准配置寄存器再到编写考虑超时、错误处理的健壮驱动每一步都需要耐心和实践。当你成功驱动起第一块SPI FLASH并稳定地读写数据时你对嵌入式系统“对话”的理解就真正上了一个台阶。记住多看数据手册善用调试工具遇到问题先抓波形这些习惯会让你在嵌入式开发的道路上走得更稳、更远。