MCU的看门狗到底在防什么

汇鼎金融 26-09-26

《极海芯得》系列内容为用户使用极海系列产品的经验总结,均转载自21ic论坛极海半导体专区,全文未作任何修改,未经原文作者授权禁止转载。

前言

我记得自己第一次接触看门狗的时候,心里有一个很大的问号。

教科书上写着:程序要定期喂狗,否则系统会复位。好,我照做了,在 main 循环末尾加了一行喂狗代码,编译通过,板子跑起来了。但那个问号一直卡在我脑子里——我的程序好端端的,为什么要喂狗?程序会出什么问题?谁在搞破坏?

一、程序其实很脆弱

我们写代码的时候,有一种错觉:代码是我写的,逻辑我想清楚了,编译也通过了,烧进芯片就应该老老实实跑。

我在学校里做课程设计的时候就是这么想的。在 IDE 里仿真,跑得好好的,觉得万事大吉。直到后来工作时,板子扔到客户现场,才发现——IDE 里的世界和真实世界,完全是两回事。

你的电脑运行在什么环境里?恒温、恒湿、电磁屏蔽良好,操作系统帮你管着内存,ECC 纠错帮你兜着数据。但 MCU 呢?它可能待在一个工业电机的控制板上,旁边就是一台几十千瓦的变频器;可能塞在汽车发动机舱里,120 度高温加剧烈振动;可能挂在户外电线杆上,雷雨天的大气电磁扰动全扛着。

在这种环境里,MCU 的程序远没有我们以为的那么安全。

1.1 电磁干扰:PC 指针跑偏

这是我觉得最隐蔽、也最让人后背发凉的一种威胁。

MCU 执行程序的方式,是程序计数器(PC 指针)指向当前指令地址,逐条取指令执行。PC 指针就像读书时手指指着的那一行——正常情况下,手指一行一行往下移,稳稳当当。

但强电磁干扰可以通过电源线、信号线、甚至空间辐射,耦合到 MCU 内部,篡改总线上的数据。如果恰好篡改了 PC 指针的值呢?

举个例子:程序本来在地址 0x08000100 执行一条正常的赋值指令,一个电磁脉冲过来,PC 指针变成了 0x20003F40。这是一个完全随机的地址。MCU 会老老实实去那个地址取指令——那个地址里存的可能根本不是代码,而是一段传感器数据、一块未初始化的 RAM、甚至一个被擦除了一半的 Flash 扇区。

关键点来了:MCU 不会报错,不会停机。从硬件层面看,它无法区分"正常指令"和"被篡改出来的乱码指令"。它只会忠实地执行。程序就这么跑飞了。

跑飞之后会怎样?我见过几种情况:

跑到无效指令区,触发 HardFault,系统直接卡死

跑到 Flash 空白区,执行全 0xFF,行为完全不可预测

跑到另一段正常代码区,但寄存器状态和栈内容全是错的——这种情况最可怕,表面上系统还在跑,但输出全是垃圾数据,而且你根本查不出问题在哪

跑到外设配置寄存器的写入逻辑里,把关键外设的配置改了,硬件行为开始诡异

有没有遇到过这种事:设备运行了几个月一直好好的,突然某天 LED 闪烁频率变了,传感器读数跳了,电机转速不稳了。你把代码翻来覆去看了三遍,没找到 bug。因为代码本身确实没有 bug——是运行环境把程序搞坏了。

1.2 栈溢出:行为失控

栈这个东西,每次函数调用的时候用来存局部变量、返回地址、寄存器现场。MCU 的 RAM 本来就小,几 KB 到几百 KB,栈空间更是寸土寸金。我早期写代码的时候,随手就定义一个 uint8_t buf[1024] 当局部变量,觉得没什么。后来才知道,这在栈空间只有 4KB 的芯片上意味着什么。

栈溢出的触发条件:

递归调用层级太深

局部变量数组开得太大

中断嵌套层数过多

多任务共享栈空间不够

溢出时,数据写到栈边界以外的内存区域,悄悄篡改了别的变量。更要命的是,函数返回地址也存在栈里。如果栈溢出恰好覆盖了返回地址,函数执行完要返回的时候,MCU 会跳到一个被篡改的地址——和电磁干扰导致 PC 跑偏的效果一模一样。

栈溢出最折磨人的地方在于:它不一定每次都发生。正常工况下栈使用量刚好在边界附近,只有某些特殊工况(比如传感器突然返回了一个异常大的数据包)才会触发。这种间歇性故障,排查起来简直要命。

