STM32 SPI+DMA双机通信:从接线到代码的完整实践 📅 发布时间:2026/9/2 8:58:36 👁 浏览次数: 简介本例程面向使用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通信为什么一定要拉上DMA1.1 先想清楚主从关系和速率需求双MCU架构里通常一主一从比如采集板上放一颗STM32负责AD采样和电机控制显示板上放另一颗STM32负责屏幕刷新和按键扫描。两块板子之间要传采样数据、状态字和控制指令数据量不是一两个字节而是一帧几百字节刷新率还要到100Hz以上。这时候算一下200字节一帧100Hz刷新就是160kbit/s串口115200bps已经扛不住了还得分包传输、频繁进中断应用层处理起来很痛苦。SPI天然是主从结构主机提供时钟从机被动跟随。两块STM32直连SPI1的速率在F103上最高能到36MHz在F407上最高42MHz实际跑个几Mbps非常轻松带宽根本不成问题。更重要的是SPI是全双工主机发数据的同时能收从机回的状态不需要像UART那样把收发逻辑绕来绕去帧设计也简单得多。1.2 DMA到底省掉了什么如果不加DMASPI每收发一个字节CPU就要去处理一次中断在中断里读写SPI_DR寄存器。速率低的时候还行一旦速率提到几MbpsCPU基本就陷在中断里出不来了主循环实时性直接崩掉。DMA的核心理念是把“外设寄存器到内存”的搬运工作交给DMA控制器CPU只在一帧数据全部传输完成时收到一次完成中断。举个例子一帧数据512字节用查询模式或者中断模式CPU要参与512次搬运用了DMACPU发出一次传输请求后就可以去干别的任务等DMA搬运完512字节再通知你。这个差别在高速率、大批量传输时非常明显。而且DMA还支持循环模式可以持续搬运数据流CPU只需要在合适的时间去取走数据就行。1.3 为什么不是UART、I2C或者CANUART虽然简单但标准波特率下带宽有限且两块板子之间如果要双向高速传数据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做主从通信时双方必须设成同样的模式否则收发时序对不上数据全是乱的。最常用的是模式0CPOL0时钟空闲为低CPHA0第一个边沿采样。我自己做双机通信时主从两边都固定用这个模式简单可靠。速率方面主机的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_Channel2SPI1_TX对应DMA1_Channel3。到了F407SPI1的收发请求在DMA2上RX对应Stream0、Channel3TX对应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);其实不只SPIUART、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_TxCpltCallback、HAL_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模式或手动置位SSI5. 几点实测体会当个收尾说点实在的。我每次做两块STM32的SPI通信都不会一上来就直接写DMA而是先用查询模式把链路验证一遍。查询模式代码简单主循环里调用HAL_SPI_Transmit和HAL_SPI_Receive确认硬件接线、SPI模式、CS时序都没问题再替换成DMA版本。这样一旦后面出问题排查范围就能缩小到DMA配置本身而不是硬件和软件混在一起能省一大半调试时间。DMA缓冲区用全局数组少用局部变量局部变量在栈上的地址存在不确定性而且生命周期结束后缓冲区内容会被覆盖。F4以上型号如果开了DCacheDMA写入内存后CPU可能读到Cache里的旧数据这时候需要在DMA接收完成后做Cache Invalidate或者直接配置MPU把DMA缓冲区设置为Non-cacheable区域。这个坑我在F407上踩过表现就是数据总是隔几帧跳变一次查了一下午才发现是Cache一致性问题。最后提一个协议层面的建议两个STM32之间传数据不要只发裸数据最好加一个简单帧头比如0xAA 0x55后面跟长度和数据接收方校验帧头后再解析。SPI没有应答机制线上一个毛刺就可能丢字节帧头校验能兜住大部分问题。我自己后来做板间通信都是统一用这套方法SPIDMA传输CS做帧同步帧头校验稳定跑了好几个项目都没再出过幺蛾子。本文还有配套的精品资源点击获取