为什么你的看门狗形同虚设?

在嵌入式开发中,看门狗(Watchdog Timer)是防止程序跑飞或死循环的硬件机制。然而,很多开发者只是简单地在主循环末尾或中断里周期性喂狗,这导致看门狗只能检测到“CPU完全卡死”的极端情况,而无法发现任务级故障——比如某个模块陷入死循环但中断仍正常触发,或者某个任务被饿死但主循环依然在跑。

核心问题:传统喂狗方式缺乏“任务健康”信息,看门狗复位后你依然不知道系统为何崩溃。

看门狗工作原理与喂狗的本质

看门狗本质上是一个递减计数器,当它减到0时触发系统复位。喂狗(Kick/Feed)就是重新装载计数值,防止复位。

关键点:

  • 喂狗是系统健康声明,不是简单的时间填充。
  • 喂狗时机应反映“关键任务是否按预期执行”。
  • 喂狗间隔必须小于看门狗超时时间,但又要留出足够余量。

常见喂狗误区

  1. 在定时器中断里喂狗:中断优先级高,即使主循环死机,中断仍可喂狗,看门狗永远不触发。
  2. 主循环末尾统一喂狗:无法区分哪个任务卡死,且若某个任务耗时过长,可能导致误复位。
  3. 喂狗代码散落各处:难以维护,且容易遗漏。

模块化喂狗架构设计

核心思想

将系统划分为多个独立任务(模块),每个任务维护自己的“健康标志”。主循环或专用监控任务检查所有任务标志,只有全部正常时才喂狗。若某个任务超时未更新标志,则禁止喂狗,让看门狗复位,同时记录故障模块。

架构图(文字描述)

任务A -> 更新标志A
任务B -> 更新标志B
任务C -> 更新标志C
...
监控任务 -> 检查所有标志 -> 全部OK? -> 喂狗 : 不喂狗(并记录故障)

实现步骤

  1. 定义任务健康结构体:每个任务对应一个标志和超时阈值。
  2. 任务入口/出口更新标志:在任务的关键执行点(如循环开始或完成)更新标志。
  3. 监控任务统一喂狗:周期性地检查所有标志,若全部有效则喂狗,否则记录故障并等待复位。
  4. 故障记录:在复位前将故障模块ID存入备份寄存器或EEPROM,便于重启后诊断。

完整代码示例(基于STM32 HAL库)

// watchdog_task.h
#ifndef WATCHDOG_TASK_H
#define WATCHDOG_TASK_H

#include <stdint.h>
#include <stdbool.h>

#define MAX_TASKS 4

typedef enum {
    TASK_LED = 0,
    TASK_SENSOR,
    TASK_COMM,
    TASK_DISPLAY
} TaskID;

typedef struct {
    bool valid;          // 标志是否有效
    uint32_t last_update; // 上次更新时间(ms)
    uint32_t timeout_ms;  // 超时阈值
} TaskHealth;

void Watchdog_Init(void);
void Watchdog_TaskUpdate(TaskID id);
void Watchdog_Monitor(void);

#endif
// watchdog_task.c
#include "watchdog_task.h"
#include "main.h"

extern IWDG_HandleTypeDef hiwdg;
extern uint32_t HAL_GetTick(void); // 使用系统tick

static TaskHealth task_health[MAX_TASKS];
static uint32_t fault_task = 0xFFFFFFFF; // 故障任务记录

void Watchdog_Init(void) {
    for (int i = 0; i < MAX_TASKS; i++) {
        task_health[i].valid = false;
        task_health[i].last_update = 0;
        task_health[i].timeout_ms = 1000; // 默认1秒超时
    }
    // 设置不同任务的超时时间(根据实际需求调整)
    task_health[TASK_LED].timeout_ms = 500;
    task_health[TASK_SENSOR].timeout_ms = 2000;
    task_health[TASK_COMM].timeout_ms = 1000;
    task_health[TASK_DISPLAY].timeout_ms = 1500;
}

void Watchdog_TaskUpdate(TaskID id) {
    if (id < MAX_TASKS) {
        task_health[id].valid = true;
        task_health[id].last_update = HAL_GetTick();
    }
}

void Watchdog_Monitor(void) {
    uint32_t now = HAL_GetTick();
    bool all_ok = true;

    for (int i = 0; i < MAX_TASKS; i++) {
        // 检查标志是否有效且未超时
        if (!task_health[i].valid || (now - task_health[i].last_update) > task_health[i].timeout_ms) {
            all_ok = false;
            fault_task = i; // 记录故障任务
            break;
        }
    }

    if (all_ok) {
        // 所有任务健康,喂狗
        HAL_IWDG_Refresh(&hiwdg);
        fault_task = 0xFFFFFFFF;
    } else {
        // 有任务故障,不喂狗,等待看门狗复位
        // 可选:将fault_task写入备份寄存器(如RTC备份域)
        // 注意:这里不要做耗时操作,以免影响复位
    }
}

// 在main.c中,每个任务循环内调用Watchdog_TaskUpdate
// 例如LED任务:
void Task_LED(void) {
    while (1) {
        // 任务主体...
        Watchdog_TaskUpdate(TASK_LED);
        // 其他操作...
    }
}

// 在主循环或低优先级中断中调用Watchdog_Monitor
// 例如:
int main(void) {
    // 初始化...
    Watchdog_Init();
    // 创建任务...
    while (1) {
        Watchdog_Monitor();
        // 其他低优先级处理...
    }
}

注意事项

  • 超时时间设置:每个任务的超时时间应大于其最大执行周期,但小于看门狗超时时间。例如,若看门狗超时4秒,任务超时设为1-2秒,确保监控任务有足够时间检测并停止喂狗。
  • 监控任务执行频率:监控任务应至少比看门狗超时时间快2倍执行,例如看门狗4秒超时,监控每500ms执行一次。
  • 任务更新点:更新标志的位置要选在任务的关键路径上,避免在中断里更新(除非任务本身是中断驱动的)。
  • 故障记录:复位后通过读取备份寄存器或外部EEPROM,可以快速定位故障模块,极大缩短调试时间。
  • 多任务环境:若使用RTOS,可将监控任务设为最高优先级,但注意不要阻塞其他任务。

总结

模块化喂狗将看门狗从“死机检测器”升级为“任务健康监视器”。通过为每个关键任务分配健康标志,并统一在监控点喂狗,你不仅能防止系统崩溃,还能在故障发生时快速定位问题模块。这种架构简单、可扩展,适用于裸机或RTOS环境,是提升嵌入式系统稳定性的重要实践。