STM32 SPI+DMA双机通信实战:从硬件接线到CS时序避坑指南 简介面向STM32嵌入式开发者的双机SPI通信例程专门解决两块STM32之间通过SPI总线进行高速数据交换时的CPU占用问题借助DMA传输机制让数据搬运不再依赖中断频繁打断内核显著提升CPU利用率与传输效率。工程基于标准外设库编写代码结构完整包含头文件、源文件与启动文件适合正在学习STM32通信机制、需要DMASPI工程参考的开发者也适合作为毕业设计或项目初期的验证模板。压缩包共111个文件其中34个.h头文件、32个.c源文件及32个汇编.s启动文件构成核心工程另有工程配置文件、备份文件与说明文档等整体仅443KB结构紧凑、便于直接导入编译。目前已有1272人学习下载。通过此例程可掌握SPI主从模式配置、DMA通道初始化与传输完成中断处理流程并可在LCD屏上观察通信结果代码注释清晰可作为实际项目中的通信模板快速移植。 两块STM32之间用SPI通信听着不难但一旦加上DMA很多人就卡在“从机DMA接收长度未知”“CS提前释放导致最后一个字节错位”这类问题上。我最早做一主一从的数据采集板时也是这样阻塞式SPI调通了一帧512字节要占CPU近1毫秒主循环里的屏幕刷新明显卡顿改成DMA后CPU占用掉到几乎为零但随之而来的是一堆回调时序坑。这篇文章就围绕“两块STM32通过SPI加DMA做双向通信”这个具体场景把硬件连接、CubeMX配置、HAL库调用和调试中常见的坑从头理一遍适合SPI基本收发已经跑通、准备用DMA降CPU占用的人。1. 为什么SPI要拉上DMA阻塞传输的CPU占用与实时性损失1.1 阻塞式SPI的CPU占用从哪来很多人在刚开始调SPI时用的都是HAL_SPI_Transmit / HAL_SPI_Receive这类阻塞接口。每发一个字节CPU都会循环等待TXE标志每收一个字节CPU又会循环等待RXNE标志。SPI主时钟每输出8个脉冲才能传一个字节CPU就在这个等待循环里一直空转数据量一大整段时间都被“焊死”在SPI函数里。实测过一组数据SPI时钟10MHz一次传512字节物理传输时间大约409微秒而阻塞方式下CPU忙等的时间也差不多是400多微秒。在裸机系统里这400微秒意味着按键扫描、LCD刷新、AD采样都可能被推迟在RTOS里虽然可以用任务调度隐藏一部分但通信任务独占CPU这么久其他任务的实时性还是会受影响。1.2 DMA到底省了哪部分时间DMA的引入是把“内存和SPI数据寄存器之间的搬运”完全交给DMA控制器。SPI每收到一个字节硬件RXNE事件会触发DMA把数据从DR搬到内存每发送一个字节TXE事件会触发DMA把内存里的下一个字节写入DR。CPU需要做的只是在启动DMA时设置好源地址、目的地址、传输长度然后在传输完成中断里做一次收尾。这样CPU占用从“持续忙等”降到了“发起和收尾”中间一大段时间完全还给业务逻辑。需要注意DMA并没有缩短物理传输时间SPI时钟该是10MHz还是10MHz它省的是CPU参与搬运的时间。这一点想清楚后面调优方向就不会跑偏。1.3 数据量小就别为了DMA而DMADMA不是银弹。每次启动DMA都有初始化、中断响应、回调处理等额外开销如果一帧只传4个字节阻塞方式的那点等待几乎可以忽略硬上DMA反而增加复杂度出问题后定位也更麻烦。DMA真正适合的场景是“大数据量、高频次、后台传输”。比如两块STM32持续交换控制命令和设备状态一帧1KB或者要做双缓冲流水传输。新手项目如果只是几个字节的简单通信建议先把SPI协议、CS时序、电平匹配这些基础搞定再考虑上DMA。基础不牢的时候引入DMA往往会把“SPI没调通”变成“DMA和SPI都没调通”。2. 双机硬件联调前先定好的几件事线序、片选和SPI模式2.1 线序MOSI接MOSIMISO接MISO别交叉STM32的引脚命名在主机端和从机端容易把人绕晕。主机端叫MOSIMaster Out Slave In和MISOMaster In Slave Out从机端通常也叫MOSI和MISO但语义变了从机的MOSI其实是数据输入口从机的MISO才是数据输出口。所以正确接法是主机的MOSI连从机的MOSI主机的MISO连从机的MISOSCK对SCKCS对CS最后共地。很多初学者想当然地交叉连接MOSI-MISO结果数据全乱原因就是只看引脚名称、没理解“Out/In是相对主机来说的”。另外还要检查电平如果两块板子供电不同比如一块3.3V、一块5VSPI引脚直接怼着接有烧引脚风险最好加电平转换。即使都是3.3V不同开发板的供电地也要可靠连在一起否则信号漂移会非常明显。2.2 硬件片选还是软件片选DMA场景我推荐软件CS硬件NSS由SPI外设自动管理看似省事但和DMA配合时经常出问题。CS释放时机往往和SPI最后一个字节不同步尤其在一些STM32系列上DMA写完最后一个数据后CS立刻拉高从机还没来得及采样最后一位导致末尾数据错位。我做过两块F103板子的主从通信用硬件NSS时经常少收最后一个字节或收到多余数据。改成软件片选后用一个GPIO手动拉低CS、启动DMA在传输完成回调里等几个微秒再拉高CS稳定性明显改善。如果整套系统只有一块从机也可以把从机NSS直接固定为低电平但这样从机无法通过CS沿来复位内部状态协议字节对齐需要额外处理。综合来看还是软件控制CS最灵活也最好排查。2.3 CPOL/CPHA和速率不是小事两块STM32的CPOL和CPHA配置不一致常见表现是偶发错位或首字节丢失。最省心的组合是SPI模式0也就是CPOL0、CPHA0主机从机都设成一样。不要在从机侧随意改模式除非你非常清楚对方外设的采样沿逻辑。速率方面板级通信10MHz以上对连线要求很高。两块独立开发板用杜邦线连接时强烈建议先降到1MHz验证通信再逐步提升。如果发现速率提高后数据偶发错位不要急着查DMA代码先看波形上升沿/下降沿有没有明显过冲或振铃。另外主从两端的SPI数据帧格式也要一致比如都选8位不要一端8位另一端16位否则解析会直接错位。3. 用HAL库实现双机SPI-DMA收发从CubeMX配置到核心代码3.1 CubeMX里的三处配置第一处是SPI模式。主机选Master从机选Slave方向都选Full-Duplex硬件NSS设为Disable改用GPIO软件片选。第二处是DMA设置。在DMA Settings里添加SPIx_RX和SPIx_TX两个请求模式选Normal数据宽度按你要传的数据格式选Byte优先级给Medium以上。第三处是NVIC中断。SPI的全局中断、DMA的传输完成中断都要勾选。CubeMX生成代码后不要去手动改HAL_SPI_HandleTypeDef里的DMA句柄关联生成时会自动把hdma_spi1_rx和hdma_spi1_tx挂到hspi1上。热词里提到的“dma句柄与dac句柄关联”是另一回事DAC和SPI的DMA句柄结构类似但关联逻辑仍由CubeMX自动处理手动乱改反而会出现初始化顺序问题。3.2 主机侧用TransmitReceive_DMA而不是两个独立调用SPI本质是全双工主机发送每个字节的同时也会收到一个字节。如果只调用HAL_SPI_Transmit_DMA接收数据会堆在SPI的DR寄存器里不读出来就会溢出。两块STM32做双向通信最方便的是直接用同一个函数完成收发uint8_t aTxBuf[FRAME_LEN]; uint8_t aRxBuf[FRAME_LEN]; HAL_SPI_TransmitReceive_DMA(hspi1, aTxBuf, aRxBuf, FRAME_LEN);这个函数会同时启动发送DMA和接收DMA主机产生SCK后每个周期发一个字节、收一个字节。调用前要确保aTxBuf和aRxBuf长度都至少是FRAME_LEN发送缓冲里提前填好本机要发出去的数据。完成一次传输后会触发HAL_SPI_TxRxCpltCallback。3.3 从机侧必须先挂好DMA再等时钟从机不产生SCK它的DMA接收不会自己触发。要从机配合通信必须在主机启动传输之前就让从机进入等待状态HAL_SPI_TransmitReceive_DMA(hspi2, aSlaveTxBuf, aSlaveRxBuf, FRAME_LEN);执行完这条语句后从机的SPI外设和DMA通道都处于就绪状态但不占用CPU。主机一旦拉低CS并输出SCK从机就在硬件层面自动完成数据收发。这里有个非常容易踩的坑如果从机还没调用DMA主机就发起传输从机端的数据会直接挤在数据寄存器里或者触发溢出错误。所以项目里最好约定主机先通过一根普通GPIO线或UART通知从机“我要开始传了”或者干脆让从机上电后就先调用DMA等待函数。3.4 从机“不知道要收多长”怎么处理DMA接收前必须指定长度所以两块STM32之间建议使用固定帧长协议。比如每条消息固定64字节前2字节放命令中间放数据最后2字节放CRC校验。如果一定要变长帧可以这样做第一种利用定时器做接收超时判断CS低电平时持续接收超过一段时间没有新字节就算一帧结束再按实际长度处理。第二种利用CS的上升沿作为帧结束标志在上升沿时读取DMA剩余计数器推算出实际接收到的字节数。我在项目里最常用的是固定帧长简单、可靠不依赖额外外设。因为DMA和CS时序配合起来后变长帧会显著增加边际条件出bug的概率也会高很多。4. 实测最容易翻车的三个点回调重入、CS释放和MISO接线4.1 传输完成回调里再启动下一轮的策略收到HAL_SPI_TxRxCpltCallback回调只代表当前帧的DMA传输已经结束但SPI外设可能还有一些尾部状态没清干净比如BSY标志还处于忙状态。直接在回调里立刻调用下一次HAL_SPI_TransmitReceive_DMA有时候会偶发失败。我习惯在主循环里置一个标志位由主循环发起下一帧如果必须在回调里连续传输就先调用HAL_SPI_DMAStop(hspi)停一次等状态稳定后再启动。另外回调函数名要写对TransmitReceive完成时走的是HAL_SPI_TxRxCpltCallback不是HAL_SPI_TxCpltCallback或HAL_SPI_RxCpltCallback。很多人在错误回调里等不到事件还以为DMA没配置好其实只是回调函数挂错了。4.2 数据错位的根源往往是CS时序而不是DMA典型的错位现象是从机收到的数据整体左移或右移一个字节或者最后一个字节变成0xFF。这种问题只查DMA配置很难发现因为DMA本身没有错真正原因通常是主机在DMA传输完成后马上释放了CS而从机还没有完成最后一个字节的采样或转移。处理办法是让主机在传输完成回调里保持CS低电平若干微秒我通常加2到5微秒再拉高CS。如果你用的是软件CS可以在拉高前检查从机是否也进入了完成回调两边都对齐了再释放。简单说CS就是帧同步线它的释放时机直接决定对端能不能正确锁定帧边界。把这个问题想明白了很多SPIDMA的“灵异现象”都能解释清楚。4.3 用排查表理清方向别上来就抓DMA现象优先检查项完全收不到数据SCK是否连接主机是否有时钟输出DMA是否配置成了TX-only数据整体错位CS释放过早CPOL/CPHA不一致MOSI/MISO线序接反偶发丢字节电源地和信号地没有共地速率过高CS悬空受干扰收到全0xFFMISO线没接好从机MISO未配置为复用推挽输出排查顺序也有讲究先不要用DMA用阻塞模式在两块板子之间跑几个字节确认SPI物理层和参数正确再用逻辑分析仪抓SCK、CS和MOSI/MISO波形看CS释放点与最后一个SCK上升沿的相对位置最后才把DMA加回来。跳过前两步直接查DMA配置通常会浪费大量时间。5. 进阶玩法双缓冲DMA、三线SPI与长线传输的取舍5.1 双缓冲DMA连续传输不打断在需要持续交换数据的场景单个Normal模式DMA每次传输完都要CPU重新配置帧间会出现一段空档。可以用DMA双缓冲模式配置两个内存缓冲区DMA在搬第一个缓冲区时CPU可以填充第二个缓冲区然后自动切换。STM32上通常通过DMA控制器的双缓冲模式实现配合传输完成中断和半传输中断可以把“一边收发、一边处理”做成流水线。对两块STM32的SPI从机来说双缓冲能让从机一直处于接收就绪状态减少主机和从机的启动时隙差。但帧长依然是固定的否则缓冲区切换点无法对齐反而会越传越乱。5.2 三线SPI和菊花链少一根线但代价不小热词里能看到“stm32三线spi”。严格意义上的三线SPI指SCK、CS和一根双向数据线常出现在屏幕和Flash芯片上由主机半双工控制。两块STM32之间如果只要求主机单向发送、从机接收理论上可以用三线SPI但DMA在半双工模式下需要处理方向切换实际效率并不一定更高。我的建议是双机通信除非受引脚数限制否则还是用标准四线全双工否则调试成本会明显上升。“SPI菊花链”则是把多片从机串联起来共享SCK和CS数据像移位寄存器一样从第一片传到第二片。两片STM32做菊花链意义不大但如果将来要组多机系统可以研究一下它能把多个片选信号合并成一个节省GPIO。需要注意的是STM32从机并不是专门为菊花链设计的能否按你的数据格式做转发必须自己验证。5.3 长线通信SPI不是出租车距离长了老老实实换方案SPI是为板内或近距离通信设计的正常建议距离在10到20厘米以内。两块独立STM32用杜邦线拉长到30厘米以上速率稍高就容易误码。可以尝试降到1MHz、使用屏蔽线或双绞线、在发送端串联22到33欧姆电阻抑制振铃但这些只是补偿不是根治。如果两块板卡要相距1米以上建议直接改用UART加RS485、CAN或者加SPI转光纤/以太网模块。热词里经常有人搜“spi通信方式通信距离”说明很多人踩过这个坑。我的观点是不要在SPI物理层死磕长距离换总线方案才是正确选择。最后分享一点我个人调双板SPI-DMA的经验。不要一上来就用示波器抓满通道先固定一帧简单数据比如0x55、0xAA交替用阻塞方式把数据路径打通再上DMADMA跑通后再设计真正的工作帧。这样能把“接线问题”和“DMA配置问题”分开通常一个晚上就能调完。还有从机端的NSS引脚如果不用一定要配置成内部上拉或者软件控制别让它悬空否则抖动会在片选沿上制造假帧这种问题最难查。本文还有配套的精品资源点击获取