1.3 死循环:系统僵死

这个好理解。程序某处有个 while(1) 等条件成立,但条件***不会成立了——硬件坏了、通信对端挂了、某个标志位被电磁干扰清零了。程序就死在循环里,再也出不来。

还有一种是逻辑 bug:状态机漏写了 break,进入错误状态无限循环;指针运算出错,数组越界访问。

死循环和跑飞的区别:跑飞是 PC 去了不该去的地方,死循环是 PC 困在一个地方出不来。结果一样——系统正常功能全部停了。该采样的不采样了,该输出的不输出了,该通信的不通信了。

1.4 电源波动:灰色地带

MCU 工作电压有允许范围,比如 3.3V ±10%。负载突变、线路压降、电源模块不稳定,都可能导致电压短暂跌落。

这里有个反直觉的事实:完全掉电反而安全。MCU 复位,从头开始执行,一切清零。真正危险的是电压跌落到"灰色地带"——MCU 没有完全停止工作,但内部逻辑已经不可靠了。在这个区间,MCU 可能取到错误指令、写入错误数据、漏掉该执行的中断。

这种跌落可能只有几微秒到几毫秒,用示波器都不一定抓得到,但足以让程序跑偏。

1.5 软件缺陷:迟早会暴露

最后说说软件本身。说句实话,再优秀的程序员也不敢保证零 bug。复杂的嵌入式系统动辄几十万行代码,边界条件、竞态条件、内存操作的返回值检查,总有覆盖不到的地方。

这些 bug 在仿真环境下可能不会触发。但真实产品运行几个月、几年后,总会被某些"巧合"暴露出来:计数器运行 497 天后溢出、DMA 在特定时序下丢了一个字节、浮点运算在特定输入下产生了 NaN。

软件缺陷和硬件环境威胁叠加在一起,就是 MCU 系统面临的完整风险图谱。

二、看门狗的本质

理解了上面这些威胁,再看门狗就豁然开朗了。

看门狗的核心思想,就是给系统一个心跳检测机制。

我喜欢用一个比喻:想象你一个人值夜班,旁边坐着一个保安。保安手里有个倒计时器,你必须每隔一段时间拍他一下肩膀,告诉他"我还醒着"。如果你忘了拍——可能是睡着了,可能是晕倒了,可能是出了什么事——倒计时归零,保安就把你摇醒(复位)。

程序也是一样。看门狗就是一个独立的倒计时器,程序必须定期去"喂狗"(重置倒计时器)。如果因为任何原因——跑飞了、死循环了、栈溢出了、被电磁干扰搞懵了——没能按时喂狗,倒计时归零,看门狗直接复位整个 MCU。复位之后,PC 指针回到复位向量地址,所有外设回到初始状态,系统从零开始运行。

这个机制最让我觉得巧妙的地方在于:它不需要知道程序出了什么问题。它不检测 PC 是否跑偏,不检测栈是否溢出,不检测是否死循环。它只检测一件事——程序有没有在规定时间内执行到喂狗代码。没有就复位。

这是一种"黑盒"式的保护策略。我不关心你内部发生了什么,我只关心你的行为是否正常。就像医生不需要知道你身体里每个细胞的状态,只需要检查心跳、血压、体温。

三、硬件怎么工作的

主流 MCU 的看门狗,硬件上就是一个独立的计数器。我把它的关键特征拆开来说:

独立性:计数器由独立的 LSI 低速内部 RC 振荡器驱动(约 32kHz 或 40kHz),不依赖主时钟。即使外部晶振停振了,看门狗依然能正常工作。这一点至关重要——如果看门狗本身也会被威胁影响,那它就失去了保护的意义。

递减计数:从预设初始值往下递减,减到 0 时产生复位信号。

喂狗操作:在计数器减到 0 之前,向特定寄存器写入刷新命令,计数器重新从初始值开始递减。这就是"喂狗"。

超时时间:从初始值减到 0 的时间。太短了,正常程序还没来得及喂狗就误复位;太长了,出了问题要等很久才复位。

举个例子:超时时间设 1 秒,主循环每 500ms 跑一遍并在末尾喂狗。正常情况下每 500ms 喂一次,永不溢出。程序跑飞或死循环后,超过 1 秒没喂狗,触发复位。

四、独立看门狗 IWDG

一般 MCU 提供两种看门狗:独立看门狗(IWDG)和窗口看门狗(WWDG)。先说 IWDG。

工作方式

非常简单:

配置超时时间(如 1 秒)

计数器开始递减

