看门狗为什么必要

嵌入式设备长期无人值守,一颗静电、一次电压跌落、一段被踩坏的栈,都可能让程序跑飞或者卡死在某个死循环里。看门狗(Watchdog)就是最后一道防线:它是一颗独立运行的计数器,主程序必须定期"喂狗"清零,一旦超时没有喂狗,它就强制复位整个系统。

没有看门狗的设备,故障往往是永久性的:一个卡死的中断、一个等待永不放下的信号量,会让产品停在坏状态,直到人工断电重启。有了看门狗,异常最多让设备重启一次,配合上电自检与状态恢复,就能做到自愈。

本文核心知识点
  • IWDG 独立不受主时钟影响的自由运行计数器。
  • WWDG 窗口必须在时间窗口内喂狗。
  • 喂狗策略主循环喂 + 任务心跳校验。
  • 恢复治理复位原因记录与故障降级。

看门狗本质上是"信任但验证"的工程手段:它假设软件一定会出 bug,所以用硬件兜底。设计良好的看门狗方案,不是简单在主循环里喂一下狗,而是把系统健康度、任务活性和关键路径的执行频率都纳入监护范围。

嵌入式看门狗配图:深蓝渐变背景上带眼睛图案的芯片,上方悬浮盾牌图标,两侧为心电图式波形的扁平科技插画
看门狗像一只不眨眼的眼睛,盯着程序的心跳。

独立看门狗 IWDG

独立看门狗由独立的低速时钟驱动,即使主时钟因为外部晶振故障停摆,它依然在走。它的寄存器只有写保护能挡住误操作,一旦启动几乎关不掉,非常适合做最后的兜底。

  • 时钟源:通常来自 LSI 低速内部时钟,约 32kHz。
  • 分频:可配置 4 到 256 分频,控制计数节拍。
  • 重装载值:12 位,决定超时窗口长度。
  • 超时计算:超时时间约等于分频系数乘重装载值再除以时钟频率。

把超时设成主循环周期的几倍是常见做法:既不会因为偶发抖动误复位,又能容忍主循环短暂卡顿。注意 IWDG 一旦使能,软件就无法关闭,只有复位才能重新初始化,所以调试阶段要谨慎。

窗口看门狗 WWDG

窗口看门狗更进一步:它规定只能在某个时间窗口内喂狗,喂得太早(计数器还没走到下窗口)或太晚(超过上窗口)都会复位。这能抓住"程序还在跑但已经失去正确节奏"的情况——比如死循环仍然在执行,主循环照常喂狗,但某个周期性任务已经停了。

// STM32 HAL 初始化窗口看门狗 WWDG
WWDG_HandleTypeDef hwwdg;
hwwdg.Instance = WWDG;
hwwdg.Init.Prescaler = WWDG_PRESCALER_8;
hwwdg.Init.Window = 0x50;                 // 上窗口值,必须在此前刷新
hwwdg.Init.Counter = 0x7F;                // 计数器初值
hwwdg.Init.EWIMode = WWDG_EWI_ENABLE;     // 提前唤醒中断
HAL_WWDG_Init(&hwwdg);

// 喂狗必须在窗口内完成
HAL_WWDG_Refresh(&hwwdg);

WWDG 还带一个提前唤醒中断(EWI):在真正复位之前先通知软件,让软件有机会记录现场、保存关键参数,再优雅地重启。提前唤醒中断里的处理要短而快,不能在里面做耗时的任务,否则窗口一到照样复位。

喂狗策略与任务监护

最简单的喂狗是在主循环里定时刷新,但这只能证明主循环活着,证明不了业务任务活着。更可靠的策略是任务心跳监护:每个关键任务周期性更新自己的心跳计数,喂狗前检查所有心跳是否都在预期范围内,缺一个就不喂,让看门狗把系统拉回来。

// 任务心跳 + 统一喂狗
volatile uint32_t task_beats[4] = {0};

void task_a(void) { task_beats[0]++; /* 业务逻辑 */ }
void task_b(void) { task_beats[1]++; /* 通信处理 */ }
void task_c(void) { task_beats[2]++; /* 传感器采集 */ }
void task_d(void) { task_beats[3]++; /* 显示刷新 */ }

void wdg_monitor(void) {
  for (int i = 0; i < 4; i++) {
    if (task_beats[i] == 0) return;   // 有任务从未跑过,不喂狗
  }
  HAL_IWDG_Refresh(&hiwdg);            // 全部健康才刷新
}

把喂狗点放在任务调度器或者协议栈主循环里,比放在中断里可靠得多。中断里喂狗是最常见的反面教材:中断正常而主流程卡死时,看门狗形同虚设。另外喂狗代码要尽量放在单一线程,避免多线程竞态导致喂狗时序漂移。