ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32H5 GPDMA踩坑实录:非活动通道Abort导致SUSP残留卡死SPI DMA

STM32H5 GPDMA踩坑实录:非活动通道Abort导致SUSP残留卡死SPI DMA 先说结论这个问题的本质不是SPI外设配置错了而是STM32H562的GPDMA在“通道根本没有进入活动状态时执行abort”会把CxCR.SUSP位残留为1。这个残留位会卡死整个GPDMA通道的后续传输而且用常规的“重新使能通道”方式救不回来最后只能复位整颗芯片或者复位GPDMA外设。我是在做一个SPI从机接收功能时踩到这个坑的。MCU用的STM32H562SPI1从机接收开了GPDMA通道搬运数据。产线测试时发现板子偶发通信卡死复位后恢复。最开始怀疑是SPI配置问题或者片选时序问题查了大半天最后定位到GPDMA的CxCR寄存器上。这篇把整个排查过程、寄存器的状态变化、以及最终怎么绕过这个硬件坑讲清楚给用STM32H5系列做SPI DMA的朋友一个参考。1. GPDMA和SPI的搭配先理清架构再谈踩坑1.1 STM32H5的DMA架构和F4/H7有什么区别用过STM32F4的朋友应该对DMA1/DMA2很熟8个流、FIFO、几个外设共用通道每次配置都要查一遍请求映射表。STM32H562不一样它用的GPDMA是ST新一代的DMA控制器设计思路更接近LPDMA或者SoC里的通用DMA引擎。GPDMA有8个独立通道没有传统的FIFO缓冲区概念所有传输描述通过一组寄存器配置还支持linked-list链表模式可以一条一条自动执行多个传输任务。它的请求来源也不是固定的“外设请求线编号”而是通过请求映射选择每一个外设的request灵活度更高但出问题时排查思路也得跟着变。H5上SPI1的RX请求、TX请求各自对应一条GPDMA通道你可以把SPI的接收映射到GPDMA通道0把发送映射到通道1。配置时需要在CTR1寄存器里设置Request Select在CTR2里设置传输方向、数据宽度、地址增量等。第一次从H7迁到H5的工程时不少人会惯性思维去找“DMA_Stream”和“FIFO”相关的寄存器结果发现全部找不到这就是架构换了。1.2 GPDMA核心寄存器速览排查这个故障时我最重要的工具就是GPDMA的寄存器dump。核心要盯这几个CxCRChannel x Control Register偏移0x08通道控制寄存器EN、ABORT、SUSP、HALTE这些关键控制位都在这里。CxSRChannel x Status Register偏移0x10中断状态和通道状态像TCIF、HTIF、CAIF这些标志位。CxTR1/CxTR2Transfer Configuration Registers配置传输大小、地址、数据宽度、请求源等。CxBR1块传输相关配置。这里面的SUSP位就是坑的根源。数据手册写的很简单软件写1请求挂起通道当通道从挂起状态恢复时硬件会自动清零。听起来像一个普通的暂停功能实际在异常abort场景下会出问题。1.3 SPI DMA的请求过程SPI使用DMA传输时数据流大致是SPI外设收到或发送一个字节产生事件请求GPDMA检测到请求后从源地址读一个数据单元写入目的地址传输完成后再等下一个事件请求。这个流程靠着GPDMA通道内部的握手信号维持。问题场景通常出现在SPI从机接收。比如你和某个主机通信主机没有严格的连续时钟而是断断续续发几个字节。如果你在GPDMA通道上配置了一个很大的传输长度比如64字节而主机实际只发了8个字节那么传输就一直不会完成。这时候你一般会加一个超时逻辑超时后调用abort来中止这次DMA传输准备接收下一包。我踩坑的操作恰恰就发生在这超时调用abort后再启动下一次SPI DMA接收通道不动了。2. 故障现场寄存器状态和复现路径2.1 怎么稳定复现我们把故障复现条件总结成一句话SPI从机在没有任何有效通信或通道未激活的情况下软件调用了DMA abort随后立刻重新启动SPI DMA接收。具体复现步骤SPI1配置为从机模式使用GPDMA通道0作为接收通道传输长度配置为64字节。软件启动HAL_SPI_Receive_DMA。主机不发送任何数据或者只发了几字节就停住。软件超时后调用HAL_SPI_DMAStop这个函数内部会对GPDMA调用abort。软件再次调用HAL_SPI_Receive_DMA启动下一轮接收。观察SPI外设状态SPI本身正常但是GPDMA通道没有搬运任何数据。如果是全新的空通道从头到尾没有使能过直接调abort再启动不一定每次都卡。最容易触发的场景是通道曾经跑过一次传输当前处于IDLE状态或者挂起状态你在这个状态下执行一次abort。因为硬件状态机残留了一些状态第二次abort才真正把SUSP锁死。2.2 寄存器现场抓取我在卡住之后用调试器把GPDMA寄存器全读出来关键值如下以通道0为例GPDMA_C0CR 0x00000004GPDMA_C0SR 0x00000000GPDMA_C0TR1 0x00000040传输长度64和配置一致GPDMA_C0TR2 0x00000001RX方向GPDMA_C0BR1 0x00000000GPDMA_C0DAR 目标地址正常0x00000004就是CCR的bit2也就是SUSP位。这时候ENbit0是0ABORTbit1是0HALTEbit3是0SUSP反而成了唯一置1的位。这就是最诡异的地方通道没有被使能EN0也没有abort执行中ABORT0但是SUSP1。这个组合本来不应该出现因为SUSP是软件挂起请求位正常逻辑是EN1的通道才有意义去置SUSP。2.3 初步判断是不是SPI外设问题出问题时我第一反应是SPI外设挂死。于是做了几个对照实验直接操作SPI寄存器收发数据不经过DMA——SPI外设收发正常。把GPDMA换成另一个空闲通道比如通道3重新配置成一样的参数SPI DMA接收立刻恢复正常。把GPDMA通道0重新初始化写0清除寄存器再次配置传输仍然卡死。这三条实验合在一起基本排除了SPI外设的问题确定卡死的位置在GPDMA通道0本身。通道0像一个上了锁的门SPI的DMA请求进来了但是门没有打开数据根本没有搬运。实验3有一点值得注意即使把整条通道重新初始化只要RCC没有复位GPDMA外设这个SUSP位似乎会“记住”之前的错误状态。我在代码里做了一整套通道deinit流程包括清中断、清通道配置寄存器、重新初始化TR1和TR2最后再置EN1结果通道仍然不工作。3. 根因分析非活动通道上的abort如何留下SUSP3.1 GPDMA通道状态机看RM0481STM32H562参考手册第9章的GPDMA状态机通道的内部状态大致是IDLE → CONFIG → READY → RUNNING → BLOCKED然后回到READY或者IDLE。正常流程中abort命令发送后状态机应该从当前状态跳转到终止状态清掉所有控制位包括SUSP。关键是这个正常清理流程依赖“当前存在一个正在进行的传输”或者“CHANNEL被正确enable并完成一次总线握手”。如果在状态机处于IDLE非活动时你给ABORT位写1硬件并没有一个明确的“中止一个不存在的传输”动作。它可能进入了某种中间状态并且错误地把SUSP作为挂起证据置位了。由于没有实际的DMA传输来触发传输完成事件SUSP位永远不会被硬件清零。这有点像你去关一个已经关闭的阀门阀门没有打开过但操作机构卡在了半开位置。下次再打开时阀门的联动机构认为处于关闭中拒绝执行打开命令。3.2 abort调用时的软件时序竞态还有一种常见触发原因需要特别注意HAL库的abort函数和中断回调之间存在竞态。调用HAL_SPI_DMAStop时HAL会调用HAL_DMA_Abort在HAL_DMA_Abort内部它会先清DMA中断使能然后检查通道状态最后写ABORT位。如果此时通道刚刚完成一次传输TCIF置位但软件还没来得及清除abort又进来了通道控制逻辑就可能出现SUSP残留。更典型的场景是上一次DMA传输已经正常完成通道处于IDLE你收到TCIF回调后没有清标志没有做通道复位直接在回调外面调用了一次abort。这个时候状态机的状态和“非活动通道”几乎一样最容易捅出SUSP残留。3.3 为什么新的传输会被“永久阻塞”你可能会想重新配置TR1、TR2重新写EN1通道应该能重新跑起来吧GPDMA硬件对SUSP的处理逻辑是当通道使能时如果SUSP位为1硬件不会启动新的传输请求处理。你可以理解为SUSP位在通道控制逻辑里拥有比EN更高的优先级它把通道判定为“软件要求挂起”。即使EN重新置1状态机仍然认为通道处于挂起不会对外设的请求做响应。我在调试中验证过在卡死状态下直接写EN1观察C0SRTCIF和通道状态位没有任何变化。然后我又试着把SUSP位写0通道就恢复了。所以修复的关键就是想办法把SUSP清零而并不是重新配置传输参数。但是这里有一个坑CxCR的SUSP位在某些错误场景下直接写0清不掉或者写0的动作被硬件忽略了。我实际测试发现如果只是执行一次C0CR ~(1 2)寄存器值仍然保持0x04SUSP纹丝不动。必须要做“先写1再写0”或者“先触发一个挂起流程再恢复”的操作才可能把它解掉。4. 修复方案三个能落地的做法4.1 方案一abort之后安全清除SUSP位这是最直接、侵入性最小的方案我在实际项目中最后采用的就是这个。核心思想是每次执行abort之后不要立刻返回先写一次SUSP位再主动清掉确保通道状态干净。代码示例如下C语言// 假定传入gpdma通道基地址 static void gpdma_abort_safe(GPDMA_Channel_TypeDef *ch) { uint32_t ccr; uint32_t csr; // 先正常发起abort ch-CCR | GPDMA_CCR_ABORT; // 等待abort完成TCIF或者通道释放EN uint32_t timeout 10000; while ((ch-CCR GPDMA_CCR_EN) timeout--) { csr ch-CSR; if (csr (GPDMA_CSR_TCIF | GPDMA_CSR_CAIF)) { break; } } // 清掉中断标志 ch-CSR | GPDMA_CSR_TCIF | GPDMA_CSR_HTIF | GPDMA_CSR_CAIF | GPDMA_CSR_CFEI; // 检查是否有SUSP残留 ccr ch-CCR; if (ccr GPDMA_CCR_SUSP) { // 先写1再写0强制状态机走一次挂起/恢复流程 ch-CCR | GPDMA_CCR_SUSP; __DSB(); ch-CCR ~GPDMA_CCR_SUSP; __DSB(); // 再清一次标志确保中间状态没有产生额外事件 ch-CSR | GPDMA_CSR_TCIF | GPDMA_CSR_CAIF; } }这个函数的意义在于它覆盖了“非活动通道abort”和“正常活动通道abort”两种情况。在正常通道上ABORT之后SUSP本来就是0走if分支不会误触发。在异常通道上通过写1再写0的方式强制清除SUSP残留。实测效果我给产线固件加上这个函数后连续跑几千次短帧收发、随机abort压力测试没有再出现通道卡死。之前基本几十次内必现。4.2 方案二先保证通道进入活动状态再abort既然问题频繁出现在“非活动通道”上abort那换个思路调用abort之前先确保通道确实在传输中或者先通过软件发起一次有效的挂起再abort。具体代码上可以这样处理static void spi_dma_stop_safe(SPI_HandleTypeDef *hspi, GPDMA_Channel_TypeDef *ch) { uint32_t ccr ch-CCR; // 如果EN为0直接认为通道已停止不做abort避免触发异常路径 if (!(ccr GPDMA_CCR_EN)) { // 但还需要处理可能残留的SUSP if (ccr GPDMA_CCR_SUSP) { ch-CCR ~GPDMA_CCR_SUSP; __DSB(); } return; } // 通道使能中正常abort ch-CCR | GPDMA_CCR_ABORT; uint32_t timeout 10000; while ((ch-CCR GPDMA_CCR_EN) timeout--) { if (ch-CSR (GPDMA_CSR_TCIF | GPDMA_CSR_CAIF)) { break; } } }这个方案的原理是不在通道非活动状态乱触发abort从源头切断SUSP残留的可能。但是在某些场景下比如你确实不确定通道当前是什么状态或者你想拿abort作为“无论什么状态都停止DMA”的兜底逻辑这个方案就不够用。因为你无法保证调用之前通道一定是EN1。所以我把这个方案作为一个辅助保护而不是唯一修复手段。项目里两个函数都保留一个负责清SUSP一个负责跳过非活动abort。4.3 方案三直接复位整个GPDMA外设如果你不在乎其他通道被一起复位这个是暴力但有效的方案。通过RCC寄存器强制复位GPDMA然后重新初始化所有使用到的通道。static void gpdma_force_reset(void) { // STM32H562上GPDMA挂在AHB总线上 __HAL_RCC_GPDMA_FORCE_RESET(); // 加一些延时让复位生效 for (volatile uint32_t i 0; i 100; i); __HAL_RCC_GPDMA_RELEASE_RESET(); }执行完这个函数GPDMA所有寄存器回到复位值SUSP问题自然消失。代价是如果你同时用GPDMA的多个通道比如SPI的RX和TX或者UART的RX和TX全部需要重新初始化。而且如果这些通道正在传输强制复位会导致传输中断数据丢失。所以只适合“DMA已经卡死准备恢复现场”的兜底逻辑。我建议把它放在错误恢复分支里作为最后一层保险而不是每条SPI通信都调用。4.4 中断和主循环的同步细节排查这个问题的过程中我还发现一个容易踩的并发坑如果在DMA传输完成中断里清除标志、重新启动下一轮传输而主循环里恰好也在处理超时逻辑调用abort两者会打架。这种冲突的表现是主循环先发起abort回调触发TCIF中断里认为传输完成立刻启动下一轮DMA。但此时主循环的abort还没完全结束SUSP残留就发生了。解决办法是为每个GPDMA通道定义一个软件状态标志比如typedef enum { DMA_CH_IDLE 0, DMA_CH_BUSY, DMA_CH_ABORTING, } dma_ch_state_t;启动DMA前先判断状态只有IDLE时才允许启动。abort函数先把状态改为ABORTING完成后再改为IDLE同时用一个标志告诉中断回调“这次不要启动新传输”。简单说DMA停止、启动、完成回调必须串行化。不能像裸机时代那样随手写几个寄存器了事。5. 常见误区与排查清单5.1 容易迷惑人的几个现象CxSR全0不代表通道就是干净的。这个项目里CCR的SUSP比CSR更容易反映异常状态。EN写1了不代表DMA会跑。SUSP置1时EN1会被忽略最好读回CCR确认是不是真的设置成功。重新初始化整个通道不能清除SUSP残留。试过全寄存器清零结果CCR清完后SUSP还是1要借助RCC或者写1清0操作。HAL_DMA_Abort返回HAL_OK也不代表没出问题。HAL库不一定检测SUSP残留异常路径上它可能成功返回但通道已经废了。5.2 排查同类问题的步骤如果遇到类似“DMA偶发不工作”的问题我建议按以下顺序排查先dump DMA所有相关寄存器不要只看中断标志。检查CCR的控制位组合是否合理特别注意SUSP和EN的组合。看一下CSR的TCIF、HTIF、CAIF区分是正常完成还是错误终止。确认这次abort是在通道活动时调用还是通道已经空闲时调用。查阅参考手册和勘误表。STM32H562的勘误表里确实有GPDMA在异常路径下状态位残留的描述。5.3 排查时的调试技巧在调试器里把GPDMA的寄存器窗口固定出来每次跑完一轮SPI收发都看一眼。如果你发现SUSP位偶尔置1但通信还能跑那说明可能已经踩在坑边缘了建议提前把abort流程优化掉。另外一个技巧是给SPI DMA的超时逻辑加一个寄存器快照功能typedef struct { uint32_t cr; uint32_t sr; uint32_t tr1; uint32_t tr2; uint32_t ndtr; uint32_t dar; } gpdma_snapshot_t; static gpdma_snapshot_t gpdma_snap; void gpdma_capture_snapshot(GPDMA_Channel_TypeDef *ch) { gpdma_snap.cr ch-CCR; gpdma_snap.sr ch-CSR; gpdma_snap.tr1 ch-CTR1; gpdma_snap.tr2 ch-CTR2; gpdma_snap.ndtr ch-CBR1; gpdma_snap.dar ch-CDAR; }把这份快照打日志或者存RAM里出现卡死时能直接拿到第一手数据不用每次靠复现碰运气。6. 写在最后这个坑最烦人的地方不是代码debug难而是它让你怀疑自己的软件逻辑为什么SPI外设没问题、中断也触发了、DMA配置也重新写过通道就是不动如果你以后在STM32H562上遇到“SPI DMA偶发卡死、复位后恢复”的问题先查一下相关GPDMA通道的CxCR看SUSP位是否为1。我现在项目里的做法是把所有GPDMA相关的abort操作封装成统一接口内部做SUSP清理外部所有外设SPI、UART、I2C都走这个接口不再直接调HAL_DMA_Abort。另外我再加了一处熔断保护如果检测到连续两次abort失败直接RCC复位GPDMA外设避免产品在客户现场长时间卡死。再多提一个小细节如果你用HAL_SPI_DMAStop之后紧接着调HAL_SPI_Receive_DMA卡死概率会更高。我建议在HAL_SPI_DMAStop之后留一个极短的延时或者主动读取一次CxCR确认SUSP再启动下一次接收。这几十微秒的开销换来的是DMA通道稳定可靠非常值。
返回列表