在超时前的任何时刻喂狗,计数器重新加载初始值

超过超时时间没喂狗,计数器减到 0,触发复位

IWDG 的特点:喂狗窗口很宽。第 100ms 喂或第 900ms 喂都行,只要不超时。

为什么喜欢用它?

配置简单:只需设置超时时间,其他基本不用管

使用灵活:喂狗时间点没有严格限制,对程序架构要求低

可靠性高:独立 LSI 时钟源,系统时钟挂了也能工作

局限

只能检测"程序没有按时喂狗"。如果程序跑飞后恰好跳到另一段包含喂狗操作的代码里,IWDG 会被正常喂狗,不会触发复位。换句话说,IWDG 的保护粒度是时间,不是路径。

五、窗口看门狗 WWDG

WWDG 在 IWDG 基础上加了一个维度:不仅要求喂狗,还要求在正确的时间窗口内喂狗。

工作方式

WWDG 有两个关键计数值:

窗口值(Window Value):上限值,如 0x50(80)

初始值(Reload Value):起始值,如 0x7F(127)

计数器从 0x7F 开始递减,喂狗操作有一个"窗口":

不能太早,也不能太晚

为什么"不能太早"?

我第一次看到 WWDG 的这个设计时,也觉得奇怪:喂狗喂早了有什么不好?后来想明白了。如果程序在预期之前就执行到了喂狗代码,说明执行节奏出了问题:

程序跑飞了,恰好跳到包含喂狗指令的区域

某个运算因为数据异常提前结束了

中断处理出了问题,主循环节奏被打破

WWDG 通过限制喂狗窗口,确保程序不仅"活着",而且是"以正确的节奏活着"。这是一种更高层次的保护。

提前中断(EWI)

WWDG 还有一个 IWDG 没有的能力:可以在计数器减到下限之前先产生一个 Early Wakeup Interrupt,给最后一次喂狗的机会。这个中断很有用。我们可以在中断服务程序里做"临终抢救"——保存关键数据到非易失性存储器、记录故障日志、尝试恢复操作。抢救成功就喂狗继续跑,失败就等计数器归零复位。

典型场景:设备正在写入 Flash,突然复位可能导致数据损坏。利用 EWI 确保 Flash 写入完成或回滚。

时钟源

WWDG 使用 APB1 总线时钟(PCLK1),精度远高于 LSI(几十 MHz vs 32kHz),但如果 APB1 时钟出问题,WWDG 也会受影响。这是可靠性上的一个妥协。

六、IWDG 与 WWDG 怎么选

我自己的经验是:大多数场景 IWDG 就够用了。配置简单,独立时钟源保证高可靠性,能覆盖绝大部分异常。

以下场景我会考虑 WWDG:

实时控制系统(电机控制、电源控制)——执行周期必须严格稳定

安全关键系统(医疗设备、汽车电子)——需要更细粒度的监控

RTOS 多任务系统——检测优先级反转等导致的关键任务节奏异常

在一些高可靠性系统中,两者可以同时使用:WWDG 检测执行节奏,IWDG 作为最后防线防止系统时钟故障导致保护失效。

七、踩过的坑

看门狗原理简单,但用起来有不少讲究。以下是我踩过的坑和见到别人踩过的坑。

7.1 喂狗放在中断里

这大概是最常犯的错误。

定时器中断是周期性触发的,只要系统时钟在跑,中断就会按时来。把喂狗放在中断里,等于把喂狗和程序逻辑解耦了。主程序跑飞了,中断还在正常喂狗,看门狗形同虚设。

// 错误示例:在中断里喂狗

void Timer_IRQHandler(void)

{

Watchdog_Feed(); // 错误!中断会一直执行,即使主程序跑飞

}

int main(void)

{

while (1) {

Task_Sensor_Read();

Task_Communication();

Task_Control();

Task_Display();

// 主程序跑飞了,中断还在正常喂狗

}

}

正确做法:喂狗放在主循环中,所有关键任务执行完成之后才喂。

// 正确示例:在主循环末尾喂狗

int main(void)

{

while (1) {

Task_Sensor_Read();

Task_Communication();

Task_Control();

Task_Display();

Watchdog_Feed(); // 所有任务完成后才喂狗

}

}

任何一个任务卡住,狗就不会被喂,系统复位。

7.2 超时时间设不对

三条原则:

大于主循环完整周期:主循环 200ms,超时至少 500ms 以上

按最坏情况设置:特殊工况(通信数据量大、传感器响应慢)可能需要 500ms

