第 10 讲 | 看门狗、异常恢复与"卡机"排查方法论
【本讲要点】设备卡机是量产后最贵的 bug——它不报错、不复现、还挑现场。这一讲给你一套从"看门狗设计"到"复位原因回溯"的完整方法。
一、一个真实的卡机案例
现象描述(来自现场工单):
“按下紧急求助按键后,设备偶尔就没反应了,灯也不闪、按键也不响应。断电重启才恢复。”
关键词:“偶尔”、“没反应”、“重启恢复”。
这类问题最折磨人,因为它:
- 不复现:实验室按 100 次可能一次都不出;
- 无日志:卡住的时候设备已经死了,什么都打不出来;
- 归因难:用户说"按了按键就这样",但按键可能只是触发条件,不是原因。
这一讲就按这个案例,把排查路径完整走一遍。
二、卡机的四类根因
先把"卡机"拆开。设备没反应,只有四种可能:
| 类型 | 现象 | 典型成因 |
|---|---|---|
| 死循环 | CPU 满载,中断还能进 | while(!flag);等一个永远不来的标志 |
| 死锁 | 任务互相等待 | 两个任务交叉拿锁;在临界区里等信号量 |
| HardFault | 直接进异常处理 | 空指针、栈溢出、非对齐访问 |
| 中断风暴 | 主循环永远得不到执行 | 中断太频繁且没清标志 → 反复触发 |
排查第一步永远是:先确定属于哪一类。
怎么区分?
- 看喂狗:如果看门狗没复位,说明系统还在"活着",可能是死锁或某个任务卡住;
- 如果看门狗复位了:说明确实是死循环或 HardFault(忙等也会停止喂狗);
- 接调试器打断:看 PC 停在哪、哪个任务在跑——这是最快的定位方式。
三、看门狗设计:谁喂、在哪喂
看门狗(Watchdog)是量产设备的最后一道保险。但用错了,它就成了"随机重启机"。
3.1 两个最常见的错误
错误 1:在一个高优先级任务里喂狗
/* 危险:只要这个任务活着,其他任务全死了也不复位 */voidled_task(void*arg){for(;;){toggle_led();iwdg_feed();/* 只看这一个任务,等于没看 */vTaskDelay(pdMS_TO_TICKS(500));}}错误 2:在中断里喂狗
/* 更危险:只要系统 tick 还在跑,狗就永远不咬 */voidSysTick_Handler(void){iwdg_feed();/* 完全失去意义 */}看门狗要能发现"任务卡死",就必须由"所有关键任务的健康状态共同决定"。
3.2 正确的喂狗:健康标志位
/* 每个关键任务定期置自己的位;只有全部置位才喂狗 */#defineHB_SAMPLE(1u<<0)#defineHB_PUBLISH(1u<<1)#defineHB_FSM(1u<<2)#defineHB_ALL(HB_SAMPLE|HB_PUBLISH|HB_FSM)staticvolatileuint32_ts_heartbeat=0;/* 各任务到达自己的循环末尾就置位 */voidhb_report(uint32_tbit){taskENTER_CRITICAL();s_heartbeat|=bit;taskEXIT_CRITICAL();}/* 监护任务:只有所有位都置齐了,才喂狗并清位 */voidwdg_task(void*arg){for(;;){vTaskDelay(pdMS_TO_TICKS(3000));/* 看门狗超时要 > 这个值 */if(s_heartbeat==HB_ALL){iwdg_feed();s_heartbeat=0;/* 清位,下一轮重新累积 */}else{log_warn("hb missing: 0x%x",s_heartbeat);/* 不喂狗 → 让看门狗复位 */}}}这样设计的价值:
- 任何一个关键任务卡死,标志位凑不齐,看门狗必复位;
- 复位前还能打印出"缺哪一个"——
hb missing: 0x2直接告诉你 publish 任务挂了。
这是本讲最重要的一段代码。
3.3 看门狗超时怎么定
| 窗口 | 说明 |
|---|---|
| 太短 | 正常的长耗时操作(如 OTA 写 Flash)会被误复位 |
| 太长 | 卡死后要等很久才恢复,影响体验 |
经验值:超时时间是"最长正常业务周期"的 2~3 倍。如果最慢的任务 3 秒转一圈,看门狗设 8~10 秒。
特殊场景:OTA 写入、大批量 Flash 擦除这类"必然长耗时"的操作,要么在期间主动喂狗,要么临时切换到窗口看门狗或延长超时。
四、HardFault 现场还原
如果卡机是 HardFault 引起的,必须把现场"冻结"下来。Cortex-M 的异常进入会自动压栈,我们可以把它取出来。
/* 取自 FreeRTOS/裸机的通用 HardFault 处理 */__attribute__((naked))voidHardFault_Handler(void){__asmvolatile("tst lr, #4 \n""ite eq \n""mrseq r0, msp \n""mrsne r0, psp \n""b hardfault_dump \n");}voidhardfault_dump(uint32_t*sp){/* 压栈顺序:R0, R1, R2, R3, R12, LR, PC, xPSR */uint32_tr0=sp[0];uint32_tr1=sp[1];uint32_tr2=sp[2];uint32_tr3=sp[3];uint32_tr12=sp[4];uint32_tlr=sp[5];uint32_tpc=sp[6];/* ← 出事的那条指令地址 */uint32_tpsr=sp[7];/* 用 RTT 输出(此时串口可能已不可靠) */SEGGER_RTT_printf(0,"\r\n[HardFault]\r\n""R0 =%08x R1 =%08x R2 =%08x R3 =%08x\r\n""R12=%08x LR =%08x PC =%08x PSR=%08x\r\n",r0,r1,r2,r3,r12,lr,pc,psr);/* 把 PC 丢进 map 文件,就能定位到具体函数和行 */for(;;);/* 停在原地,等看门狗复位 或 调试器接管 */}关键用法:
- 记下
PC的值; - 打开编译生成的
.map文件,搜索这个地址落在哪个函数区间; - 结合反汇编,就能定位到具体哪一行出的问题。
这套方法把"随机死机"变成了"可定位的崩溃点"。
五、复位原因判断:开机第一件事
每次上电/复位,第一件事应该是读取复位原因,并记下来。
typedefenum{RST_POWER_ON,/* 上电复位 */RST_WATCHDOG,/* 看门狗复位 ← 卡机的证据 */RST_SOFTWARE,/* 软件主动复位 */RST_PIN,/* 复位引脚 */RST_BROWNOUT,/* 掉电/欠压 */RST_UNKNOWN,}reset_cause_t;staticreset_cause_ts_boot_cause;voidboot_read_reset_cause(void){if(rcu_flag_get(RCU_FLAG_FWDGTRST)){/* 看门狗 */s_boot_cause=RST_WATCHDOG;}elseif(rcu_flag_get(RCU_FLAG_SWRST)){/* 软件复位 */s_boot_cause=RST_SOFTWARE;}elseif(rcu_flag_get(RCU_FLAG_PORRST)){/* 上电 */s_boot_cause=RST_POWER_ON;}elseif(rcu_flag_get(RCU_FLAG_EPRST)){/* 引脚 */s_boot_cause=RST_PIN;}else{s_boot_cause=RST_UNKNOWN;}rcu_all_reset_flag_clear();/* 清标志,下次才能测准 */}这个值要落到 Flash 里、要随上报数据一起传上去:
/* 每次启动记录一次,量产设备靠这个统计"卡机率" */nvm_write_u8(NVM_KEY_LAST_RESET,s_boot_cause);log_info("boot cause = %d",s_boot_cause);价值:
- 如果现场统计发现 30% 的启动都是
RST_WATCHDOG,说明卡机是系统性问题,必须查; - 如果全是
RST_POWER_ON,说明设备其实很健康,问题在别处。
六、回到案例:SOS 按键卡机
有了上面的工具,我们回头看那个案例。
第一步:判断类型
在现场版本的固件里加上复位原因统计,跑一周后:
RST_WATCHDOG占比明显偏高,且都发生在"求助按键"被按之后。
结论:不是死锁,是"某个任务卡死导致喂狗失败"。范围立刻缩小。
第二步:看健康位
因为有hb missing日志,复位前的最后一条日志是:
hb missing: 0x6 (HB_PUBLISH | HB_FSM 缺位 → 通信任务没转圈)指向:通信/上报任务卡住了。
第三步:定位代码
检查求助功能与通信任务的交互:
/* 问题代码(简化) */voidsos_key_isr(void){g_sos_pending=1;}voidpublish_task(void*arg){for(;;){if(g_sos_pending){g_sos_pending=0;sos_send_immediately();/* ← 内部有一个同步等 ACK 的循环 */continue;/* ← 跳过了 hb_report()! */}/* 正常上报 */...hb_report(HB_PUBLISH);}}问题找到了:
sos_send_immediately()里有一个"等待 ACK"的同步循环,在弱网下会阻塞很久(这就是"偶尔"的原因——网络好的时候不卡);- 更致命的是,走 SOS 分支时用了
continue,跳过了hb_report(HB_PUBLISH),健康位永远凑不齐 → 看门狗复位。
两个 bug 叠在一起:一个是阻塞,一个是漏报心跳。
第四步:修复
voidpublish_task(void*arg){for(;;){/* 1. SOS 改成"插队"而不是"阻塞立即发送" */if(g_sos_pending){g_sos_pending=0;sos_enqueue();/* 投递到高优先级队列,不阻塞 */}/* 2. 无论走哪个分支,都要报心跳 */process_normal();hb_report(HB_PUBLISH);}}修复后:SOS 上报走独立的高优先级通道,且不再阻塞主上报循环;心跳必定上报。
这就是"看门狗 + 健康位 + 复位原因"三件套的价值——它把"偶尔卡机"变成了"可观测、可定位、可证伪"的工程问题。
七、可靠性工具箱(可直接落地)
- 看门狗:多任务健康位汇总喂狗,缺位则复位并记录缺哪一位
- HardFault 处理:压栈现场 + RTT 输出
PC,配合 map 文件定位 - 复位原因统计:开机读取 + 落 Flash + 随上报上传
- 任务栈水位监视:第 3 讲讲过,防栈溢出
- 堆水位监视:防内存耗尽
- 关键路径日志落 Flash:卡机前最后几条日志是最宝贵的线索
八、小结
四句话:
1. 卡机只有四类根因,先用"看门狗是否复位"把范围砍一半。
2. 看门狗不能在一个任务里喂,必须由所有关键任务的健康位共同决定。
3. HardFault 要保存现场,PC+ map 文件是定位崩溃点的标准姿势。
4. 复位原因必须记录并上传——这是判断"卡机是系统性问题还是偶发"的唯一依据。
至此 Part 4 结束。下一讲(第 11 讲)进入 Part 5:我们从零实现一个Modbus RTU 从机,讲清寄存器映射、帧解析和主从通信的真实调试过程。