news 2026/9/4 19:18:22

STM32 SPI+DMA双机通信:从接线到代码的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 SPI+DMA双机通信:从接线到代码的完整实践

简介:本例程面向使用STM32进行嵌入式开发与通信实验的工程师和学习者,解决两块STM32之间通过SPI接口高速传输数据的问题,并借助DMA方式减少CPU占用、提升系统利用率,同时兼顾LCD显示应用场景。压缩包共111个文件,约443KB,其中包含34个h头文件、32个c源文件以及32个s汇编启动文件,另有UVProj工程文件、配置文件、备份文件与文本说明等,覆盖标准外设库和工程构建所需的主要模块。工程结构清晰,涉及定时器、Flash、RCC、ADC、I2C、USART等多个外设驱动,c源文件与头文件相互对应,便于查阅底层寄存器和外设初始化逻辑,可帮助读者快速理解SPI主从配置及DMA传输链路的建立。目前已有1272人学习下载,说明该例程在SPI双机通信与DMA优化场景中具备一定参考价值,适合作为课程设计、毕设或实际项目中的通信模块样板。 两块STM32之间用SPI通信,再拉上DMA,这个组合我这两年在好几个项目里都用到过。双MCU架构很常见,比如一块板子负责采集和实时控制,另一块负责界面显示或者协议处理,板间数据量一旦上去,UART那点带宽就不太够用了。SPI是全双工、速率高,配合DMA之后,数据搬运基本不占用CPU,主控能腾出手来处理业务逻辑。这篇文章把我实际调通过的主从机接线、DMA配置、代码示例和排坑过程梳理出来,适合正在做双机通信,或者想把串口升级成SPI的同行参考。

1. 两块STM32做SPI通信,为什么一定要拉上DMA

1.1 先想清楚主从关系和速率需求

双MCU架构里,通常一主一从,比如采集板上放一颗STM32负责AD采样和电机控制,显示板上放另一颗STM32负责屏幕刷新和按键扫描。两块板子之间要传采样数据、状态字和控制指令,数据量不是一两个字节,而是一帧几百字节,刷新率还要到100Hz以上。这时候算一下:200字节一帧,100Hz刷新,就是160kbit/s,串口115200bps已经扛不住了,还得分包传输、频繁进中断,应用层处理起来很痛苦。

SPI天然是主从结构,主机提供时钟,从机被动跟随。两块STM32直连,SPI1的速率在F103上最高能到36MHz,在F407上最高42MHz,实际跑个几Mbps非常轻松,带宽根本不成问题。更重要的是,SPI是全双工,主机发数据的同时能收从机回的状态,不需要像UART那样把收发逻辑绕来绕去,帧设计也简单得多。

1.2 DMA到底省掉了什么

如果不加DMA,SPI每收发一个字节,CPU就要去处理一次中断,在中断里读写SPI_DR寄存器。速率低的时候还行,一旦速率提到几Mbps,CPU基本就陷在中断里出不来了,主循环实时性直接崩掉。DMA的核心理念是把“外设寄存器到内存”的搬运工作交给DMA控制器,CPU只在一帧数据全部传输完成时收到一次完成中断。

举个例子,一帧数据512字节,用查询模式或者中断模式,CPU要参与512次搬运;用了DMA,CPU发出一次传输请求后就可以去干别的任务,等DMA搬运完512字节再通知你。这个差别在高速率、大批量传输时非常明显。而且DMA还支持循环模式,可以持续搬运数据流,CPU只需要在合适的时间去取走数据就行。

1.3 为什么不是UART、I2C或者CAN

UART虽然简单,但标准波特率下带宽有限,且两块板子之间如果要双向高速传数据,UART的FIFO和中断机制要花很多心思去调。I2C有地址和ACK机制,但速度通常在400kHz级别,传大块数据偏慢。CAN虽然抗干扰能力强,但需要外接收发器,而且帧有效载荷最多8字节,大数据量拆包组包麻烦。SPI在两块STM32直连的场景下是最省事的方案:速率高、全双工、无协议开销。当然它也有缺点,就是没有应答和流控机制,所以帧协议得自己定,这个后面会说到。

