样章-第10讲-看门狗异常恢复与卡机排查方法论

第 10 讲 | 看门狗、异常恢复与"卡机"排查方法论

【本讲要点】设备卡机是量产后最贵的 bug------它不报错、不复现、还挑现场。这一讲给你一套从"看门狗设计"到"复位原因回溯"的完整方法。


一、一个真实的卡机案例

现象描述(来自现场工单):

"按下紧急求助按键后,设备偶尔就没反应了,灯也不闪、按键也不响应。断电重启才恢复。"

关键词:"偶尔" 、"没反应" 、"重启恢复"。

这类问题最折磨人,因为它:

  • 不复现:实验室按 100 次可能一次都不出;
  • 无日志:卡住的时候设备已经死了,什么都打不出来;
  • 归因难:用户说"按了按键就这样",但按键可能只是触发条件,不是原因。

这一讲就按这个案例,把排查路径完整走一遍。


二、卡机的四类根因

先把"卡机"拆开。设备没反应,只有四种可能:

类型 现象 典型成因
死循环 CPU 满载,中断还能进 while(!flag); 等一个永远不来的标志
死锁 任务互相等待 两个任务交叉拿锁;在临界区里等信号量
HardFault 直接进异常处理 空指针、栈溢出、非对齐访问
中断风暴 主循环永远得不到执行 中断太频繁且没清标志 → 反复触发

排查第一步永远是:先确定属于哪一类。

怎么区分?

  • 看喂狗 :如果看门狗没复位,说明系统还在"活着",可能是死锁 或某个任务卡住;
  • 如果看门狗复位了 :说明确实是死循环或 HardFault(忙等也会停止喂狗);
  • 接调试器打断:看 PC 停在哪、哪个任务在跑------这是最快的定位方式。

三、看门狗设计:谁喂、在哪喂

看门狗(Watchdog)是量产设备的最后一道保险。但用错了,它就成了"随机重启机"。

3.1 两个最常见的错误

错误 1:在一个高优先级任务里喂狗

