第4章:软件看门狗实现:MCU内部看门狗配置、喂狗策略、多任务环境下的看门狗管理

各位工程师朋友,咱们接着聊看门狗。上一章讲了硬件看门狗的原理,这一章咱们深入软件层面——说白了,就是怎么把MCU内部那个看门狗外设用起来,让它真正干活,而不是摆设。

我见过不少项目,看门狗是开了,但喂狗喂得毫无章法。结果呢?该复位的时候不复位,不该复位的时候乱复位。嗯,这比没看门狗还可怕。

4.1 MCU内部看门狗配置——别让初始化变成埋雷

先说说配置。不同MCU的看门狗外设大同小异,但有几个关键参数你必须搞清楚:

  • 时钟源:是独立RC振荡器还是系统时钟分频?独立RC通常更可靠,但精度差。
  • 超时时间:从最后一次喂狗到复位触发的时间窗口。
  • 窗口模式:有些MCU支持早期喂狗触发复位,防止你喂得太勤快。
  • 调试模式行为:仿真暂停时,看门狗是否继续跑?

我个人习惯,在项目初期就把看门狗配置封装成一个独立模块。别等到最后才加,那时候改起来牵一发动全身。

核心原则:看门狗配置必须在系统时钟稳定之后、任务调度启动之前完成。我曾经见过有人把看门狗初始化放在main函数第一行,结果时钟还没稳,看门狗已经开始倒计时了——系统反复复位,连调试器都连不上。

来看一个典型的STM32 IWDG配置代码:

// 独立看门狗初始化
void IWDG_Init(uint8_t prescaler, uint16_t reload) {
    // 1. 使能写访问
    IWDG->KR = 0x5555;
    
    // 2. 配置预分频器(4分频 ~ 256分频)
    IWDG->PR = prescaler;
    
    // 3. 设置重装载值(12位,最大4095)
    IWDG->RLR = reload;
    
    // 4. 等待寄存器更新
    while(IWDG->SR & IWDG_SR_PVU);
    while(IWDG->SR & IWDG_SR_RVU);
    
    // 5. 启动看门狗(写入0xCCCC)
    IWDG->KR = 0xCCCC;
}

注意:IWDG一旦启动,除非系统复位,否则无法停止。所以调试阶段我建议先用窗口看门狗(WWDG)或者软件模拟看门狗,等逻辑稳定了再切到硬件IWDG。

4.2 喂狗策略——不是随便找个地方喂就完事

喂狗看似简单,写个寄存器就行。但喂狗的位置、频率、条件,直接决定了看门狗能不能起到保护作用。

我总结了几种常见的喂狗策略,你根据实际情况选:

策略名称 实现方式 适用场景 风险
主循环喂狗 在while(1)末尾喂狗 单任务、简单轮询系统 子任务死锁但主循环仍在跑时失效
定时器喂狗 在定时器中断中喂狗 需要精确时间窗口的系统 中断正常但主逻辑卡死时无法检测
分步喂狗 多个关键节点各喂一部分 多任务、复杂流程 实现复杂,容易遗漏
心跳喂狗 独立任务周期性喂狗 RTOS多任务环境 需要任务间通信机制

我个人最不推荐的是「定时器喂狗」。为什么?你想想看,中断还能正常触发,但主循环可能早就卡死在某个死循环里了。这种喂狗方式,说白了就是掩耳盗铃。

我的经验:在医疗设备中,我通常采用「分步喂狗 + 主循环喂狗」的组合策略。每个关键任务完成后设置一个标志位,主循环检查所有标志位都置位后才喂狗。这样任何一个任务卡死,看门狗都会复位系统。

4.3 多任务环境下的看门狗管理——RTOS的喂狗艺术

到了RTOS环境,事情就复杂了。你不可能让每个任务都去喂狗,那样会乱套。但你又不能只让空闲任务喂狗——万一高优先级任务卡死了,空闲任务还在跑,看门狗永远不会复位。

我常用的方法是「看门狗任务 + 任务监控表」:

// 任务监控表结构
typedef struct {
    TaskHandle_t taskHandle;    // 任务句柄
    uint32_t lastHeartbeat;     // 上次心跳时间戳
    uint32_t timeoutMs;         // 超时阈值
    uint8_t  status;            // 0:正常 1:超时
} TaskMonitor_t;

// 看门狗监控任务
void WatchdogTask(void *param) {
    while(1) {
        // 遍历所有被监控任务
        for(int i = 0; i < TASK_MONITOR_COUNT; i++) {
            uint32_t now = GetSysTick();
            if((now - monitorTable[i].lastHeartbeat) > monitorTable[i].timeoutMs) {
                // 任务超时,记录错误但不立即复位
                LogError("Task %s timeout!", pcTaskGetName(monitorTable[i].taskHandle));
                monitorTable[i].status = 1;
            }
        }
        
        // 所有任务都正常?喂狗!
        if(AllTasksAlive()) {
            IWDG_Feed();
        }
        
        vTaskDelay(pdMS_TO_TICKS(100)); // 每100ms检查一次
    }
}

每个被监控的任务,需要在它的主循环里定期更新自己的心跳时间戳:

void SensorTask(void *param) {
    while(1) {
        // 任务主体逻辑
        ReadSensor();
        ProcessData();
        
        // 更新心跳
        monitorTable[SENSOR_TASK_INDEX].lastHeartbeat = GetSysTick();
        monitorTable[SENSOR_TASK_INDEX].status = 0;
        
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

关键点:看门狗任务本身也要被监控。我通常让看门狗任务的优先级设为中等,既不会被高优先级任务完全抢占,也不会影响关键实时任务。另外,看门狗任务的栈空间要留足——我遇到过因为栈溢出导致看门狗任务自己先挂了的惨案。

4.4 避坑指南——我曾经踩过的雷

做医疗设备这些年,看门狗相关的坑我踩了不少。挑几个典型的说说:

  • 调试阶段忘记关闭看门狗:单步调试时,看门狗一直在跑。你刚设个断点喝口水,回来发现系统复位了。我现在的做法是:在调试配置里加个宏,DEBUG模式下看门狗初始化直接跳过。
  • 喂狗函数放在中断里:中断频繁触发,看门狗永远喂得饱饱的。但主循环早就卡死了。这种bug最难查,因为看门狗不会复位,你只能靠串口日志慢慢分析。
  • 窗口模式配置错误:有些MCU的窗口看门狗要求喂狗时间必须在窗口内。太早喂或者太晚喂都会触发复位。我刚开始用的时候没仔细看手册,结果系统每隔几秒就复位一次,查了两天才发现是喂狗喂得太勤快了。
  • 低功耗模式下看门狗还在跑:进入休眠前没暂停看门狗,结果休眠期间看门狗超时复位,设备莫名其妙重启。这个在电池供电的医疗设备里特别坑。

最后提醒一句:看门狗是最后一道防线,不是第一道。别指望看门狗能解决所有问题。该做的异常处理、状态检查、数据校验,一个都不能少。看门狗只负责在你没兜住的时候,把系统拉回来。

好了,这一章就到这里。下一章咱们聊聊「看门狗在IEC 62304中的认证要求」,这个在医疗设备认证里是必考题,到时候我会结合我实际过认证的经验来讲。