2. 硬件连接和参数设计,先把地基打牢

2.1 接线:四线SPI之外,别忘了共地

两块STM32之间连接,标准SPI需要四根线:SCK、MOSI、MISO、CS,另外必须有一根GND把两块板子的地连起来。别小看共地这件事,我见过不少人只接四根信号线不接地,结果数据偶尔错位,逻辑分析仪上看波形全是过冲和毛刺。两块板子都是3.3V供电时可以直接连,只要电源是3.3V逻辑就行;如果哪边是5V系统,必须加电平转换芯片,不能直连。

连线长度尽量短,杜邦线超过10厘米就要开始警惕信号完整性。SPI是板级总线,不适合长距离传输。如果机箱内两块板子相隔几十厘米,速率必须降下来;如果隔着几米,那就别用SPI了,老老实实换CAN或者RS485。我自己的习惯是先用1MHz左右的速率让链路跑通,再用逻辑分析仪看波形,确定信号质量没问题再逐步提高分频系数。

2.2 时钟极性、相位和速率的搭配

SPI有四种模式,靠CPOL和CPHA两个参数区分。两块STM32做主从通信时,双方必须设成同样的模式,否则收发时序对不上,数据全是乱的。最常用的是模式0:CPOL=0(时钟空闲为低),CPHA=0(第一个边沿采样)。我自己做双机通信时,主从两边都固定用这个模式,简单可靠。

速率方面,主机的BaudRatePrescaler直接决定SCK频率。F103的SPI1挂载在APB2上,时钟72MHz,分频系数16就是4.5MHz,分频32就是2.25MHz。起步阶段建议从分频64甚至128开始,也就是1MHz左右,等通信稳定了再逐级提高。有人一上来就配36MHz,结果杜邦线稍长一点就出错,然后开始怀疑代码,其实换个低速试试马上就好了。信号不是只看理论速率,还要看实际布线和电平转换的压摆率。

2.3 片选策略:硬件NSS还是软件GPIO

片选是SPI通信里最容易出问题的地方。主机端的CS建议直接用普通GPIO软件控制,想拉低就拉低,想拉高就拉高,帧起始和结束的时序完全自己掌控。从机端的NSS建议配置成硬件模式,也就是让SPI外设自己检测CS引脚电平,CS变低时自动进入激活状态,这样能避免很多同步问题。

如果用主机硬件NSS模式,有时候CS时序不受控,容易出现提前拉高导致从机提前结束接收的情况。所以我一般的手法:主机SPI配置成NSS软件模式,同时把一个普通GPIO当CS用;从机SPI配置成NSS硬件模式,CS引脚直接接主机的GPIO。这里多说一句,从机如果配置成NSS软件模式,必须把SSI位设为1,否则从机不会进入接收状态,不少人就卡在这个细节上。

2.4 三线SPI和菊花链的注意事项

网上常说的三线SPI有两种含义。一种是指SCK、MOSI、MISO三根线不接CS,这种用法也可以,但主机和从机只能用软件机制保持同步,比如通过延时或者靠固定长度的帧来区分,我实际项目中很少这么用,因为CS线本身成本很低,却能带来明确的帧边界,非常值。

另一种是STM32的SPI单向模式,也就是只用一根数据线发送或者接收,方向固定。如果两块板子之间只是单方向传数据,比如采集板只往显示板发数据、不要求回传,那可以省掉MISO线,配置成SPI_DIRECTION_1LINE_TX或对应接收模式。但这属于“够用就行”的省法,如果后续要扩展交互命令,还是老老实实把MISO接上。菊花链适合一主多从且从机可以串联接收转发的场景,双板通信用不上,就不展开了。

3. DMA收发代码怎么写,才能稳定跑起来

3.1 确定DMA通道:F103和F407的映射别搞错

