STM32 串口连续接收“假死“:一个判错 DMA 通道的低级错误

STM32 串口连续接收“假死“:一个判错 DMA 通道的低级错误 单帧发送一切正常一连发就卡住——这种 bug 最磨人。这篇记一下前阵子踩的一个坑现象看着像协议解析挂了实际是发送完成中断判错了 DMA 通道的标志结果把接收这条线整个锁死。现象协议很简单就是帧头 命令字 数据 帧尾。接收走中断每来一个字节塞进软件 FIFO主循环再从 FIFO 里取出来解析。单帧发没问题。可上位机一连发多帧就会出现前几帧解析正常到某一帧之后本地数据不再更新但串口工具还在不停地发MCU 也没复位、没进异常。看上去就是解析流程突然不动了。收发那几段代码长这样// 接收中断每来一字节入 FIFOvoidUSARTx_IRQHandler(void){if(UART_RX_NOT_EMPTY)EnFifo(uart_rx_fifo,READ_UART_DATA());if(UART_IDLE_DETECTED){uart_tx_busy0;// 注意这句收到帧间空闲却顺手清了发送忙标志CLEAR_UART_IDLE_FLAG();}}// 主循环发送不忙时才解析voidProtocol_Handle(void){uint8_tdata;if(uart_tx_busy0DeFifo(uart_rx_fifo,data))Protocol_Parse(data);}// 解析成功后用 DMA 回 ACKvoidUart_SendAck(void){uart_tx_busy1;HAL_UART_Transmit_DMA(huart,tx_buf,len);}这里先记住一点主循环要不要解析卡在uart_tx_busy这一个标志上。后面的麻烦全从这儿来。怎么找到的挂上 debug 单步跟很快就缩小了范围。串口接收中断照常进字节也确实进了 FIFO说明收这一侧没问题但DeFifo后来再没被调到解析停在了入口之前。盯住那个入口条件一看uart_tx_busy连发的时候卡在 1 再也没回过 0。再往 ACK 发送那条线追。在清零那行打个断点连发时根本不命中同时拿逻辑分析仪扫一眼 TX 脚ACK 明明早就发完了。也就是说DMA 传输是正常完成的只是发完之后清忙这一步没执行。问题这下指向了发送完成中断。真正的原因通道判错了进中断一核对破绽就出来了。以 F1、USART1_TX走DMA1_Channel4为例原代码大致是这样voidDMA1_Channel4_IRQHandler(void){// 实际是通道 4这里却判了通道 5 的 TC 标志if(__HAL_DMA_GET_FLAG(hdma_uart_tx,DMA_FLAG_TC5)!RESET){__HAL_DMA_CLEAR_FLAG(hdma_uart_tx,DMA_FLAG_TC5);uart_tx_busy0;// ← 这句永远进不来}HAL_DMA_IRQHandler(hdma_uart_tx);}通道 4 的活却判通道 5 的完成标志条件永远不成立uart_tx_busy 0这句从来没跑过。F4/F7 是流 TCIFx_y 组合标志把 Stream 号或者宏写错是同一个坑的另一副长相。不过这里有个前提得说清楚不然懂行的会觉得逻辑对不上这段代码末尾还调了HAL_DMA_IRQHandler如果句柄绑定的通道是对的HAL 自己就会识别完成、回调HAL_UART_TxCpltCallback——那手动判错通道其实不会卡死HAL 会兜底。所以这个 bug 能成立要么是发送完成处理完全寄存器手写、没接 HAL 回调要么是压根没实现TxCpltCallback、清忙全靠上面那段手判。我这边属于后者。捋顺了就明白卡在哪了发 ACK 前uart_tx_busy置 1发完没清掉于是主循环里if (uart_tx_busy 0)永远过不去。这边串口中断还在收、还在往 FIFO 里塞那边主循环却再也不去取——数据堆在 FIFO 里没人理表面看就成了还在收、解析却停了。说到底判错标志只是导火索真正别扭的是设计本身拿一个发送状态去闸接收侧的处理把两条本该各走各的线绑死在了一个变量上。为什么单帧好好的这 bug 最唬人的地方就是单帧、或者帧间隔大一点的时候一切正常。原因就藏在前面那句被我标了注释的代码里——接收中断的 IDLE 分支顺手把发送忙标志也给清了。两帧之间留了空隙就会触发 IDLE于是流程变成发一帧、解析、回 ACK、置忙、DMA 发完但没清bug 潜伏着、帧间空闲触发 IDLE、忙标志被误清——下一帧照样能解析看着风平浪静。可连发的时候帧挨着帧几乎没有空隙IDLE 不一定及时来uart_tx_busy就长期卡在 1。换句话说是 RX 的 IDLE 事件去清 TX 的 busy 这种职责混乱在单帧场景下歪打正着替真 bug 打了掩护反倒让问题更难露馅。怎么修最直接的用哪个通道就判哪个通道的标志if(__HAL_DMA_GET_FLAG(hdma_uart_tx,DMA_FLAG_TC4)!RESET){__HAL_DMA_CLEAR_FLAG(hdma_uart_tx,DMA_FLAG_TC4);uart_tx_busy0;}但更省心的是干脆别手写判标志直接用 HAL 的发送完成回调从根上躲开写错通道的风险正常模式下也不用每次手动HAL_UART_DMAStopHAL 会自己收尾voidHAL_UART_TxCpltCallback(UART_HandleTypeDef*huart){if(huarttarget_huart)uart_tx_busy0;}用了回调就把那段手判删干净别留着两套并存徒增混乱。顺带说说设计上的坑一行就能修好的 bug背后其实是两处状态串味凑一块儿才闹出来的值得一并改掉。一是 TX 和 RX 的状态别共用一个变量更别让 IDLE 这种接收事件去碰发送的 busy——谁的事归谁管免得一个标志被好几条流程随手改。二是想清楚发送时为什么要停解析。如果是 RS485 半双工发的时候本来就收不了那道闸是合理的正确做法是让 busy 标志可靠比如上面的回调而不是把闸拆了要是全双工、收发互不影响那接收 FIFO 就该一直消费别让连发的数据堆到溢出。先看自己是哪种再决定怎么改。顺便提一句FIFO 写失败最好加个计数不然真溢出了你也只看到解析不动了根本分不清到底丢没丢数据。还有就是测试别只测单帧连发、无间隔、异常帧混入、FIFO 快满这些场景才是真正能把状态机和标志管理里的雷逼出来的——这次就是活例子。小结整条因果链是判错 DMA 通道的完成标志 →uart_tx_busy清不掉 → 主循环停掉接收 FIFO 的消费 → 解析停摆 → 连发时表现为卡死。单帧之所以没事纯粹是帧间空闲触发 IDLE间接把那个错误的忙状态清掉了。留两句话串口里收到数据从来不等于数据被处理掉了中间任何一个状态标志卡死都能让接收整条线停摆、却伪装成解析的 bug而连发测试往往比单帧更能把这种藏在状态机和 DMA 标志里的问题逼出来——越是单帧好好的越要留个心眼。