ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32H7 SPI+DMA Busy状态卡死根因与EOT中断解决方案

STM32H7 SPI+DMA Busy状态卡死根因与EOT中断解决方案 1. 问题现象与背景定位1.1 一个让人抓狂的现场SPI 总线上挂着一颗 ADS8681STM32H7 作为主机用 SPI DMA 的方式读取采样数据。代码烧进去第一次传输正常第二次开始HAL_SPI_GetState()一直返回HAL_SPI_STATE_BUSY_TX_RX或者干脆卡在while(__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY))这个死循环里出不来。示波器抓 SCK 和 CS发现 CS 拉低之后 SCK 只跑了几个周期就停了DMA 的传输完成中断倒是进了但 SPI 的 BSY 标志位死活不归零。这个现象在 STM32H7 上尤其常见原因跟 H7 系列 SPI 外设的架构改动有直接关系。F1/F4 时代那套 SPI 配置逻辑直接搬到 H7 上大概率会踩坑。我前后在 H743 和 H750 上各调过一轮把这个问题彻底理清楚了下面把根因、排查路径和最终方案完整拆开讲。1.2 为什么 H7 的 SPI 更容易出这个问题STM32H7 的 SPI 外设相比 F4 有几个关键变化这些变化直接导致了 Busy 状态持续第一H7 的 SPI 支持更大的 FIFO 深度16 字节并且引入了 TSIZE传输数据量寄存器。DMA 请求的触发时机和 FIFO 水位强相关如果 FIFO 阈值配置不当DMA 会在 SPI 还没真正把数据发完的时候就报告传输完成。第二H7 的 SPI 时钟域更复杂SPI1/2/3 挂在 APB2 上SPI4/5/6 挂在 APB1 上内核时钟和外设时钟的比值会影响 BSY 标志的清除时序。如果 DMA 中断优先级配置得比 SPI 中断高DMA 完成中断里立刻去操作 SPI此时 SPI 的最后一个字节可能还在移位寄存器里没发完BSY 自然不归零。第三H7 的 DMA 控制器是 DMAMUX 架构请求映射和 F4 的固定通道完全不同。如果 DMAMUX 的请求配置有误DMA 可能提前触发完成中断而 SPI 侧的传输根本没结束。核心结论H7 上 SPIDMA 的 Busy 状态持续绝大多数情况不是 SPI 本身坏了而是 DMA 完成中断的触发时机和 SPI 实际传输结束之间存在时间差代码在这个时间差里做了不该做的事。1.3 这个问题的影响范围别以为这只是 ADS8681 或者某一颗芯片的问题。只要你在 H7 上用 SPIDMA并且满足以下任一条件就可能踩到DMA 配置为 Normal 模式而非 Circular 模式在 DMA 传输完成回调里立即发起下一次 SPI 传输使用了HAL_SPI_TransmitReceive_DMA()且没有正确处理HAL_SPI_TX_RX_CpltCallbackSPI 的 NSS 采用软件控制且 CS 拉高时机由 DMA 完成中断决定中断优先级分组设置中DMA 中断优先级高于 SPI 中断我见过最典型的场景是用 SPIDMA 驱动 ADS8681、ADS127L11 这类高精度 ADC采样率要求高代码在 DMA 完成中断里直接拉 CS 并启动下一次转换。结果就是 CS 拉高时最后一个字节还没发完ADC 认为命令无效下一次读取全是 0 或者 0xFFFF。2. 根因拆解DMA 完成不等于 SPI 传输结束2.1 DMA 完成中断到底意味着什么很多人有一个直觉性的误解DMA 传输完成中断触发了就说明 SPI 已经把数据全部发出去了。这个理解在 F1/F4 上勉强能用在 H7 上完全不成立。DMA 的传输完成中断准确含义是DMA 控制器已经把指定数量的数据从源地址搬运到了 SPI 的 DR数据寄存器或者从 DR 搬运到了目标地址。注意是“搬运到 DR”不是“SPI 已经把这个字节移位输出到 MOSI 引脚上”。H7 的 SPI 有一个 16 字节的 TX FIFO。DMA 把数据写进 FIFO 就算完成搬运但 FIFO 里的数据还需要 SPI 的移位逻辑逐个搬到移位寄存器再从 MOSI 引脚一位一位发出去。当 DMA 报告完成时FIFO 里可能还有 8 个字节没发移位寄存器里还有 1 个字节正在发。这就是 Busy 状态持续的根源DMA 说“我搬完了”SPI 说“我还没发完”代码听信了 DMA 的话去读状态或者启动下一次传输SPI 自然还是 Busy。2.2 BSY 标志位的清除条件STM32H7 参考手册里对 SPI 的 BSY 标志有明确定义BSY 在 SPI 开始传输时置 1在传输结束、最后一个字节的最后一个时钟边沿之后清除。关键在于“最后一个时钟边沿之后”这意味着 BSY 的清除和 SCK 的停止是同步的。如果你在 DMA 完成中断里立刻去查 BSY此时 SCK 可能还在跑BSY 当然是 1。更糟糕的是如果你在 BSY 为 1 的时候去写 SPI 的 CR1 寄存器修改使能位或者配置位H7 的 SPI 会直接忽略这次写入甚至可能进入错误状态。我实测过一组数据SPI 时钟配置为 10 MHzDMA 搬运 16 个字节从 DMA 完成中断触发到 BSY 真正归零中间有大约 12.8 微秒的延迟。这 12.8 微秒就是 FIFO 里剩余字节的发送时间。如果你的代码在这 12.8 微秒内做了任何 SPI 操作必出问题。2.3 中断优先级配置的陷阱H7 的中断优先级分组和 F4 类似但 NVIC 的响应行为有细微差别。假设你配置了DMA 传输完成中断优先级抢占优先级 1SPI 全局中断优先级抢占优先级 2DMA 完成中断触发后会抢占 SPI 中断。如果 SPI 中断里正在处理 TXE 或者 RXNE被 DMA 中断打断而 DMA 中断里又去操作 SPI 的 CS 引脚或者启动新传输就会造成状态机混乱。更隐蔽的问题是HAL 库的HAL_SPI_TransmitReceive_DMA()内部会开启 SPI 的 ERR 中断和 TXE/RXNE 中断取决于配置。如果 DMA 完成中断优先级更高DMA 中断里调用了HAL_SPI_DMAStop()这个函数会关闭 SPI 的 DMA 请求并使能位但此时 SPI 的移位寄存器可能还在工作BSY 保持为 1后续再调用HAL_SPI_TransmitReceive_DMA()就会返回HAL_BUSY。2.4 软件 NSS 与硬件 NSS 的差异用软件 NSS 时CS 引脚由 GPIO 控制拉高拉低的时机完全由代码决定。很多代码这样写void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 拉高 CS // 启动下一次传输 HAL_SPI_TransmitReceive_DMA(hspi, tx, rx, len); }这段代码的问题在于回调触发时SPI 的最后一个字节可能还在发送中CS 提前拉高会导致从机认为传输被中止。对于 ADS8681 这类对 CS 时序敏感的芯片直接表现为数据错误或者 Busy 状态持续。用硬件 NSS 时SPI 外设自动控制 NSS 引脚但 H7 的硬件 NSS 在 DMA 模式下有额外的时序要求。如果 NSS 脉冲模式配置不当NSS 会在 DMA 完成时就拉高同样早于实际传输结束。3. 完整解决方案与实操配置3.1 方案选型三种可行路径对比针对这个问题有三条路可以走各有适用场景方案核心思路优点缺点适用场景方案ADMA完成中断BSY轮询DMA完成中断里等待BSY归零再操作改动小逻辑直观中断里轮询实时性差低速SPI非实时系统方案BSPI传输完成中断用SPI的EOT中断替代DMA完成中断时机准确不阻塞需要配置EOT使能H7专属推荐方案CDMA双缓冲TSIZE利用H7的TSIZE和双缓冲机制效率最高适合高速配置复杂高速连续采样我最终在ADS8681的项目里用的是方案B下面重点讲这个。方案A作为备选也会给出代码。3.2 方案B使用SPI的EOT中断推荐H7 的 SPI 有一个 EOTEnd of Transfer中断在 SPI 真正传输结束时触发此时 BSY 已经归零或者即将归零。这个中断比 DMA 完成中断晚但时机准确。配置步骤第一步在 CubeMX 里使能 SPI 的 EOT 中断。注意CubeMX 默认不开启这个中断需要手动在代码里设置// SPI初始化后使能EOT中断 __HAL_SPI_ENABLE_IT(hspi1, SPI_IT_EOT);第二步配置 DMA 为 Normal 模式不要在 DMA 完成回调里做任何 SPI 操作void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { // 这里只做标记不做SPI操作 spi_dma_done_flag 1; }第三步在 SPI 的 EOT 中断处理里做真正的收尾工作void SPI1_IRQHandler(void) { if (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_EOT)) { __HAL_SPI_CLEAR_FLAG(hspi1, SPI_FLAG_EOT); // 此时BSY已归零可以安全操作CS和启动下一次传输 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 启动下一次传输 HAL_SPI_TransmitReceive_DMA(hspi1, tx_buf, rx_buf, len); } HAL_SPI_IRQHandler(hspi1); }关键点EOT 中断触发时SPI 的 BSY 标志已经清除此时拉高 CS 是安全的启动下一次传输也不会返回 BUSY。3.3 方案ADMA完成中断BSY轮询备选如果不想改中断配置可以在 DMA 完成回调里加一个带超时的 BSY 轮询void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { uint32_t timeout 10000; // 超时计数 while (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_BSY)) { if (--timeout 0) { // 超时处理记录错误 break; } } HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); HAL_SPI_TransmitReceive_DMA(hspi, tx_buf, rx_buf, len); }这个方案的缺点是中断里轮询会阻塞其他中断如果 SPI 时钟很慢比如 1 MHz16 个字节的发送时间可能达到 128 微秒中断里等这么久是不可接受的。只适合 SPI 时钟较高10 MHz且系统实时性要求不高的场景。3.4 关键参数计算与配置SPI 时钟配置直接影响 BSY 的持续时间。以 ADS8681 为例它支持最高 20 MHz 的 SCLK。H7 的 SPI1 挂在 APB2 上假设 APB2 时钟为 100 MHz要得到 10 MHz 的 SCKSPI 时钟分频 APB2时钟 / 目标SCK 100MHz / 10MHz 10H7 的 SPI 波特率分频器只支持 2、4、8、16、32、64、128、256 这几档10 不是合法值所以只能选 8 或者 16。选 8 得到 12.5 MHz选 16 得到 6.25 MHz。如果 ADS8681 能跑 12.5 MHz就选 8。DMA 传输长度配置ADS8681 是 16 位数据SPI 配置为 16 位数据宽度DMA 的传输长度设为采样点数。注意 H7 的 SPI 数据宽度和 DMA 传输宽度要匹配// SPI配置为16位数据宽度 hspi1.Init.DataSize SPI_DATASIZE_16BIT; // DMA配置为半字16位传输 hdma_spi1_rx.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_spi1_rx.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD;如果这里配错了比如 SPI 是 16 位但 DMA 是字节对齐DMA 完成中断会在传输一半的时候就触发BSY 问题会更严重。3.5 中断优先级配置实操在 CubeMX 的 NVIC 配置里按以下优先级设置中断源抢占优先级子优先级说明SPI1全局中断10包含EOT中断优先级最高DMA SPI1 RX20接收DMA完成DMA SPI1 TX21发送DMA完成其他外设3-低于SPI相关这样配置的理由EOT 中断必须在 DMA 完成中断之后触发但如果 DMA 中断优先级更高EOT 中断可能被延迟处理。把 SPI 全局中断设为最高确保 EOT 能被及时响应。注意H7 的中断优先级分组必须统一设置通常在HAL_Init()里调用HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)4 位全部用于抢占优先级子优先级为 0。这样上面表格里的子优先级设置才有意义。4. 常见问题排查与避坑指南4.1 问题速查表现象可能原因排查方法解决方案BSY一直为1DMA完成中断里操作了SPI在DMA回调里打断点查BSY改用EOT中断或加BSY轮询第二次传输返回BUSY第一次传输未真正结束查HAL_SPI_GetState返回值确保EOT中断已使能数据错位CS拉高时机早于传输结束示波器抓CS和SCK在EOT中断里拉高CSDMA完成中断不触发DMAMUX请求映射错误查DMAMUX配置寄存器重新配置请求映射传输一半就完成DMA数据宽度与SPI不匹配查DMA的DataAlignment统一为半字或字EOT中断不触发未使能EOT中断查SPI的IER寄存器调用__HAL_SPI_ENABLE_IT4.2 踩过的坑DMAMUX配置H7 的 DMA 请求通过 DMAMUX 路由和 F4 的固定通道映射完全不同。CubeMX 通常会自动配置但如果你手动改过 DMA 配置很容易把请求映射搞错。以 SPI1_RX 为例在 H7 上它对应的 DMAMUX 请求编号是 37具体编号查参考手册的 DMAMUX 请求映射表。如果这个编号配错DMA 根本不会触发或者触发的是别的外设的请求。排查方法在 DMA 完成中断里翻转一个 GPIO用示波器看有没有波形。如果没有先查 DMAMUX 配置。4.3 踩过的坑FIFO阈值配置H7 的 SPI FIFO 有一个阈值配置位决定了 DMA 请求的触发时机。默认情况下TX FIFO 阈值是 0意味着 FIFO 空的时候就触发 DMA 请求。这个配置在高速传输时会导致 DMA 频繁触发但在低速时反而容易造成 DMA 提前完成。我建议把 TX FIFO 阈值设为 FIFO 深度的一半8 字节这样 DMA 一次搬运 8 个字节减少中断次数同时保证 FIFO 不会溢出// 设置TX FIFO阈值为8字节 MODIFY_REG(hspi1.Instance-CFG1, SPI_CFG1_TXFTHRESH, (8 SPI_CFG1_TXFTHRESH_Pos));RX FIFO 阈值类似设为 8 字节确保接收 DMA 在 FIFO 有足够数据时才触发。4.4 踩过的坑FreeRTOS下的中断优先级如果在 FreeRTOS 下使用 SPIDMA中断优先级必须符合 FreeRTOS 的约束。FreeRTOS 的configMAX_SYSCALL_INTERRUPT_PRIORITY通常设为 5意味着优先级高于 5 的中断不能调用 FreeRTOS 的 API。SPI 的 EOT 中断如果优先级设为 1就不能在中断里调用xSemaphoreGiveFromISR()。解决方案是把 EOT 中断优先级设为 6 或更低或者用二值信号量在任务里处理后续逻辑。我个人的做法是EOT 中断里只做最紧急的事拉 CS、启动下一次 DMA把数据处理放到任务里通过信号量同步。这样中断优先级可以设低一些不影响 FreeRTOS 的调度。4.5 一个容易被忽略的细节SPI使能顺序H7 的 SPI 使能顺序有讲究。正确的顺序是配置 SPI 的 CR1、CR2、CFG1、CFG2 寄存器使能 DMA 的 SPI 请求使能 SPI 的 EOT 中断最后使能 SPI设置 CR1 的 SPE 位如果先使能 SPI 再配置 DMASPI 可能已经开始工作但 DMA 还没准备好导致第一个字节丢失或者 BSY 状态异常。HAL 库的HAL_SPI_TransmitReceive_DMA()内部会处理这个顺序但如果你手动操作寄存器一定要按上面的顺序来。5. 实测验证与性能数据5.1 验证方法我用逻辑分析仪抓了四路信号CS、SCK、MOSI、MISO。触发条件设为 CS 下降沿采样率 100 MHz。重点看三个时间点T1DMA 完成中断触发时刻T2EOT 中断触发时刻T3BSY 标志归零时刻实测数据SPI 时钟 12.5 MHz传输 16 个 16 位数据时间点相对CS下降沿的延迟说明T118.5 微秒DMA搬完32字节T220.8 微秒SPI真正发完最后一个字节T320.8 微秒BSY归零与T2同步T1 和 T2 之间差了 2.3 微秒这就是 FIFO 里剩余字节的发送时间。如果代码在 T1 时刻操作 SPI必然出问题。5.2 方案B的性能表现改用 EOT 中断后连续传输 1000 次无一次 Busy 错误。单次传输耗时从原来的 25 微秒含轮询等待降到 21 微秒效率提升约 16%。CPU 占用率方面DMA 完成中断和 EOT 中断各触发一次中断服务程序执行时间约 1.2 微秒对系统整体影响很小。5.3 方案C的扩展思路如果采样率要求更高比如 ADS127L11 这种 1 MSPS 以上的 ADC方案B 的中断开销会成为瓶颈。这时候可以考虑方案C用 DMA 双缓冲 SPI 的 TSIZE 自动重载。H7 的 SPI 支持 TSIZE 寄存器可以设置每次传输的数据量传输完成后自动重载配合 DMA 双缓冲实现连续无间断传输。这个配置比较复杂涉及 DMAMUX 的双缓冲模式和 SPI 的 TSIZE 联动后续可以单独写一篇。6. 代码模板与快速复现6.1 CubeMX配置清单按以下配置生成代码SPI1全双工主模式16位数据CPOLLowCPHA1EdgeNSS软件控制预分频8DMASPI1_RX和SPI1_TX各一个通道Normal模式半字对齐优先级MediumNVICSPI1全局中断使能优先级1DMA中断使能优先级2GPIOCS引脚配置为输出初始高电平6.2 核心代码// spi_dma.h extern SPI_HandleTypeDef hspi1; extern DMA_HandleTypeDef hdma_spi1_rx; extern DMA_HandleTypeDef hdma_spi1_tx; #define ADC_CS_Pin GPIO_PIN_4 #define ADC_CS_GPIO_Port GPIOA void SPI_DMA_Init(void); void SPI_DMA_StartTransfer(uint16_t *tx, uint16_t *rx, uint16_t len); // spi_dma.c volatile uint8_t spi_eot_flag 0; void SPI_DMA_Init(void) { // 使能EOT中断 __HAL_SPI_ENABLE_IT(hspi1, SPI_IT_EOT); // 设置FIFO阈值 MODIFY_REG(hspi1.Instance-CFG1, SPI_CFG1_TXFTHRESH, (8 SPI_CFG1_TXFTHRESH_Pos)); MODIFY_REG(hspi1.Instance-CFG1, SPI_CFG1_RXFTHRESH, (8 SPI_CFG1_RXFTHRESH_Pos)); } void SPI_DMA_StartTransfer(uint16_t *tx, uint16_t *rx, uint16_t len) { HAL_GPIO_WritePin(ADC_CS_GPIO_Port, ADC_CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive_DMA(hspi1, (uint8_t*)tx, (uint8_t*)rx, len); } // 在stm32h7xx_it.c里添加 void SPI1_IRQHandler(void) { if (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_EOT)) { __HAL_SPI_CLEAR_FLAG(hspi1, SPI_FLAG_EOT); HAL_GPIO_WritePin(ADC_CS_GPIO_Port, ADC_CS_Pin, GPIO_PIN_SET); spi_eot_flag 1; } HAL_SPI_IRQHandler(hspi1); }6.3 使用示例uint16_t tx_buf[16] {0x0000}; uint16_t rx_buf[16] {0}; SPI_DMA_Init(); while (1) { SPI_DMA_StartTransfer(tx_buf, rx_buf, 16); // 等待EOT while (!spi_eot_flag); spi_eot_flag 0; // 处理rx_buf数据 process_adc_data(rx_buf, 16); HAL_Delay(1); }这个模板可以直接复用到 ADS8681、ADS127L11、MT6701 等 SPI 接口的芯片上。唯一需要改的是 SPI 的时钟分频和数据宽度根据具体芯片的手册调整。6.4 最后分享一个调试技巧如果你不确定 BSY 问题出在哪个环节可以在 DMA 完成中断和 EOT 中断里各翻转一个空闲 GPIO用示波器同时抓 CS、SCK、这两个 GPIO。如果 DMA 完成 GPIO 翻转时 SCK 还在跑就说明问题确实出在 DMA 完成过早。这个方法比看代码快得多我每次调新板子都这么干。另外H7 的 SPI 有一个容易忽略的 ERR 中断如果传输过程中出现模式错误或者溢出ERR 中断会触发但 HAL 库默认不处理。建议在 SPI 中断里加上 ERR 标志的检查和清除否则一次错误可能导致后续所有传输都返回 BUSY。
返回列表