STM32家族里,不同类型的DMA请求映射不同,写代码之前先查数据手册里的DMA请求映射表。F103上,SPI1_RX对应DMA1_Channel2,SPI1_TX对应DMA1_Channel3。到了F407,SPI1的收发请求在DMA2上,RX对应Stream0、Channel3,TX对应Stream3、Channel3。

这里要特别提醒,一定要对着参考手册核对,哪怕是同型号的不同型号变体也可能有差异。CubeMX图形化配置可以自动完成映射,你只需要在DMA设置里选择SPI1_RX和SPI1_TX,它会自动填好Stream和Channel参数。如果你习惯手写标准库或者HAL初始化结构体,那就每一步都要对照手册来,一个Stream填错了,DMA传输根本不会触发,SPI数据就一直卡在发送寄存器里。

3.2 HAL库配置:SPI句柄与DMA句柄怎么关联

HAL库的核心思路是,每个外设的句柄里保存着它的DMA句柄指针,通过__HAL_LINKDMA这个宏把SPI句柄和DMA句柄关联起来。这一步不做,后面调用HAL_SPI_Transmit_DMA时函数内部找不到DMA句柄,直接返回错误。

__HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx); __HAL_LINKDMA(&hspi1, hdmarx, hdma_spi1_rx);

其实不只SPI,UART、ADC、DAC这些外设也都是同一个套路。HAL的DMA句柄里面除了channel配置,还要注意Direction参数:内存到外设是DMA_MEMORY_TO_PERIPH,外设到内存是DMA_PERIPH_TO_MEMORY。这个方向配置反了,发送会变成从外设读数据写回内存,现象就是SPI总线上一直没波形。另外,PeriphInc必须设为DMA_PINC_DISABLE,因为SPI的数据寄存器地址固定;MemInc要设为DMA_MINC_ENABLE,数据是从连续内存缓冲区读出或写入的。

3.3 主从机代码示例:从CS下降沿开始

先以主机发、从机收这个最基础场景为例。主机端,SPI配置成主机模式,CS用普通GPIO,发送前把CS拉低,然后调用HAL_SPI_Transmit_DMA,在DMA发送完成回调里把CS拉高。

// 主机发送一帧数据 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit_DMA(&hspi1, txBuf, FRAME_SIZE); // 发送完成回调 void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } }

从机端稍微讲究一点。从机是硬件NSS模式,CS引脚上有下降沿时SPI外设被激活,但这时候DMA接收如果还没准备好,SCK上来的数据就没人接收。稳妥的做法是:在从机上用外部中断检测CS下降沿,然后在外部中断回调里启动DMA接收。这个顺序保证了DMA一定先准备好,主机再发出时钟和数据。主机这边也要配合,CS拉低之后稍微延时几个微秒再启动SPI发送,给从机留出准备时间。

// 从机:CS下降沿外部中断 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == CS_Pin) { HAL_SPI_Receive_DMA(&hspi1, rxBuf, FRAME_SIZE); } } // 从机接收完成回调 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { frame_ready = 1; } }

如果主从两边需要同时收发数据,方向对等,都调用HAL_SPI_TransmitReceive_DMA。这个函数会同时配置TX和RX两条DMA通道,主机的MISO线上能收到从机数据,从机的MISO线上能收到主机数据,和普通全双工SPI一样,只是搬运工作全部交给了DMA。

3.4 循环模式与双缓冲模式怎么取舍

DMA的模式有两种常见选择:Normal(普通模式)和Circular(循环模式)。按帧通信时用Normal就够了,每传完一帧触发一次完成中断,CPU处理一帧数据,然后准备下一帧。如果数据是持续流,比如从机不断采集AD数据、通过SPI连续回传,那用Circular模式更合适,DMA会在缓冲区填满后自动回卷,配合双缓冲可以做到“一边DMA写数据,一边CPU读数据”,不用等传输完成再拷贝,减少延迟。

