看门狗与系统容错:让设备更可靠的实用技巧
看门狗与系统容错:让设备更可靠的实用技巧
嵌入式产品一旦发出去,就很难像桌面程序那样随时打补丁。尤其是部署在工业现场、车载或者无人值守环境里的设备,出了故障你可能几个月都碰不到它。所以,工程师必须在设计阶段就把「挂了怎么办」这件事想清楚。
看门狗(Watchdog,简称 WDT)是最基础、也最容易被误用的一道防线。这篇文章不打算重复教科书里「定时喂狗」的套话,而是结合我在几个量产项目里踩过的坑,聊聊怎么把看门狗从「摆设」变成真正能在现场救命的容错机制。
一、看门狗到底在看什么
从原理上讲,就是一个倒计时定时器:软件在超时前「喂」它一次,计数就被清零;一旦软件忘了喂,或者卡在某个地方跑不动了,计数器归零,硬件就触发一次复位。
关键在于后半句——看门狗检测的是「主程序是否还能正常运转」,而不是「程序是否会复位」。很多人把喂狗语句塞进中断服务函数里,以为这样就算喂过了。实际上这是最典型的错误。
设想一下:主循环因为一个死锁卡住了,但是某个周期中断还在正常触发,中断里的喂狗语句照常执行。结果就是看门狗永远被喂饱,系统明明已经瘫痪,复位却迟迟不来。
正确的做法是:喂狗必须放在主循环的关键路径上,让它成为「主循环健康」的代言人。
二、一个容易复现的坑:在中断里喂狗
下面这段代码在不少老项目里都能看到,它是有问题的:
/* 错误示例:在定时器中断里喂狗 */
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
IWDG_ReloadCounter(); /* 危险:主循环死了也照样喂 */
feed_counter++;
}
}
假设主循环里有一段代码因为等待某个永远到不了的事件而卡死,此时定时器中断还在欢快地跑,看门狗计数器每次都被清零。设备从外表看一切正常,实际上业务逻辑已经停摆——这比直接死机更可怕,因为它不产生复位,也就不会触发任何报警,问题静默地蔓延。
改进的思路是引入「喂狗标志位」,让主循环来最终决定是否允许喂狗:
volatile uint32_t g_wdg_allow_feed = 0;
/* 定时器中断只负责计时,不喂狗 */
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
g_wdg_allow_feed = 1; /* 周期性给主循环一个「可以喂」的机会 */
}
}
/* 主循环,只有真正跑到了这里才喂狗 */
void main_loop(void)
{
while (1)
{
if (g_wdg_allow_feed)
{
g_wdg_allow_feed = 0;
IWDG_ReloadCounter();
}
run_task_a();
run_task_b();
run_task_c();
}
}
这样一改,喂狗被严格绑定到了主循环的执行上。只要主循环被卡住,喂狗标志位就再也等不到主循环来消费,看门狗必然超时复位。脱钩了「中断活性」和「主循环活性」这两件事,复位才真正有意义。
三、RTOS 场景下的看门狗设计
裸机还好说,上了 FreeRTOS 之后情况更复杂:可能有多个任务,每个任务都可能单独卡死。单纯在主循环喂狗已经不够了,因为主循环可能只负责创建任务,任务才是业务主体。
业内比较通用的做法是「多任务看门狗」,也就是给每个关键任务一个独立的「喂狗槽位」,由一个专门的看门狗监控任务统一检查:
#define MAX_MONITORED_TASKS 4
typedef struct {
TaskHandle_t task_handle;
uint32_t last_alive_tick;
uint32_t timeout_ticks;
uint8_t enabled;
} wdg_task_slot_t;
static wdg_task_slot_t g_wdg_slots[MAX_MONITORED_TASKS];
/* 各业务任务周期性调用,声明「我还活着」 */
void wdg_kick(uint8_t slot_id)
{
if (slot_id < MAX_MONITORED_TASKS && g_wdg_slots[slot_id].enabled)
{
g_wdg_slots[slot_id].last_alive_tick = xTaskGetTickCount();
}
}
/* 看门狗监控任务:所有被监控任务都健康时才喂硬件狗 */
void vWatchdogMonitorTask(void *pvParameters)
{
(void)pvParameters;
for (;;)
{
uint8_t all_ok = 1;
uint32_t now = xTaskGetTickCount();
for (uint8_t i = 0; i < MAX_MONITORED_TASKS; i++)
{
if (g_wdg_slots[i].enabled &&
(now - g_wdg_slots[i].last_alive_tick) > g_wdg_slots[i].timeout_ticks)
{
all_ok = 0;
break;
}
}
if (all_ok)
{
IWDG_ReloadCounter();
}
/* 不喂狗 = 让它超时复位,这是故意的 */
vTaskDelay(pdMS_TO_TICKS(100));
}
}
这套方案额外带来一个好处:当某个任务真的超时时,你可以先尝试一些恢复手段(比如重启该任务、记录现场、刷日志),再决定是否放任硬件狗复位。在生产环境里,「记录故障现场」往往比「快速复位」更有价值,尤其是那种偶发、难以复现的问题。
四、独立看门狗与窗口看门狗的区别
STM32 上常说的两类看门狗,很多人分不清该用什么场景,这里简单做一个对比。
独立看门狗(IWDG) 由专用的内部低速时钟(LSI)驱动,不依赖主时钟。就算主时钟崩了、PLL 失锁,IWDG 依然独立工作。它的特点是「只要别超时才喂就行,喂早了、喂晚了都可以」,适合做最后一道兜底——检测最粗粒度的「系统整体停滞」。
窗口看门狗(WWDG) 则有「窗口」限制:喂狗的时间不能太早,也不能太晚,必须落在某个时间窗内。它通常由系统时钟驱动,精度更高,适合检测那种「程序没死但跑飞了时序」的微妙故障。比如一段本该 1ms 执行完的中断,因为数据异常跑了 50ms,虽然还在喂狗,但喂的时间点已经偏离了预期窗口,WWDG 就会触发。
工程上的常见组合是:WWDG 用于早期预警和故障记录,IWDG 用于最终兜底复位。前者挂了通常还有机会保存现场、通知上位机;后者挂了就是硬复位,整个系统重新来过。
五、容器之外的容错:光有看门狗还不够
看门狗解决的是「程序跑飞 / 卡死」这一类问题。但现场设备面临的故障远不止这些,完整的容错设计至少还要覆盖下面几个层面:
1. 上电时序与掉电保护。 电源不稳导致的「半上电」状态,往往会引发 EEPROM 或者 Flash 的随机写入损坏。关键参数要做「双备份 + 校验和」,写一半掉电了,下次上电用另一份恢复,而不是读到一个乱码就用。
2. 关键数据的三重冗余。 对于像设备序列号、校准参数这类「丢了就废」的数据,可以考虑存三份,启动时投票表决。配合 CRC 校验,能在单份损坏时自动纠错。
3. 自检(POST)。 每次上电或者复位后,跑一遍快速自检:内存读写测试、外设应答确认、传感器通信 CRC 校验。很多「莫名其妙」的复位,其实在自检阶段就能定位到具体外设。
4. 复位原因跟踪。 复位之后第一件事,是把复位原因存起来——是看门狗复位、掉电复位、还是软件复位?这个信息对现场排查极其宝贵。STM32 的 RCC 里专门有寄存器可以区分复位源,复位后别急着清掉,先记日志再清理。
/* 记录复位原因 */
void log_reset_cause(void)
{
uint32_t cause = 0;
if (RCC_GetFlagStatus(RCC_FLAG_IWDGRST) != RESET)
cause |= RESET_CAUSE_IWDG;
if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET)
cause |= RESET_CAUSE_POR;
if (RCC_GetFlagStatus(RCC_FLAG_SFTRST) != RESET)
cause |= RESET_CAUSE_SOFT;
save_to_backup_ram(RESET_CAUSE_SLOT, cause);
RCC_ClearFlag(); /* 记录完之后再清标志 */
}
六、几个实操建议
结合这些年在量产项目里的经验,我总结几条可以直接落地的准则:
- 喂狗只放在主循环关键路径,绝不放进任何中断服务函数里。
- RTOS 用「多任务看门狗 + 监控任务」,任何关键任务单独卡死都能触发复位或恢复。
- 复位原因必须记录,否则「设备偶尔重启」这种 bug 你会查到天荒地老。
- 看门狗超时时间要结合最坏执行时间来定,留足余量但别太宽松——太短会误复位,太长则故障恢复太慢。
- 关键参数做冗余 + 校验,看门狗只能救「跑飞」,救不了「数据损坏」。
最后说一句可能会让你不太舒服的话:如果你从来没有在生产现场经历过设备半夜死机、第二天被客户连环夺命 call 的经历,那你可能很难真正理解看门狗的价值。 它不是写代码时随便抄的一段示例,而是在真实世界里帮你把「不可接受的宕机」转成「一次可恢复的复位」的最后保险。
下次写驱动,记得回头看看你的喂狗语句放对了没有——也许它现在正躺在某个中断里,假装自己在保护系统。