c 复制代码
/* 危险:只要这个任务活着,其他任务全死了也不复位 */
void led_task(void *arg)
{
    for (;;) {
        toggle_led();
        iwdg_feed();          /* 只看这一个任务,等于没看 */
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

错误 2:在中断里喂狗

c 复制代码
/* 更危险:只要系统 tick 还在跑,狗就永远不咬 */
void SysTick_Handler(void)
{
    iwdg_feed();              /* 完全失去意义 */
}

看门狗要能发现"任务卡死",就必须由"所有关键任务的健康状态共同决定"。

3.2 正确的喂狗:健康标志位

c 复制代码
/* 每个关键任务定期置自己的位;只有全部置位才喂狗 */
#define HB_SAMPLE   (1u << 0)
#define HB_PUBLISH  (1u << 1)
#define HB_FSM      (1u << 2)
#define HB_ALL      (HB_SAMPLE | HB_PUBLISH | HB_FSM)

static volatile uint32_t s_heartbeat = 0;

/* 各任务到达自己的循环末尾就置位 */
void hb_report(uint32_t bit)
{
    taskENTER_CRITICAL();
    s_heartbeat |= bit;
    taskEXIT_CRITICAL();
}

/* 监护任务:只有所有位都置齐了,才喂狗并清位 */
void wdg_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 的异常进入会自动压栈,我们可以把它取出来。

c 复制代码
/* 取自 FreeRTOS/裸机的通用 HardFault 处理 */
__attribute__((naked)) void HardFault_Handler(void)
{
    __asm volatile (
        "tst lr, #4        \n"
        "ite eq            \n"
        "mrseq r0, msp     \n"
        "mrsne r0, psp     \n"
        "b hardfault_dump  \n"
    );
}

void hardfault_dump(uint32_t *sp)
{
    /* 压栈顺序:R0, R1, R2, R3, R12, LR, PC, xPSR */
    uint32_t r0  = sp[0];
    uint32_t r1  = sp[1];
    uint32_t r2  = sp[2];
    uint32_t r3  = sp[3];
    uint32_t r12 = sp[4];
    uint32_t lr  = sp[5];
    uint32_t pc  = sp[6];      /* ← 出事的那条指令地址 */
    uint32_t psr = 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 (;;);      /* 停在原地,等看门狗复位 或 调试器接管 */
}

关键用法:

  1. 记下 PC 的值;
  2. 打开编译生成的 .map 文件,搜索这个地址落在哪个函数区间;
  3. 结合反汇编,就能定位到具体哪一行出的问题。

这套方法把"随机死机"变成了"可定位的崩溃点"。


五、复位原因判断:开机第一件事

每次上电/复位,第一件事应该是读取复位原因,并记下来。

c 复制代码
typedef enum {
    RST_POWER_ON,       /* 上电复位 */
    RST_WATCHDOG,       /* 看门狗复位 ← 卡机的证据 */
    RST_SOFTWARE,       /* 软件主动复位 */
    RST_PIN,            /* 复位引脚 */
    RST_BROWNOUT,       /* 掉电/欠压 */
    RST_UNKNOWN,
} reset_cause_t;

static reset_cause_t s_boot_cause;

void boot_read_reset_cause(void)
{
    if (rcu_flag_get(RCU_FLAG_FWDGTRST)) {          /* 看门狗 */
        s_boot_cause = RST_WATCHDOG;
    } else if (rcu_flag_get(RCU_FLAG_SWRST)) {      /* 软件复位 */
        s_boot_cause = RST_SOFTWARE;
    } else if (rcu_flag_get(RCU_FLAG_PORRST)) {     /* 上电 */
        s_boot_cause = RST_POWER_ON;
    } else if (rcu_flag_get(RCU_FLAG_EPRST)) {      /* 引脚 */
        s_boot_cause = RST_PIN;
    } else {
        s_boot_cause = RST_UNKNOWN;
    }
    rcu_all_reset_flag_clear();                     /* 清标志,下次才能测准 */
}

这个值要落到 Flash 里、要随上报数据一起传上去:

c 复制代码
/* 每次启动记录一次,量产设备靠这个统计"卡机率" */
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 缺位 → 通信任务没转圈)

指向:通信/上报任务卡住了。

第三步:定位代码

检查求助功能与通信任务的交互:

c 复制代码
/* 问题代码(简化) */
void sos_key_isr(void)
{
    g_sos_pending = 1;
}

void publish_task(void *arg)
{
    for (;;) {
        if (g_sos_pending) {
            g_sos_pending = 0;
            sos_send_immediately();      /* ← 内部有一个同步等 ACK 的循环 */
            continue;                    /* ← 跳过了 hb_report()! */
        }
        /* 正常上报 */
        ...
        hb_report(HB_PUBLISH);
    }
}

问题找到了:

  1. sos_send_immediately() 里有一个"等待 ACK"的同步循环,在弱网下会阻塞很久(这就是"偶尔"的原因------网络好的时候不卡);
  2. 更致命的是,走 SOS 分支时用了 continue,跳过了 hb_report(HB_PUBLISH),健康位永远凑不齐 → 看门狗复位。

两个 bug 叠在一起:一个是阻塞,一个是漏报心跳。

第四步:修复

c 复制代码
void publish_task(void *arg)
{
    for (;;) {
        /* 1. SOS 改成"插队"而不是"阻塞立即发送" */
        if (g_sos_pending) {
            g_sos_pending = 0;
            sos_enqueue();               /* 投递到高优先级队列,不阻塞 */
        }

        /* 2. 无论走哪个分支,都要报心跳 */
        process_normal();
        hb_report(HB_PUBLISH);
    }
}

修复后:SOS 上报走独立的高优先级通道,且不再阻塞主上报循环;心跳必定上报。

这就是"看门狗 + 健康位 + 复位原因"三件套的价值------它把"偶尔卡机"变成了"可观测、可定位、可证伪"的工程问题。


七、可靠性工具箱(可直接落地)

  1. 看门狗:多任务健康位汇总喂狗,缺位则复位并记录缺哪一位
  2. HardFault 处理 :压栈现场 + RTT 输出 PC,配合 map 文件定位
  3. 复位原因统计:开机读取 + 落 Flash + 随上报上传
  4. 任务栈水位监视:第 3 讲讲过,防栈溢出
  5. 堆水位监视:防内存耗尽
  6. 关键路径日志落 Flash:卡机前最后几条日志是最宝贵的线索

八、小结

四句话:

1. 卡机只有四类根因,先用"看门狗是否复位"把范围砍一半。

2. 看门狗不能在一个任务里喂,必须由所有关键任务的健康位共同决定。

3. HardFault 要保存现场,PC + map 文件是定位崩溃点的标准姿势。

4. 复位原因必须记录并上传------这是判断"卡机是系统性问题还是偶发"的唯一依据。

至此 Part 4 结束。 下一讲(第 11 讲)进入 Part 5:我们从零实现一个 Modbus RTU 从机,讲清寄存器映射、帧解析和主从通信的真实调试过程。

相关推荐
Zw-awa1 小时前
让机器回答“我在哪”:激光雷达与 SLAM
stm32
启友玩AI1 小时前
微安级唤醒、毫安级识别:基于 CI3422 的 AI 眼镜语音交互开发实战
c语言·人工智能·科技·嵌入式硬件·ai·ar·语音识别
开发笔记-阿牛1 小时前
AM收音机蓝牙二合一设计复盘
人工智能·单片机·嵌入式硬件·音视频·音频
sunoo-2291 小时前
嵌入式Linux驱动学习笔记(第3-6天):从misc封装、platform总线到设备树、中断与单总线外设实战
linux·arm开发·驱动开发·笔记·单片机·学习
半杯咖啡半行码15 小时前
STM32之ARM通用汇编指令详解:从指令格式到寻址方式
汇编·arm开发·stm32
创界工坊工作室15 小时前
22-基于STM32的汽车防盗系统设计
stm32·单片机·嵌入式硬件·proteus
雨田言炎17 小时前
STM32之RCC系统时钟控制(芯片的心脏控制器)
stm32·单片机·嵌入式硬件
李永奉17 小时前
日常使用的锂电池上,新、旧锂电池的差异分析
单片机·嵌入式硬件·物联网·智能手机·电脑
额额额对了18 小时前
IMX6ULL 杂项设备 LED 驱动
stm32·单片机·嵌入式硬件