HK32L010 充电态 OTA 后看门狗复位:UART ORE 中断风暴排查
一次 MCU OTA,固件已经升级成功,新版本也能发出启动消息,但设备随后陷入约 15 秒一次的循环复位。更反常的是:问题只在插着充电器时稳定复现,不充电时 OTA 正常,插着充电器整机冷启动也正常。
表面上看,这很像充电检测、Flash 写入或看门狗喂狗出了问题。最终根因却在新应用的启动窗口:
Bootloader 关中断跳转应用后,MCU 在重新开中断之前向在线主控发送了一个请求。主控返回的多字节 ACK 无法被及时读取,触发 UART Overrun Error(ORE)。开中断后,错误处理代码又没有真正清掉 ORE,CPU 持续进入 UART ISR,主循环得不到运行机会,最终无法喂狗并在约 15 秒后复位。
这次排查最有价值的并不是某个寄存器写法,而是如何利用复位周期、启动日志和场景差异,把一个看似与“充电”相关的问题收敛为确定的启动时序故障。
1. 系统背景与故障边界
设备由一个主控和一颗 HK32L010F8N6 MCU 组成,两者通过 UART 通信。MCU 负责部分电源与外设控制,主控可以通过通信链路为 MCU 升级固件。而设备是锂电池供电,充电检测和充满检测以至于电池电量都由这颗 MCU 采集。
故障发生在 OTA 完成后的 MCU 重启阶段,而不是固件下载或 Flash 写入阶段:
| 观察项 | 现象 |
|---|---|
| 触发条件 | 充电器插入、主控在线、MCU OTA 或软复位 |
| 复位周期 | 约 15 秒,与当前固件的 IWDG 超时配置一致 |
| OTA 结果 | 新固件版本已经生效 |
| 启动表现 | 每轮都能发出复位原因和 ready,随后彻底沉默 |
| 主控查询 | MCU 不再响应版本和状态查询 |
| 不充电 OTA | 正常 |
| 充电状态下整机冷启动 | 正常 |
“新版本已经运行并发出 ready”非常关键。它说明 Bootloader 已经完成升级和跳转,问题发生在新应用初始化的后半段。继续围绕 OTA 包、Flash 搬运和升级进度打转,方向就偏了。
2. 先把看门狗当作症状,而不是根因
约 15 秒的稳定周期首先指向 IWDG,因为它与当前项目的看门狗超时配置一致。但看门狗只回答了“系统最后为什么复位”,没有回答“主循环为什么失去执行机会”。
日志还有第二个线索:MCU 每轮都能发出 ready,然后马上沉默。这更像某个初始化阶段结束后发生了永久阻塞或中断风暴,而不是业务运行一段时间后随机崩溃。
因此排查问题可以进一步缩小为:
- 哪个动作发生在 ready 前后?
- 为什么只有“充电 + 主控在线 + MCU 单独复位”时会触发?
- 什么机制能让 CPU 长时间无法回到主循环,同时又不触发 HardFault?
3. 三组对照实验揭示了真正的触发条件
最有效的实验不是继续修改代码,而是把“充电”“主控是否在线”和“MCU 如何启动”拆开。
| 场景 | MCU 初始化期间是否发送电源状态查询 | 主控是否及时回 ACK | 结果 |
|---|---|---|---|
| 充电 + OTA / MCU 软复位 | 是 | 是 | 稳定复现循环复位 |
| 不充电 + OTA / MCU 软复位 | 否 | 否 | 正常 |
| 充电 + 整机冷启动 | 是 | 主控尚未完成启动,不能立即应答 | 正常 |
这张表把“充电电路故障”改写成了一个更准确的问题:
只有 USB/充电状态成立、主控已经在线、MCU 正处于关中断启动窗口时,UART 才会收到一段无法及时处理的回复。
充电只是让 MCU 在初始化期间发送查询的业务条件;主控在线才让回复落入危险窗口。两者缺一不可。
几个早期怀疑也由此被排除:
- 关闭充电指示灯后仍然复现,因此不是灯效任务占满 CPU;
- 关闭 USB 相关外部中断后仍然复现,因此不是 USB EXTI 风暴;
- 新应用已经发出 ready,因此不是 Bootloader 没有跳转;
- 新版本号已经生效,因此不是 OTA 写入失败;
- 不充电时走同一套 OTA 流程正常,因此 Flash 写入期间喂狗不是主要矛盾。
4. 还原故障发生的启动时序
在这个系统中,Bootloader 使用 __disable_irq() 关闭全局中断后跳转到应用。对 Cortex-M0 而言,这会设置 PRIMASK;被屏蔽的普通中断要等到应用重新开中断后才会得到处理。
问题在于:应用已经初始化 UART 接收,却在重新开中断之前发送了一个需要主控应答的电源状态查询。
在本平台和当前配置下,UART 接收数据寄存器没有足够深度容纳这段 ACK。关中断期间,第一个字节尚未被软件取走,后续字节继续到达,硬件置位 ORE。
仅仅产生 ORE 本不应该让系统永久卡死。真正把一次短暂溢出放大为循环复位的,是错误的中断清理路径。
5. GetITStatus 不一定等于“读取原始硬件标志”
原来的 ISR 使用了类似下面的逻辑:
if (UART_GetITStatus(UART2, UART_IT_ORE) == SET) {
(void)UART_ReceiveData(UART2);
UART_ClearITPendingBit(UART2, UART_IT_ORE);
}
看起来它检查并清除了 ORE,但在本项目使用的 HK32 标准外设库中,UART_GetITStatus() 判断的不只是状态位,还会检查对应中断源是否使能。当前代码使能了 RXNEIE,却没有使能 CR3.EIE,因此这个判断始终返回 RESET,清理分支永远不会执行。
与此同时,本平台实测表现为:RXNEIE 已开启时,ORE 仍然能够让 UART 中断持续挂起。结果就是 ISR 退出后立即再次进入,主循环被完全饿死。
这里需要特别限定结论:这是对当前 HK32L010、当前 UART 配置和当前版本标准外设库源码的审查结果。不同 MCU、不同 UART IP 和不同 HAL 对 GetITStatus 的定义可能不同,不能只凭函数名类推。
修复方向是将“原始硬件状态”和“中断源是否使能”分开处理。下面是经过简化的伪代码,实际清除顺序应以目标芯片用户手册和外设库实现为准:
void uart_rx_isr(void)
{
while (UART_GetFlagStatus(UART2, UART_FLAG_RXNE) == SET) {
uint8_t byte = (uint8_t)UART_ReceiveData(UART2);
rx_buffer_push(byte);
}
if (UART_GetFlagStatus(UART2, UART_FLAG_ORE) == SET) {
UART_ClearITPendingBit(UART2, UART_IT_ORE);
uart_error_counters.ore++;
}
/* FE、NE、PE 采用同样原则:按硬件 Flag 判断并按手册清除。 */
}
核心不是照抄这段代码,而是确认三个问题:
- ISR 判断的是原始状态位,还是“状态位 AND 中断使能位”?
- ORE 的清除序列是什么:读状态、读数据、写 ICR,还是其他组合?
- 接收错误发生后,ISR 是否一定能收敛并返回主循环?
6. 最终采用双层修复,而不是只消除触发条件
最终方案包含两部分:
| 层次 | 修改 | 解决的问题 |
|---|---|---|
| 根因修复 | ISR 按硬件 Flag 检查并正确清除 ORE、FE、NE、PE,同时排空 RX 数据 | 即使未来再次溢出,也不会形成永久中断风暴 |
| 时序加固 | 把需要主控 ACK 的查询移动到重新开中断之后 | 避免启动阶段主动制造 UART 溢出窗口 |
为什么两项都要做?
如果只把查询延后,当前复现场景会消失,但 UART 错误处理仍是坏的。以后只要调试断点、CPU 短时忙碌、波特率变化或通信突发再次触发 ORE,系统仍可能卡死。
如果只修 ISR,系统可以从 ORE 中恢复,但初始化期间发送需要应答的请求仍然会造成丢包、超时和错误的电源状态判断。
技术负责人的修复目标不应只是让眼前用例通过,而是同时处理“系统为什么会进入异常状态”和“进入异常状态后为什么无法恢复”。
7. 修复验证与结论边界
修复后的验证覆盖了原复现场景:充电器保持插入、主控在线、MCU 执行 OTA 或软复位。新应用启动后能够继续响应版本与状态查询,充电相关状态恢复上报,约 15 秒的 IWDG 循环不再出现,主控也不再因为查询超时而误判 MCU 状态。
这足以证明本次根因链条闭环,但还不能等同于完整可靠性认证。正式发布前仍应补充:
- 不同电池电压和供电边界;
- 主控在 ACK 发送中途复位或断电;
- UART 连续噪声、帧错误和高负载突发;
- 多次连续 OTA 与 MCU 单独复位;
- 更长时间运行下的 UART 错误计数与看门狗记录。
因此,准确的结论是“原稳定复现场景已修复并完成针对性验证”,而不是“所有 UART 和 OTA 异常都已解决”。
8. 从这个案例提炼出的排障方法
8.1 用时间规律定位最后一道保护机制
稳定的 15 秒周期提示 IWDG,但不要止步于“增加喂狗”。应继续追问:是主循环阻塞、关中断过久、死循环,还是高频 ISR 让主循环根本没有机会运行?
8.2 用“最后一条正常日志”划分启动阶段
每轮都能发 ready,说明 Bootloader、向量跳转和应用早期初始化大概率已经完成。问题边界应移动到 ready 前后的中断恢复、外设启动和任务调度,而不是继续泛查 OTA 下载。
8.3 对条件组合做正交拆分
“充电时复现”不是完整条件。把充电状态、主控在线状态、MCU 热复位和整机冷启动分开,才能发现真正的交集是“关中断窗口收到回复”。
8.4 记录被排除的方向
关闭灯效、关闭 EXTI、确认新版本生效等实验看似没有直接修复问题,却防止团队反复回到同一批错误假设。好的 Debug Case 不只记录答案,也记录哪些方向已被什么证据排除。
8.5 把异常恢复能力纳入架构设计
UART ORE、FE、NE、PE 不应被当作“正常情况下不会发生”。可靠系统要假设这些错误终会出现,并保证 ISR 能清错、计数、退出,让上层决定重试、丢包还是重建会话。
9. 一份可复用的快速检查清单
遇到“MCU 周期性复位、能发 ready 后沉默、不响应查询”时,可以依次检查:
结语
这个问题最容易让人误判的地方,是“充电”与“OTA”都很显眼。真正决定故障的却是一段只有十几毫秒的启动时序:中断关闭、请求提前发送、主控快速回复、UART 溢出,以及一个永远无法进入的错误清理分支。
排查复杂嵌入式故障时,症状名称往往不等于问题归属。把现象转换成时间线,把条件拆成对照实验,再要求根因解释所有差异,通常比继续堆日志或随机改代码更有效。
参考资料
- 航顺 HK32 MCU 官方产品列表:用于确认 HK32L010F8N6 产品系列与公开器件信息。
- Arm Cortex-M0 Devices Generic User Guide:Cortex-M0 编程模型、PRIMASK 与 CPS 指令参考。
- Arm:Cortex-M 中断优先级与 PRIMASK:
CPSID i、CPSIE i和全局中断屏蔽语义说明。
本文的 UART 库函数行为、触发时序与修复效果来自 HK32L010F8N6 真机案例;迁移到其他芯片前,请以对应芯片参考手册、外设库源码和实测结果为准。

浙公网安备 33010602011771号