## 为什么你的看门狗形同虚设? 在嵌入式开发中,看门狗(Watchdog Timer)是防止程序跑飞或死循环的硬件机制。然而,很多开发者只是简单地在主循环末尾或中断里周期性喂狗,这导致看门狗只能检测到“CPU完全卡死”的极端情况,而无法发现任务级故障——比如某个模块陷入死循环但中断仍正常触发,或者某个任务被饿死但主循环依然在跑。 **核心问题**:传统喂狗方式缺乏“任务健康”信息,看门狗复位后你依然不知道系统为何崩溃。 ## 看门狗工作原理与喂狗的本质 看门狗本质上是一个递减计数器,当它减到0时触发系统复位。喂狗(Kick/Feed)就是重新装载计数值,防止复位。 关键点: - 喂狗是**系统健康声明**,不是简单的时间填充。 - 喂狗时机应反映“关键任务是否按预期执行”。 - 喂狗间隔必须小于看门狗超时时间,但又要留出足够余量。 ## 常见喂狗误区 1. **在定时器中断里喂狗**:中断优先级高,即使主循环死机,中断仍可喂狗,看门狗永远不触发。 2. **主循环末尾统一喂狗**:无法区分哪个任务卡死,且若某个任务耗时过长,可能导致误复位。 3. **喂狗代码散落各处**:难以维护,且容易遗漏。 ## 模块化喂狗架构设计 ### 核心思想 将系统划分为多个独立任务(模块),每个任务维护自己的“健康标志”。主循环或专用监控任务检查所有任务标志,只有全部正常时才喂狗。若某个任务超时未更新标志,则禁止喂狗,让看门狗复位,同时记录故障模块。 ### 架构图(文字描述) ``` 任务A -> 更新标志A 任务B -> 更新标志B 任务C -> 更新标志C ... 监控任务 -> 检查所有标志 -> 全部OK? -> 喂狗 : 不喂狗(并记录故障) ``` ### 实现步骤 1. **定义任务健康结构体**:每个任务对应一个标志和超时阈值。 2. **任务入口/出口更新标志**:在任务的关键执行点(如循环开始或完成)更新标志。 3. **监控任务统一喂狗**:周期性地检查所有标志,若全部有效则喂狗,否则记录故障并等待复位。 4. **故障记录**:在复位前将故障模块ID存入备份寄存器或EEPROM,便于重启后诊断。 ## 完整代码示例(基于STM32 HAL库) ```c // watchdog_task.h #ifndef WATCHDOG_TASK_H #define WATCHDOG_TASK_H #include #include #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 ``` ```c // 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环境,是提升嵌入式系统稳定性的重要实践。