双缓冲的配置代码会复杂一些,需要调用HAL_DMAEx_ConfigDoubleBuffer,指定Memory0和Memory1的地址以及各自大小。我对双缓冲的建议是:先衡量需求,如果只是两块板子之间按帧通信,Normal模式+完成中断完全够用;如果是要持续传输数据流且对实时性要求很高,再上双缓冲。双缓冲调试时容易踩内存访问冲突的坑,因为CPU和DMA可能在同时访问同一个缓冲区,需要仔细设计切换时机。

4. 调试时遇到的坑和排查思路

4.1 首字节丢失:从机DMA启动太晚

这个现象非常典型:主机明明发了一帧N字节,从机收到的有效数据只有N-1个字节,或者第一字节是错的。排查下来,十有八九是从机的DMA接收没有提前准备好,SCK第一个边沿到来时,SPI外设已经把第一字节收进移位寄存器了,但DMA还没处于接收状态,这个字节就丢了。

解决思路有两个方向。一个是从机提前准备好DMA接收,比如上电后直接调用HAL_SPI_Receive_DMA,让DMA一直挂在接收状态,主机CS一拉低数据就能收进来。另一个是主机在CS拉低后、发送第一字节前,加一个延时,可以是一个字节时钟的宽度,也可以直接在代码里delay_us(10),给从机外部中断处理留出时间。两种方案我都在项目里用过,从机硬件NSS模式下,提前准备好DMA接收是更稳的做法。

4.2 数据错位或乱码:宽度和FIFO没配对

如果收到的数据字节数量对但内容错位,大概率是DMA数据宽度没有和SPI数据宽度匹配。SPI配置成8位数据宽度,DMA的PeriphDataAlignment和MemDataAlignment也必须都是BYTE。有人会把内存宽度配成WORD,结果DMA一次从内存读4个字节塞给SPI,发送自然乱套。

DMA的FIFO设置也可能引入问题。FIFO阈值需要和数据宽度匹配,8位数据宽度时用DMA_FIFO_THRESHOLD_1QUARTERFULL比较稳妥。如果数据量不大,我甚至建议直接关闭FIFO,使用Direct Mode直连模式,减少一层缓存逻辑,调试起来更直观。缓冲区内存对齐问题也要注意,DMA缓冲区建议用全局数组,定义时加上__attribute__((aligned(4)))或者放在指定内存区域,避免因地址非对齐导致访问异常。

4.3 中断和DMA打架:优先级与回调函数

还有一种情况,SPI外设中断和DMA中断同时开启,就会导致完成回调被触发两次,或者数据在处理过程中被下一帧覆盖。我的习惯是:用DMA传输时,SPI只配置DMA完成中断,不再单独开启SPI全局中断。DMA中断处理函数是HAL_SPI_TxCpltCallbackHAL_SPI_RxCpltCallback,以及全双工场景下的HAL_SPI_TxRxCpltCallback。注意别找错回调,全双工完成不会走单独的TX或RX回调。

DMA中断优先级也要调一调,不能比主循环里其他关键中断低太多。之前我把DMA中断设为默认优先级,结果系统在跑高负载任务时,DMA完成中断延迟了几百微秒,主机已经发出下一帧数据,从机上一帧还没处理完,数据就覆盖了。后来把DMA中断优先级提到最高一档,问题就消失了。

4.4 排查速查表

现象可能原因检查点解决办法
完全收不到数据SPI模式不匹配或DMA通道错误主从CPOL/CPHA是否一致,DMA Stream/Channel是否对应统一使用Mode0,对照参考手册核对DMA映射
首字节丢失从机DMA启动太晚主机CS之后是否给从机留准备时间从机提前挂DMA接收,主机拉低CS后延时几个us
数据错位乱码DMA宽度配置错误PeriphDataAlignment与MemDataAlignment全部设为BYTE,关闭FIFO先用DirectMode
连续传输丢数据DMA中断优先级太低NVIC中断优先级配置把DMA中断优先级调高
数据偶尔跳变未共地或线太长用示波器看SCK和MISO波形补齐GND线,缩短连线,降低SCK频率
从机一直接收失败从机NSS软件模式但SSI未置1检查从机SPI CR1寄存器的SSI位从机改用硬件NSS模式,或手动置位SSI

