STM32H743 SPI DMA双机通信从机端实战:从配置到避坑
简介:这份源码包面向已掌握STM32与SPI基础的嵌入式开发者,聚焦STM32H743从机端SPI DMA双机通信的完整实现,可用于工业控制、板间高速数据交互等场景的二次开发与学习参考。压缩包共1355个文件,约21.65MB,以599个c源文件与321个h头文件为主体,配合147个icf、113个s启动文件及21个sct分散加载文件,覆盖Keil、IAR等多套工程配置,另含数学与PDM滤波库、hex固件及说明文档,便于直接编译验证。资源围绕SPI接口初始化、DMA收发配置、传输完成回调、数据包封装校验与超时错误处理等环节展开,从机侧接收响应逻辑完整,可帮助读者理解无CPU干预的连续数据流传输机制,并对照手册排查时钟极性、相位与DMA地址映射等常见问题。目前已有330人学习下载,适合需要快速搭建双机通信验证环境的开发者参考。
1. STM32H743 SPI DMA 双机通信从机端:为什么值得单独啃
两块 STM32H743 之间用 SPI 做板间通信,主机好写,从机才是真正让人头大的那一端。主机想发就发,时钟由它掌控;从机只能被动等 NSS 拉低、等 SCK 边沿、在极短的窗口里把数据塞进 DR 或者从 DR 取走,稍慢一步就 OVR 溢出,读到的全是上一帧的残影。标题里的「SPI DMA 双机通信(从机)」要解决的就是这件事:让 H743 的 SPI 从机模式配合 DMA,把收发从 CPU 轮询里彻底解放出来,做到高吞吐、低抖动、不丢帧。
这套方案适合谁?做多轴运动控制板间总线、做采集前端与主控分离、做双核或多板协同的嵌入式工程师。H743 主频 480MHz、SPI 时钟可以跑到很高,但 SPI 从机模式下外设时钟不能超过内核时钟的一半,这个约束决定了你 DMA 配置的节奏。下面按「先立住原理、再动手复现、最后避坑」的顺序讲透,源码结构以 HAL 库 + CubeMX 生成的框架为基准,你可以直接对照自己的工程改。
2. SPI 从机 + DMA 的底层逻辑与 CubeMX 配置
2.1 从机模式下 SPI 的收发时序到底卡在哪
SPI 从机的核心矛盾是:数据到达和离开的时刻完全由主机时钟决定,而从机没有主动权。全双工时,主机每发一个字节,同时也在从 MISO 上收一个字节,从机必须在 SCK 的每个采样边沿前把要发的数据准备好,同时在移位寄存器满之前把收到的数据读走。用轮询,你就是在跟主机的时钟赛跑;用中断,每字节一次中断,高波特率下 CPU 直接被中断淹没。
DMA 的价值就在这里:SPI 的 RXNE 和 TXE 事件可以直接触发 DMA 请求,DMA 控制器替你把数据从 DR 搬到内存、从内存搬到 DR,CPU 只在整帧收发完成时被叫醒一次。H743 的 DMA1/DMA2 都支持 SPI 外设请求,SPI1~SPI6 的请求映射在不同 DMA 流上,选错流就是「配置全对但一个字节都不动」的经典翻车。
从机模式还有一个容易被忽略的点:NSS 的管理方式。硬件 NSS 由外部主机拉低,从机自动进入使能;软件 NSS 需要你在代码里手动控制 SSI 位。双机通信里主机通常用软件片选控制 GPIO,从机这边建议用硬件 NSS 输入模式,让外设自己判断帧边界,减少软件干预。
2.2 CubeMX 里从机 SPI 的关键参数怎么设
打开 CubeMX,选好 H743 的具体型号,进入 SPI 配置页。模式选 Full-Duplex Slave,这一项选错成 Master 后面全白搭。下面是几个必须盯死的参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Frame Format | Motorola | 标准 SPI,TI 模式少用 |
| Data Size | 8 Bits | 与主机一致,改 16 位要两边同步 |
| Clock Polarity (CPOL) | Low | 与主机严格一致 |
| Clock Phase (CPHA) | 1 Edge | 与主机严格一致 |
| NSS | Hardware NSS Input | 从机用硬件片选 |
| Baud Rate Prescaler | 不适用 | 从机时钟由主机给,此项无效 |
| First Bit | MSB First | 与主机一致 |
CPOL/CPHA 组合是双机通信第一大玄学来源。主机 CPOL=0、CPHA=0 意味着空闲低电平、第一个边沿采样;从机必须完全一致,否则你收到的数据会整体移位一位,看起来像「数据全错但偶尔对」。我一般会在示波器上先确认 SCK 空闲电平和第一个跳变沿,再回 CubeMX 对参数。
DMA 配置在 DMA Settings 标签页。给 SPI 的 RX 和 TX 各加一个 DMA 请求,方向分别是 Peripheral To Memory 和 Memory To Peripheral。模式选 Normal 还是 Circular 取决于你的协议:定长帧用 Normal,每帧重新启动;流式数据用 Circular,但要注意缓冲区翻转的同步问题。优先级建议 RX 高于 TX,因为接收溢出是不可恢复的,发送慢一点只是延迟。
NVIC 里把对应的 DMA 流中断打开,SPI 全局中断可以不开(DMA 模式下用不上)。生成代码后,HAL 会把MX_SPI1_Init和MX_DMA_Init都准备好,注意MX_DMA_Init必须在MX_SPI1_Init之前调用,否则 DMA 时钟没开,SPI 初始化里的 DMA 关联会失败。
3. 从机端 DMA 收发代码落地:从初始化到整帧回调
3.1 启动一次从机 DMA 收发的完整流程
从机不能主动发起传输,所以代码结构是「先挂起接收,等主机来时钟」。标准做法是用HAL_SPI_TransmitReceive_DMA同时挂起收发,让 DMA 在 NSS 拉低、时钟到来时自动搬运。下面是我常用的启动函数:
/* 从机端:挂起一次全双工 DMA 收发 * hspi: SPI 句柄 * tx_buf: 待发送数据(从机要回给主机的) * rx_buf: 接收缓冲(存主机发来的) * len: 本帧字节数,收发等长 */ HAL_StatusTypeDef SPI_Slave_StartTransfer(SPI_HandleTypeDef *hspi, uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len) { HAL_StatusTypeDef ret; /* 关键:先启动接收 DMA,再启动发送,避免第一个字节丢失 */ ret = HAL_SPI_TransmitReceive_DMA(hspi, tx_buf, rx_buf, len); if (ret != HAL_OK) { /* 记录错误码,常见为 HAL_BUSY:上一帧还没结束 */ Error_Handler(); } return ret; }逻辑说明:HAL_SPI_TransmitReceive_DMA内部会先使能 TX DMA 再使能 RX DMA,最后置 SPE 位启动 SPI。对从机来说,SPE 置位后外设就等 NSS 和时钟了。参数上len必须和主机约定的一致,从机不知道主机要发多少,只能靠协议固定帧长或者用 NSS 上升沿判断帧结束。如果帧长可变,就得改用 Circular 模式加空闲检测,复杂度上一个台阶,建议双机通信初期先固定帧长。
3.2 收发完成回调里该做什么、不该做什么
DMA 传输完成会触发HAL_SPI_TxRxCpltCallback,这是你处理整帧数据的唯一入口。注意这个回调在中断上下文里跑,不能放耗时操作:
/* 整帧收发完成回调,中断上下文 */ void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { /* 只做标记和缓冲区切换,解析放到主循环 */ frame_ready = 1; /* 双缓冲:切换到另一块 buffer 继续挂起接收 */ if (active_buf == 0) { SPI_Slave_StartTransfer(hspi, tx_buf1, rx_buf1, FRAME_LEN); active_buf = 1; } else { SPI_Slave_StartTransfer(hspi, tx_buf0, rx_buf0, FRAME_LEN); active_buf = 0; } } }逻辑说明:回调里立刻重新挂起下一帧,保证从机始终处于「等待接收」状态,这是不丢帧的关键。参数FRAME_LEN是协议约定的固定帧长。缓冲区切换用双缓冲,主循环处理rx_buf0时 DMA 在填rx_buf1,互不干扰。如果你在回调里直接解析协议、做浮点运算,下一帧的 DMA 启动就会被推迟,主机连续发帧时必然溢出。
3.3 错误回调与溢出恢复
从机最容易出的错是 OVR(Overrun),现象是HAL_SPI_ErrorCallback被调用,错误码HAL_SPI_ERROR_OVR。溢出后 SPI 外设的 DR 里是脏数据,必须清标志再重新挂起:
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { /* 清 OVR 标志:读 DR 再读 SR 的固定序列 */ __HAL_SPI_CLEAR_OVRFLAG(hspi); /* 中止当前 DMA,重新挂起 */ HAL_SPI_DMAStop(hspi); SPI_Slave_StartTransfer(hspi, tx_buf0, rx_buf0, FRAME_LEN); ovr_count++; /* 调试用计数器,正常应为 0 */ } }逻辑说明:__HAL_SPI_CLEAR_OVRFLAG是 HAL 提供的清溢出宏,内部执行读 DR、读 SR 的序列。HAL_SPI_DMAStop停掉可能还在跑的 DMA 流,避免状态机卡死。ovr_count在调试阶段非常有用,如果它持续增长,说明你的回调处理太慢或者主机发得太快,需要优化而不是掩盖。
4. 双机联调避坑:从机端 5 个血泪教训
4.1 现象:数据整体移位一位,偶尔全对
原因:CPOL/CPHA 与主机不一致,或者主机在 NSS 拉低后第一个时钟沿之前就发了数据。从机在 NSS 有效后的第一个采样沿才锁存,如果主机时序偏早,首位会丢。
解决:示波器同时抓 SCK 和 MOSI,确认 NSS 拉低到第一个 SCK 沿之间有足够建立时间。CubeMX 里 CPHA 选 1 Edge 还是 2 Edge 必须和主机逐位对齐,改完两边一起烧录验证。
4.2 现象:第一帧正常,后续帧全错或 OVR
原因:回调里没有及时重新挂起 DMA,或者重新挂起时上一帧的 DMA 流还没完全关闭,HAL_SPI_TransmitReceive_DMA返回 HAL_BUSY。
解决:在回调里先确认HAL_SPI_GetState回到 READY 再启动下一帧;如果频繁 BUSY,检查是不是在回调里做了串口打印这类阻塞操作。我一般会把重新挂起放在回调最前面,解析逻辑全部挪到主循环。
4.3 现象:DMA 配置全对但一个字节都不动
原因:DMA 流选错。H743 的 SPI1_RX 固定在 DMA2 Stream 0 或 Stream 2(取决于具体型号的请求映射表),SPI1_TX 在 DMA2 Stream 3 或 Stream 5。CubeMX 一般会自动分配,但手动改过之后容易错位。
解决:对照参考手册的 DMA 请求映射表逐项核对,或者干脆让 CubeMX 重新生成 DMA 配置。另一个隐蔽原因是MX_DMA_Init调用顺序在MX_SPI1_Init之后,DMA 时钟没开。
4.4 现象:低速正常,波特率一高就丢帧
原因:从机 SPI 外设时钟超过内核时钟一半,或者 DMA 传输带宽被其他外设抢占。H743 的 SPI 从机模式有明确时钟上限,超了就是随机丢。
解决:查数据手册确认 SPI 内核时钟源和分频,从机模式下SPI_ker_ck不能超过spi_ker_ck上限的一半。同时检查 DMA 仲裁优先级,把 SPI 的 DMA 流优先级设为 Very High,避免被 LCD、SDMMC 这类大带宽外设挤掉。
4.5 现象:NSS 用软件模式,帧边界判断错乱
原因:软件 NSS 下从机靠 SSI 位手动控制,主机片选拉低拉高的时刻和从机 SSI 操作不同步,导致多收或少收字节。
解决:从机一律用硬件 NSS 输入模式,让外设自己根据 NSS 引脚电平判断帧起止。如果 NSS 引脚被其他功能占用,换一个 SPI 实例,别在软件 NSS 上硬扛。
5. 用 DMA 双缓冲 + 空闲检测把可变长帧也吃下来
固定帧长能解决大部分双机通信场景,但真实协议里经常有「命令帧短、数据帧长」的混合情况。这时候纯 Normal 模式就不够了,我的做法是 Circular DMA 加 SPI 空闲检测(IDLE)配合双缓冲,让从机既能收定长也能收变长。
思路是这样:RX 用 Circular 模式持续搬运到一块大缓冲,TX 也用 Circular 但发送内容由协议层动态填充。利用 H743 SPI 的 TSIZE 和 DMA 的 NDTR 寄存器,在 NSS 上升沿触发的外部中断里读取当前 NDTR,算出这一帧实际收了多少字节。下面是一个简化实现:
/* NSS 上升沿外部中断:帧结束,计算本帧长度 */ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == NSS_PIN) { /* NDTR 是剩余待搬运数,总数减剩余就是已收字节 */ uint16_t received = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_spi1_rx); /* 记录本帧长度,主循环按长度解析 */ last_frame_len = received; frame_ready = 1; /* 重置 DMA 计数器,准备下一帧 */ __HAL_DMA_DISABLE(&hdma_spi1_rx); hdma_spi1_rx.Instance->NDTR = RX_BUF_SIZE; __HAL_DMA_ENABLE(&hdma_spi1_rx); } }逻辑说明:__HAL_DMA_GET_COUNTER返回 NDTR 当前值,即还没搬的字节数,总数减它得到已收长度。NSS 上升沿表示主机结束本帧,此时读 NDTR 最准。重置 NDTR 前要先 disable DMA,否则写 NDTR 可能不生效。参数RX_BUF_SIZE要大于协议里最长帧,留 20% 余量。
验证方法:主机端发不同长度的帧,从机打印last_frame_len,和主机发送长度逐一比对。如果长度总是偏大,检查 NSS 上升沿中断有没有被其他中断延迟;如果偏小,检查 Circular 模式下 DMA 有没有在帧中间回绕。这个方案比固定帧长复杂,但一旦跑通,协议灵活性提升明显。
我自己的习惯是:双机通信第一版永远先用固定帧长把链路跑通,确认 DMA、NSS、CPOL/CPHA 全部无误后,再上可变长。跳过第一步直接搞可变长,出问题时你分不清是时序错还是长度算错,排查成本翻倍。希望帮到你。
本文还有配套的精品资源,点击获取