UART DMA 为什么会“静默死亡”:一次噪声就让接收口永久失联

Babel36acl 嵌入式实战 无~ 17 次阅读 预计阅读时间: 11 分钟 发布于 4 天前 最后更新于 1 小时前 2434 字


摘要:在 `HAL_UART_Receive_DMA()` 模式下,串口帧错误或噪声错误可能让 HAL 结束接收却不自动恢复,结果是通信口看起来还在,实际上已经永远收不到数据。

这篇文章复盘一个在产线和现场环境里都很常见的串口问题:设备看起来正常运行,心跳还在,任务也没崩,但 HMI 或上位机命令完全无响应。根因不是协议,而是 HAL 在 UART 错误后悄悄停掉了 DMA 接收。

UART DMA 为什么会“静默死亡”:一次噪声就让接收口永久失联

串口问题最讨厌的一种,不是彻底挂死,而是“像活着一样地死掉”。

设备还在跑,心跳还在闪,主循环和任务调度也都正常,可上位机发过去的命令就是没有回音。你以为是线松了、干扰大、协议错了,最后发现根因藏在 HAL 的默认错误处理里。

现场现象

问题最早出现在带串口屏的人机接口链路上,设备在现场运行一段时间后,突然出现下面这些表现:

  • 串口命令无响应
  • 本地逻辑和定时任务还在继续执行
  • 指示灯和继电器动作都正常
  • 重启后恢复,但过一阵又会复发

这类现象很容易把人带偏。因为它不像“系统死机”,更像“外部通信偶发异常”。

如果只从表面看,常见怀疑对象会是:

  • 上位机程序发错了帧
  • 线缆接触不良
  • RS232/RS485 收发器偶发失步
  • 某一帧数据把协议状态机打乱了

这些方向都不算错,但还没到根上。

根因:HAL 在出错后把接收停了,却不帮你拉起来

HAL_UART_Receive_DMA() 模式下,HAL 对接收错误的默认处理大致是:

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    if (huart->RxState == HAL_UART_STATE_BUSY_RX) {
        UART_EndRxTransfer(huart);
        /* 结束当前接收 */
        /* 不自动恢复 DMA */
    }
}

一旦出现这些错误中的任意一种:

  • 帧错误
  • 噪声错误
  • 溢出错误

HAL 可能就会结束当前接收流程。关键在于,默认路径通常不会替你自动重启 DMA。

于是现场就变成了这样:

  1. UART 出现一次物理层干扰
  2. HAL 进入错误回调
  3. DMA 接收被结束
  4. 后续没有重新启动接收
  5. 通信口从此“活着但收不到”

系统本身没有崩,所以它会继续执行其它任务。这就是为什么现场会误判成“上位机偶尔发不通”。

为什么这个问题会特别隐蔽

1. 故障只打断通信,不打断主程序

如果是 HardFault,至少你知道系统死了。这个问题更坏,它只让接收口失效,整机其余部分仍然在继续运行。

2. 复位后恢复,容易被当成偶发干扰

只要重启一切恢复正常,团队就很容易把它归类成“电气环境不好”。但如果不补自动恢复逻辑,下次还会再来。

3. HAL 默认行为对业务层并不友好

从库设计角度看,“出错后结束当前接收,等上层决定怎么处理”并不算错误;但从设备工程角度看,如果上层没有显式补救,这就是一个会让端口永久失联的坑。

更稳妥的修复思路

直接的修复办法,是在错误回调里明确做恢复,而不是指望 HAL 自动处理。

典型做法如下:

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        HAL_UART_DMAStop(huart);
        __HAL_UART_CLEAR_FEFLAG(huart);
        __HAL_UART_CLEAR_NEFLAG(huart);
        __HAL_UART_CLEAR_OREFLAG(huart);
        HAL_UART_Receive_DMA(huart, rx_buf, RX_BUF_SIZE);
    }
}

如果项目里用的是 DMA + IDLE 收包模型,还应该一起检查:

  • IDLE 中断是否仍然打开
  • DMA 缓冲区指针是否被正确复位
  • 环形缓冲或 StreamBuffer 是否存在残留脏数据

只做“自动重启 DMA”还不够

在现场环境里,我更建议把修复拆成三层。

第一层:错误后自动恢复

这是最基本的兜底,没有它,串口口子可能一次干扰就永久失效。

第二层:把错误暴露出来

至少要能统计这些信息:

  • 最近一次 UART 错误类型
  • 错误累计次数
  • 最后一次自动恢复时间

否则系统虽然能自愈,但你永远不知道现场到底脏到什么程度。

第三层:在协议层做超时与重同步

物理层错误恢复了,不代表协议层一定还干净。建议同时检查:

  • 收包状态机能否在帧损坏后重新同步
  • 超时后是否会丢弃半包
  • 应答链路是否存在“上一次状态残留”

这个坑背后其实是“库语义”和“设备语义”不一致

HAL 的逻辑更接近“库开发者视角”:

  • 出错了
  • 我把当前传输停掉
  • 后面交给你自己决定

而设备工程更关心的是:

  • 现场偶发噪声是常态
  • 通信口必须具备自恢复能力
  • 出错后最好自己回来,而不是等人重启

两种语义不冲突,但如果项目直接把 HAL 默认行为当成最终行为,就很容易留下这种“静默死亡”的漏洞。

一份可以直接照着查的清单

如果你的项目用了 UART + DMA,建议把下面几项都过一遍:

  1. UART 错误回调里有没有显式恢复接收
  2. DMA + IDLE 路径在错误后能不能正常回到接收状态
  3. 错误计数和日志有没有留下来
  4. 协议状态机在半包、脏包、错包后能不能重新同步
  5. 现场是否区分“系统活着”和“通信链路活着”

最后一句

最难查的 bug,往往不是“完全坏掉”,而是“坏了一半”。UART DMA 的这个坑就是典型例子。

系统还在跑,所以大家觉得它没问题。可对操作员来说,收不到命令的设备,和死机其实没有区别。

此作者没有提供个人介绍。
最后更新于 2026-05-30