5. 几点实测体会,当个收尾

说点实在的。我每次做两块STM32的SPI通信,都不会一上来就直接写DMA,而是先用查询模式把链路验证一遍。查询模式代码简单,主循环里调用HAL_SPI_TransmitHAL_SPI_Receive,确认硬件接线、SPI模式、CS时序都没问题,再替换成DMA版本。这样一旦后面出问题,排查范围就能缩小到DMA配置本身,而不是硬件和软件混在一起,能省一大半调试时间。

DMA缓冲区用全局数组,少用局部变量,局部变量在栈上的地址存在不确定性,而且生命周期结束后缓冲区内容会被覆盖。F4以上型号如果开了DCache,DMA写入内存后CPU可能读到Cache里的旧数据,这时候需要在DMA接收完成后做Cache Invalidate,或者直接配置MPU把DMA缓冲区设置为Non-cacheable区域。这个坑我在F407上踩过,表现就是数据总是隔几帧跳变一次,查了一下午才发现是Cache一致性问题。

最后提一个协议层面的建议:两个STM32之间传数据,不要只发裸数据,最好加一个简单帧头,比如0xAA 0x55,后面跟长度和数据,接收方校验帧头后再解析。SPI没有应答机制,线上一个毛刺就可能丢字节,帧头校验能兜住大部分问题。我自己后来做板间通信,都是统一用这套方法:SPI+DMA传输,CS做帧同步,帧头校验,稳定跑了好几个项目都没再出过幺蛾子。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 15:43:50

IAI电缸驱动器编辑软件:从安装调试到运动控制编程实战

简介:本资源为IAI电缸驱动器专用编辑软件RCM-101-MW_USB安装包,面向自动化产线调试工程师、运动控制技术人员及机电一体化专业学习者,用于对IAI多系列控制器(含PCON、ACON、SCON、ERC2、ROBONET等共12类)进行参数配置、…

作者头像 李华
网站建设 2026/9/4 20:05:13

龙架构双周会生肉消化指南:从补丁到QEMU复现

如果你看到一条标题是“【生肉】外语龙架构双周会第 7 期(2026 年 8 月 6 日)”,第一反应可能是“没字幕,跳过”。我的建议恰恰相反:这种龙架构双周会录像是目前信息密度最高的生态入口之一。所谓生肉,只是…

作者头像 李华
网站建设 2026/9/4 15:21:14

如何准备一篇高质量CSDN技术博文?从主题到素材全攻略

收到你的标题信息,但无法直接生成一篇合格的技术博文。 原因是:CSDN 技术教程的文章必须有一个明确的技术主题,而目前只提供了“藿藿”这一个标题(这更像是游戏角色、项目代号或网络用语),没有提供项目正文…

作者头像 李华
网站建设 2026/9/4 9:09:50

RISC-V五级流水线CPU实战:从仿真到FPGA上板

简介:本资源是一套完整实现RISC-V五级流水线架构的CPU课程设计项目,面向计算机组成原理、数字逻辑与体系结构等课程的本科生,解决从指令集理解、模块划分到时序验证的全流程实践难点。压缩包共99个文件,含55个Verilog源码&#xf…

作者头像 李华
网站建设 2026/9/4 13:00:26

欧姆龙CP1H串口通讯实战:Host Link与RS485 Modbus-RTU全解析

简介:面向工业自动化开发者与PLC调试工程师,这份资源围绕OMRON PLC串口通讯实例展开,以Visual Studio解决方案形式提供完整的上位机通讯程序,覆盖串口参数设置、通讯协议封装、指令收发与响应解析等关键环节,既适合初学…

作者头像 李华