STM32 SPI双机高速通信方案:中断驱动实现全双工数据交换

📅 发布时间:2026/9/9 15:07:43
STM32 SPI双机高速通信方案:中断驱动实现全双工数据交换
简介面向STM32开发者与嵌入式初学者的SPI双机中断通信资料包涵盖从底层寄存器配置到UCOS III系统集成的完整实现思路。资源总计618个文件以C源文件140个、头文件130个为主同时包含编译生成的o、d、crf、hex等中间文件以及uvprojx工程文件、map映射文件压缩包约8.35MB方便直接导入MDK进行编译与调试。已有4663人学习下载属于较高热度的实战型参考资源。内容深入讲解SPI主从模式配置、TXE/RXNE中断处理、信号量同步机制并给出多任务通信测试方案以及示波器波形检查方法。借助工程源码与编译链接文件读者可快速复现双机SPI中断通信流程排查时序与优先级配置问题针对通信中的溢出或错误也可参考资源中的异常处理思路。适合需要基于STM32和UCOS III实现高效、实时数据交换的研发场景尤其适合快速上手与工程移植。 最近在调一套双机协作的控制项目一块主板负责逻辑运算和对外通信一块从板负责传感器采集与执行机构控制两板间距不到10厘米FPC排线连接。通信要求很直接——数据量大、刷新率高、还不能让CPU在通信上耗太多时间。最开始我用串口DMA波特率拉到921600双向收发加应答跑起来不仅CPU占用高偶尔还会因为两边波特率误差冒出丢字节的问题。后来干脆切到STM32 SPI双机中断通信主机SPI1加从机SPI1全双工全部走中断再配一根IRQ事件通知线彻底从轮询里解放出来。这篇把整个方案的选型、接线、CubeMX配置、代码框架和调试踩过的坑都整理出来给准备做板级双MCU通信的朋友一个能直接上手的参考。1. 项目背景与方案选型1.1 双机通信为什么锁定了 SPI做板级双MCU通信绕不开三种常见总线UART、I2C、SPI。我一开始用UART主要原因是代码简单、随处可用但在双向高速传输的场景下它有硬伤两边靠波特率对齐时序只要双方系统时钟存在偏差或者某一端中断响应不及时就可能整帧错位。尤其是两套系统都用内部RC振荡器的时候误差叠加起来非常难受。I2C我排除了原因是它是半双工加开源上拉协议本身还带了应答和地址机制双机固定连接根本用不上这些反而每次读写都要好几层的寄存器操作速度上限也低。SPI的优势在这个需求下几乎是为双机通信量身定做的全双工MOSI和MISO双向同时走同步传输时钟完全由主机掌控从机只要跟着SCK走就行速度可以压到F103 SPI1上最高的18MHz我们实际跑到4.5MHz已经非常稳协议极简没有地址、没有ACK、没有帧格式限制数据想怎么组织就怎么组织。板级固定连接、不需要热插拔、不需要仲裁SPI那些缺点全部无关痛痒。综合下来SPI是这里最合适的选择。1.2 SPI从机无法主动发数据所以必须要有中断这里要先说清楚一个很多人一开始没转过弯来的点SPI的时钟永远由主机产生从机哪怕再急也拉不出一个时钟。从机想上报数据只能先通过一根外部信号线通知主机主机收到通知后再发起SPI事务把数据读走。我在方案里加了一根IRQ线就是干这件事的。中断的作用也很直观。4.5MHz的SPI一个字节传输大约2.2微秒如果一帧8字节就是18微秒左右。用轮询方式发送时CPU只能一直在循环里等TXE标志这段时间什么正事都干不了。走中断后每个字节的发送和接收都由SPI外设自动触发中断CPU只在中断里搬一次数据剩余时间该跑任务跑任务。在双机协作场景里主从两个CPU本来就有各自的实时任务这个差异非常明显。所以在我的设计里SPI中断负责数据搬运外部中断负责CS边沿和IRQ事件通知整个通信链路完全异步化。2. 硬件连接与 GPIO 规划2.1 四线SPI加IRQ的具体接线我用的两块板子都是STM32F103C8T6SPI1同一组引脚接线如下主机端从机端说明PA5 SCKPA5 SCKSPI时钟主机输出PA7 MOSIPA7 MOSI主机输出到从机输入PA6 MISOPA6 MISO从机输出到主机输入PA4 CSPA4 CS片选主机GPIO推挽输出PB0PB1IRQ事件通知线主机推挽输出到从机EXTI输入几个注意点。第一MOSI和MISO不要接反这是新手最容易犯的错主机MOSI要接从机MOSI主机MISO接从机MISO方向由角色决定而不是由引脚名决定。第二两板必须共地这是所有通信正常工作的前提不共地的话数据线电平参考点都不一样没信号都很正常。第三板间距离短的时候随便接都行距离超过10厘米或者走线经过电机、继电器这类干扰源就要把SPI分频调低必要时在SCK和MOSI上串联22欧到33欧电阻能明显改善信号边沿质量。2.2 从机NSS的两种处理路径以及我的选择从机的NSS引脚处理是SPI双机里最容易踩坑的地方。STM32的SPI从机在硬件NSS模式下外设会自己感知NSS引脚的边沿CS拉低时进入传输状态。听起来挺好但实际操作中很容易出现只能收一次就再也不收或者初始化时NSS电平不对导致外设直接罢工的问题因为硬件NSS对时序要求比较苛刻HAL库里相关状态也不直观。我最后用的是方案B从机SPI配置成软件NSS管理SSM1SSI1让SPI外设永远认为自己处于被选中状态不需要靠NSS引脚来激活同时把CS引脚单独配置为EXTI下降沿中断用GPIO中断来记录主机事务开始这个事件。这样SPI纯粹靠SCK驱动CS只作为事务开始信号逻辑上非常清晰。代价是CS和SCK之间需要有一小段提前量主机必须在拉低CS后稍作延时再开始发送给从机EXTI中断留出触发时间。这个提前量在4.5MHz下给1到2微秒就足够我实际测试从机中断触发和SPI接收准备都能正确跟上没有任何漏帧。3. CubeMX初始化与中断优先级配置3.1 SPI参数配置的关键细节CubeMX的配置并不复杂但有几个参数必须统一。主机和从机的SPI1都设置成Full-Duplex模式8位数据MSB First。时钟极性CPOLLow时钟相位CPHA1 Edge也就是SPI Mode 0这是多数SPI器件和外设最常用的模式主机从机两边必须完全一致。主机这边Prescaler我选择了8分频F103的SPI1挂在APB2总线上默认36MHz8分频后就是4.5MHz。F103的SPI最高能到18MHz但双机互联还要考虑排线质量和对端中断响应速度4.5MHz是一个稳重又不会浪费性能的档位。从机的Prescaler在Slave模式下其实不产生时钟和发送速率无关但CubeMX仍会让你填一个值保持默认或者和主机一致就行。NSS设置上主机把NSS配置为软件管理CS引脚完全由普通GPIO控制。从机同样配置为软件管理同时把CS引脚复用为EXTI功能。这里要特别提醒从机的NSS引脚如果你在CubeMX里把它配置成了硬件NSS后面大概率会出问题建议从工程一开始就按软件NSS加EXTI这套来设计。3.2 外部中断和NVIC优先级的分配思路要开启的NVIC通道包括SPI1全局中断、PA4对应的EXTI4中断、PB1对应的EXTI1中断。我的优先级分配策略是SPI中段最高EXTI次之串口再次之SysTick保持默认。中断抢占优先级从优先级说明SPI110字节流不能断优先级最高EXTI4/EXTI120CS边沿和IRQ事件只置标志位USART130调试打印可以容忍延迟SysTick150HAL库时基放最低即可SPI中断优先级必须比其他普通外设高原因是SPI接收端如果没能及时读走RXNE里的数据下一个字节到达时就会产生Overrun错误数据直接丢。从机端对这个尤其敏感因为SCK是主机控制的主机改不了节奏从机一旦被更高优先级的中断卡住几十微秒字节就没了。EXTI回调里不能做耗时操作只设标志位真正解析和处理放到主循环。4. 中断收发核心代码实现4.1 主机端用全双工中断事务一次完成收发主机端我采用的是HAL_SPI_TransmitReceive_IT一次事务同时完成发送和接收。一开始我也试过先Transmit_IT再Receive_IT但SPI是全双工发送时钟出现的同时MISO上其实已经有从机的应答数据了分两步走等于白白把响应拖到下一轮事务。用TransmitReceive第一帧请求发出去的同时就能收到从机上一轮预置好的响应效率高而且状态机也简单。uint8_t master_tx[8]; uint8_t master_rx[8]; volatile uint8_t frame_busy 0; void Master_ExchangeFrame(uint8_t *req, uint8_t *resp, uint16_t len) { while (frame_busy); // 等待上一轮事务结束 memcpy(master_tx, req, len); memset(master_rx, 0, len); frame_busy 1; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); // 拉低CS事务开始 Delay_us(2); // 给从机EXTI留出准备时间 HAL_SPI_TransmitReceive_IT(hspi1, master_tx, master_rx, len); } void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 拉高CS事务结束 frame_busy 0; ParseFromSlave(master_rx); } }注意Delay_us(2)这行主机拉低CS后从机的EXTI中断需要一点时间被触发并进入SPI接收状态。这个延时我在4.5MHz下固定用2微秒实测足够。如果你很快或者用的分频更高需要适当加大这个提前量否则从机还没准备好SCK就来了。4.2 从机端CS边沿触发事务被动同步应答从机端的核心思路是CS下降沿触发一次固定长度的全双工SPI中断事务在这次的8个时钟里从机一边接收主机的命令一边把上一轮准备好的响应数据通过MISO发出去。事务完成后在回调里解析命令并立刻填充下一轮的响应缓冲区。#define FRAME_LEN 8 uint8_t slave_tx[FRAME_LEN]; uint8_t slave_rx[FRAME_LEN]; volatile uint8_t slave_event 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin CS_Pin) { // 主机CS拉低开始一轮固定长度全双工事务 HAL_SPI_TransmitReceive_IT(hspi1, slave_tx, slave_rx, FRAME_LEN); } else if (GPIO_Pin IRQ_Pin) { slave_event | EVT_HOST_REQUEST; // 主机请求事件置标志即可 } } void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { ParseHostCmd(slave_rx); // 解析主机命令 PrepareResponse(slave_tx); // 填充下一轮应答 } }很多朋友在这里会用HAL_SPI_Receive_IT做从机接收然后在接收完成回调里再调HAL_SPI_Transmit_IT去回数据。这在逻辑上说得通但实际操作很容易踩到HAL库的坑同一个hspi句柄同时管理发送和接收状态你在接收回调里再启动发送如果上一次事务的中断状态没有完全释放就可能返回HAL_BUSY程序就卡住了。把从机也统一到TransmitReceive_IT让HAL库内部状态一致是最省心的做法。4.3 固定长度帧协议设计因为方案里CS每次拉低都是一轮固定长度的事务所以帧长度必须固定我这里定的是8字节帧头0xAA、命令字、4字节数据、CRC8校验、帧尾0x55。命令字用来区分是主机下发参数、读取从机状态还是从机主动上报事件。CRC8用简单的查表算法在中断回调里计算时间可以忽略不计。回调函数里只做解析和填充响应不做复杂的业务逻辑。比如根据命令计算结果这种事应该置一个标志位等主循环再去处理。中断回调占用越短系统就越稳定这一条比其他任何优化都重要。5. 调试过程与常见问题实录5.1 实测中遇到的五个典型问题调试过程中我把能踩的坑几乎踩了一遍整理成速查表给后面的人避雷现象可能原因解决思路从机完全收不到数据从机NSS配置错误或者CPOL/CPHA不一致从机改用软件NSSEXTI方案主从都统一Mode 0数据乱码SPI速率过高排线太长或受干扰分频从18M降到4.5MSCK和MOSI串22欧电阻偶尔丢一个字节SPI中断优先级不够高或者回调里耗时过长提高SPI中断优先级回调里只做标志位处理从机回发全是0xFF主机时钟到来时从机数据还没装进DR从机用TransmitReceive_ITCS下降沿时预装响应通信偶尔出错且无法复现缺少校验和重传机制帧末尾加CRC8主机发现校验错就重发一次这些现象里最常见也最隐蔽的是第二个和第四个。从机一直回0xFF基本可以断定不是SPI模式问题而是数据根本没在正确的时间点写进从机的数据寄存器。SPI从机的MISO引脚在主机时钟到来时移出的就是DR里当前的数据如果DR是空的或者没来得及写入就会发0xFF。这就是为什么我强调CS下降沿就要预装响应而不是等到接收完成再去发。5.2 没有逻辑分析仪也能定位问题的土办法说实话调SPI这种东西没有逻辑分析仪会非常痛苦但也不是完全没法做。最实用的土办法是主机自发自收测试把主机板的MOSI和MISO用杜邦线短接发送一组已知数据看收回来的是不是自己发的。如果自发自收通过说明SPI外设初始化没问题接下来再分两头排查从机。第二个办法是用GPIO辅助观测中断耗时。在SPI中断服务函数入口把一个测试引脚拉高出口拉低用示波器看这个引脚的高电平宽度就能知道每次中断花了多少时间。如果这个时间接近甚至超过一个字节的传输时间那Overrun是迟早的事。另一个办法是某些引脚在CubeMX里没有被占用时可以用串口把中断触发次数、丢帧计数这些调试变量定周期打印出来间接判断中断有没有被打断。5.3 我最终验证的稳定性数据调通之后我做了连续12小时的压力测试主机每10毫秒发起一帧每帧8字节24小时累计超过800万帧CRC校验错误率在百万分之一以下出现错误后靠简单的三帧重传机制就能恢复。这还是在FPC排线连接、板间没有做屏蔽的情况下跑出来的对于双机协作控制项目来说已经完全够用。如果后续还要加大数据量或者进一步提高实时性方向就是把中断改成DMA加双缓冲同时把帧长度从固定帧改成固定头加变长体的结构但基本架构上已经不需要再动。最后说一句心得。SPI双机通信这种事99%的问题最后都能归到三个点上模式对不对、CS时序对不对、中断里处理时间是不是太长了。我在这项目里来回折腾最久的就是从机NSS配置和从机响应预装载把这两个点理顺之后整体通信就再没出过大问题。如果你正准备做双机通信我建议先把固定帧长、全双工事务、GPIO软CS这套组合跑通再考虑上DMA和更复杂的协议栈。这套地基扎实了上面盖什么都不会塌。本文还有配套的精品资源点击获取