不能太长:太长意味着出问题后要等很久才复位

经验值:主循环最坏执行时间的 2~3 倍。

7.3 到处都喂狗

有些人在每个 if 分支里都放喂狗操作,觉得"保险"。结果程序跑飞到任何一段代码都可能意外喂狗,看门狗的保护效果大打折扣。正确做法是,只在主循环正常出口处喂狗,让喂狗成为"程序正常执行完毕"的标志。

7.4 调试时被看门狗搞疯

MCU 的 IWDG 一旦使能就不能通过软件关闭——这是有意设计,防止程序跑飞后恰好执行到关闭看门狗的指令。但调试时就痛苦了,开了看门狗忘了喂狗,或者断点停留时间超过超时时间,MCU 不断复位,连调试的机会都没有。

建议做法:调试时先不开看门狗,准备测试可靠性时再开启;或者把超时时间设长(几秒),调试时不容易超时。

7.5 喂狗序列被中断打断

喂狗通常需要写入特定值序列(如先写 0xAAAA,再写 0x5555),防止意外写操作触发喂狗。如果写入 0xAAAA 后、写入 0x5555 前被中断打断,可能导致喂狗失败。软件层面应避免在中断和主循环中都进行喂狗操作。

八、看门狗不是万能的

聊完了看门狗的好处,也得说说它的局限。

8.1 只能复位,不能修复

看门狗检测到异常后的处理是复位。如果根本原因没消除(硬件故障、持续的强电磁干扰),系统会陷入"复位→出问题→复位"的死循环。

所以看门狗是最后一道防线,不是唯一防线。一个可靠的嵌入式系统,应该在软件架构层面就做好防御:

关键数据做冗余校验(CRC、奇偶校验)

重要状态变量做"黄金副本"(存两份,定期比对)

外部输入做合法性检查

关键外设做回读验证

使用 MPU 防止栈溢出越界

关键函数做执行时间监控

8.2 无法检测"静默错误"

前面提到过:程序跑飞后恰好跳到另一段正常代码且包含喂狗操作,看门狗不会触发复位,但行为已经错了。

检测这种错误需要更高级的机制:

程序流监控(PCF):在关键代码段设置标记,主循环检查标记是否按预期设置

代码签名校验:定期校验 Flash 中代码的 CRC 或哈希值

结果合理性检查:对输出做范围检查(如 PWM 占空比超出合理范围说明算法异常)

8.3 自身也可能出问题

LSI 频率偏差大(±5%~±30%),超时时间不准确

配置寄存器被意外修改(强电磁干扰下概率低但存在)

复位信号生成电路故障

高可靠性系统中,看门狗是多层防护策略中的一层,不是唯一手段。

九、一个配置实例

以 APM32F103 为例,LSI 频率 32kHz,预分频 64,重装载值 800:

超时时间 = (预分频 × 重装载值) / LSI频率

= (64 × 800) / 32000

= 1.6 秒

void IWDT_Init(void)

{

IWDT_EnableWriteAccess(); // 解锁写保护

IWDT_ConfigDivider(IWDT_DIVIDER_32); // 预分频64

IWDT_ConfigReload(800); // 重装载值800

IWDT_Refresh(); // 首次喂狗

IWDT_Enable(); // 使能看门狗

}

int main(void)

{

System_Init();

IWDT_Init();

while (1)

{

Task_Sensor_Read();

Task_Communication();

Task_Control();

Task_Display();

IWDT_Refresh(); // 喂狗

}

}

核心要素:初始化配置、首次喂狗、主循环定期喂狗。

十、回头看

回到最开始的那个问题:看门狗到底在防什么?

看门狗防的是"程序偏离预期行为"这件事本身。

它不关心偏离的原因——电磁干扰、栈溢出、死循环、电源波动还是软件 bug。它只关心一件事:程序有没有按预期节奏执行。没有就复位重来。

这是一种"兜底"式保护策略。它不能替代良好的软件设计和硬件防护,但它是系统可靠性的最后一道防线。就像汽车的安全气囊——不会靠它来开车,但当所有其他安全措施都失效的时候,它可能是救命的。

回头看,我觉得理解看门狗最重要的不是知道"要喂狗",而是理解我们的程序在真实环境中面临的威胁,以及如何通过合理的软件架构和看门狗配置,让系统在异常情况下安全恢复。

注:文章作者在原帖中提供了代码,有需要请至原文21ic论坛

原文地址:https://bbs.21ic.com/icview-3528596-1-1.html?_dsign=7a01157e或点击下方阅读原文跳转