看门狗定时器(WDT,Watchdog Timer)大概是我做嵌入式这行十几年里,见过最被低估、也最容易被糊弄过去的一个外设。几乎每个项目都会在初始化代码里写上几行“开狗”,然后就再也没人管它;等到现场设备隔三差五莫名其妙重启、或者更糟糕地整机僵死不动,才会有人翻回来看一眼那颗“狗”到底有没有在干活。这篇文章不谈某个具体型号的器件手册,只谈这套机制本身的逻辑:它为什么这样设计、超时时间怎么算、喂狗位置为什么是整套方案里最难写对的一行代码、现场遇到复位或死机该怎么一层层剥开。刚入行、只会照着例程抄初始化的人能从中拿到一套能直接落地的方法;已经带过几轮量产、被现场问题磨过的老手,也能在喂狗架构和排障表格里找到点共鸣。
1. 先搞清楚这只狗到底看的是什么门
1.1 一次现场死机给我的教训
早年做一款工业采集终端,主控跑着一个不算复杂的主循环,采集、计算、串口上报轮着来。当时为了偷懒,看门狗是在一个 1ms 的定时器中断里喂的——中断优先级很高,中断服务程序只有一行清计数器,稳得一塌糊涂。实验室里连续跑了两周没问题,然后第一批设备上现场,一个月内陆续有设备“假死”:屏幕还在刷新,指示灯正常闪,但数据不传了,远程指令也没反应。
后来抓回来分析,问题出在串口接收处理里一个环形缓冲区的索引回绕判断,某个极端时序下缓冲区被写满,主循环卡在一个 while 里原地打转。主循环已经死了,但定时器中断还活得好好的,它依然尽职尽责地每 1ms 去清一次看门狗计数器。狗没叫,设备看起来“活着”,实际上业务逻辑已经停止运行。这是一个经典到不能再经典的坑:把看门狗当成“系统有没有断电”的检测器,而它真正应该检测的是“关键路径有没有跑完”。
这件事之后我改了整个团队的喂狗规范:喂狗点必须放在主循环的收尾处、或者多任务系统里所有关键任务的健康度汇总通过之后再执行,绝不允许在中断里无条件喂。代价是代码结构要重排,好处是此后三年,同样类型的静默死机再没有出现过一次没被复位带走的。
1.2 WDT 的三个组成部分和它的判决逻辑
从结构上看,任何一颗看门狗定时器都逃不出三块:时钟源、递减计数器、复位输出。时钟源给计数器提供节拍,计数器从重装载值开始往下数,数到零就触发复位信号;软件在它数到零之前把计数器重新装回去,这个过程就是“喂狗”。逻辑简单到几乎没什么可讲的,但正是这份简单让它成为整个系统里唯一不依赖软件状态的可信裁判——CPU 跑飞了、栈溢出了、死循环了、中断把优先级搞乱了,这些都影响不到它,它只认一个事实:你有没有按时来清我。
严格来说,绝大多数 WDT 的计数器是递减型的,而不是递增。递减的好处是重装载值直接就是超时的计数值,软件算时间直观;同时“数到零”这个事件在硬件上比“数到比较值相等”更容易做得可靠。重装载动作也不是直接写寄存器就完事,通常要先往一个“键寄存器”写特定的解锁序列,再写重装载寄存器,最后再写一次喂狗键值,这一串流程是为了防止跑飞的程序在乱翻内存时顺手把狗的配置改掉了。
值得强调的是,看门狗触发的是复位,不是中断。这是个常被误解的点。有些芯片确实可以配置成“先产生一个早期预警中断,再超时才复位”,那个中断是给你留的抢救窗口,比如在复位前把关键现场变量写进备份寄存器或者 Flash 里,方便下次开机做故障分析。但预警中断本身不是目的,目的是让你不要走到复位那一步。
1.3 边界:它能救的和救不回来的
看门狗能救的场景,主要是软件性的失控:任务死锁、非法内存访问导致的跑飞、指针乱跳、中断风暴、Flash 擦写过程被打断。这些情况下系统状态是“乱的”,复位回到初始状态反而是最安全的处理。
它救不回来的场景同样值得列清楚:一是硬件已经损坏,比如电源不稳、晶振停振、外设芯片挂死,复位之后照样跑不起来;二是复位本身带来危险动作,比如设备正在驱动一个电机、或者控制着一个阀门,被狗咬复位之后 GPIO 回到默认态,可能直接造成机械动作或者工艺事故。这类设备绝不能满足于“有狗就行”,必须在复位后第一时间把输出侧切到安全状态,甚至考虑用外部硬件电路做双重保护。三是复位后的自恢复流程有问题,比如设备依赖联网获取配置,复位后网络没起来就一直卡在初始化里,被狗反复咬,形成重启循环,现场表现就是“设备一直在闪,但永远起不来”。
所以真正成熟的做法不是在代码里加一行“开狗”,而是在方案设计阶段就把这三件事想清楚:哪些故障该由狗来处理、处理之后系统怎么回到安全态、回到安全态失败时怎么升级上报。这三条想不明白,开了狗也只是把“僵死”换成了“反复重启”,对用户来说并没有本质区别。
2. 硬件狗与软件狗:两条路线的取舍
2.1 独立看门狗和窗口看门狗有什么区别
同一颗 MCU 上经常能看到两个看门狗外设:一个跑在独立低速时钟上的独立看门狗(IWDG),另一个挂在总线时钟上的窗口看门狗(WWDG)。名字很像,用途完全不同,很多人混着用,结果把自己坑了。
独立看门狗的关键在于“独立”两个字——它的时钟源是片内一个低精度的 RC 振荡器,不依赖主时钟、不依赖总线、不依赖 PLL。这意味着就算主时钟挂掉了、代码把系统时钟切错了、或者进了低功耗模式把主时钟关掉了,这条狗照样在数。代价是精度差,RC 振荡器的频率可能随温度漂移 ±10% 甚至更多,所以超时时间只能当下限用,不能当精确定时器用。它的配置流程通常很长:解锁保护寄存器、写预分频、写重装载值、等待更新完成标志、最后启动。这个“启动”动作在多数芯片上是不可逆的,一旦启动就不允许通过软件关闭,只能靠复位重新来过。这个设计看着很霸道,其实很有道理:如果软件能把狗关掉,那跑飞的软件也能顺手把它关了,这条狗就失去意义了。
窗口看门狗则相反,它挂在 APB 总线上,精度高,但它要求喂狗的时间必须落在一个时间窗内——不能太晚(太晚就超时复位),也不能太早(太早喂会被判定为“软件跑太快了,肯定不正常”而立刻复位)。它真正擅长检测的是时序异常:比如一个必须耗时 3ms 的算法流程,如果 0.5ms 就跑完了,说明中间的逻辑被跳过或者被优化错了,这时候窗口看门狗会替你报警。它的实现方式是把递减计数器的当前值和窗口寄存器做比较,同时检查最高两位,逻辑比独立看门狗复杂,配置也更细致。
选型上的经验很直接:独立看门狗负责“命”,窗口看门狗负责“节奏”。如果项目只需要一个兜底复位机制,用独立看门狗就够;如果有严格的安全时序要求(比如功能安全相关的控制回路),两个一起用,让独立狗兜底、窗口狗盯时序,是很常见的组合。
| 对比维度 | 独立看门狗 IWDG | 窗口看门狗 WWDG |
|---|---|---|
| 时钟源 | 片内低速 RC,独立于主时钟 | APB 总线时钟,精度高 |
| 主时钟失效时 | 仍能工作 | 停止工作 |
| 超时精度 | 差,随温漂变化 | 好,可精确计算 |
| 喂狗时机 | 只要不超时即可 | 必须落在窗口内,早晚都会复位 |
| 能否软件关闭 | 多数芯片不可关闭 | 可关闭 |
| 典型用途 | 兜底复位、无人值守设备 | 安全时序校验、控制回路 |
2.2 外部看门狗芯片值不值得加
片上看门狗有个绕不开的软肋:它和主控在同一个硅片上,供电、时钟、复位逻辑都是共用的。极端情况下,主控本身进入了某种连片内外设都失效的状态(比如电源跌落导致的半死机),片上看门狗也会跟着一起失灵。这时候就要靠一颗外部看门狗芯片。
外部看门狗芯片的工作方式和片内基本一样,软件通过一个 GPIO 周期性地给它一个跳变沿(注意是跳变沿,不是电平),它在内部计时,超时就拉低主控的复位引脚。它的价值在于三点:独立的供电域、独立的时钟、独立的复位通路。只要主控的复位引脚还能被拉低,它就能把主控拉回来。
代价当然是成本:一颗料、一颗电阻、几个电容,再加上一个 GPIO 和一条走线。所以判断标准很明确——是否需要长期无人值守、且故障后果严重。家用小家电、桌面消费电子,片内狗完全够用;工业现场设备、远程部署的采集终端、医疗或者安防类产品,多花这几分钱是值的。
用外部狗还有几个容易翻车的细节。第一,喂狗信号必须是真正的跳变沿,有的软件用 PWM 输出,占空比设得太极端,边沿太窄被芯片漏掉,结果好好的程序被外部狗咬。第二,喂狗那个 GPIO 一定要配置成推挽输出并明确初始电平,别让它在启动阶段浮空,浮空引脚被干扰成随机跳变,等于在乱喂。第三,外部狗的超时时间要设得比片内狗更长一些,否则永远是外部狗先动手,片内狗形同虚设,你也失去了从复位标志里分析故障来源的机会。
2.3 任务级的“软狗”只能当辅助
跑 RTOS 的项目里经常能看到第三种“看门狗”:一个低优先级的监控任务,逐个检查其他任务的计数器有没有更新,谁没更新就把谁标红、打日志、甚至触发重启。这套机制有它的价值——它能定位到具体是哪个任务卡住了,而硬件狗只知道“系统整体超时了”。
但它的局限同样明显:它本身也是个任务,也需要被调度。如果调度器被更高优先级的任务长期霸占,或者内存堆被写坏导致整个内核崩溃,监控任务自己也活不了,这时候它能救谁?答案是谁也救不了。所以我一直把它定位成故障定位工具,而不是安全兜底机制。硬件狗是底线,任务监控是在底线之上帮你快速定位问题的探针,两者不能互相替代。
实际做法是:每个关键任务维护一个心跳计数,监控任务周期性扫描,发现连续 N 个周期没有更新就在日志里记录任务名和当前栈使用情况;同时,所有任务的心跳都在规定时间内更新过之后,才允许执行那一次喂狗动作。这样一来,任何一个关键任务卡死,硬件狗都会在超时后复位系统,而日志里留下的记录能让你快速知道是哪一个卡死的。
3. 超时时间不是拍脑袋定的:完整推导过程
3.1 独立看门狗:从时钟源算到重装载寄存器
超时时间的计算公式本身很简单,难的是搞清楚每一级分频到底作用在哪。以一颗常见的 Cortex-M 内核 MCU 为例,独立看门狗的计数时钟来自片内约 40kHz 的低速 RC,经过预分频器之后再进入 12 位递减计数器,超时时间等于:
t = (4 × 2^PR) × (RLR + 1) ÷ f_LSI
其中 PR 是预分频寄存器里那三位,分频系数是 4×2^PR,RLR 是 12 位的重装载值,f_LSI 是低速时钟频率。之所以是 4×2^PR 而不是 2^PR,是因为这类外设的分频器前面固定有一个四分频,手册上会写得很清楚,但很多人只看预分频表不看这一层,算出来的时间和实测差四倍。
拿 f_LSI = 40kHz 代入,做一张常用配置表:
| PR 值 | 分频系数 | 计数时钟 | 单计数时间 | RLR=999 时超时 |
|---|---|---|---|---|
| 0 | 4 | 10 kHz | 100 us | 100 ms |
| 1 | 8 | 5 kHz | 200 us | 200 ms |
| 2 | 16 | 2.5 kHz | 400 us | 400 ms |
| 3 | 32 | 1.25 kHz | 800 us | 800 ms |
| 4 | 64 | 625 Hz | 1.6 ms | 1.6 s |
| 6 | 256 | 156.25 Hz | 6.4 ms | 6.4 s |
举个例子,如果我想让超时时间是 2 秒,选 PR = 4,单计数 1.6ms,那 RLR = 2000 / 1.6 - 1 = 1249。写在代码里就是 1249,实际超时 2000ms。这里有个必须注意的点:RC 振荡器的实际频率可能偏离标称值 10% 以上,所以算出来的 2000ms 在真实硬件上可能是 1.8s 到 2.2s 之间的某个值。因此定超时时间的时候要留足余量,比如原本想让正常流程在 1 秒内完成,超时时间设成 2 到 3 秒,而不是卡着 1.05 秒去设。
超时时间到底设多少合适?我的经验公式是:正常流程最坏耗时 × 3 到 5 倍。倍数太小,系统负载一高就误复位;倍数太大,真出问题时设备要僵死很久才恢复,用户体验差,对某些设备来说这段时间本身就是事故窗口。对于有明确周期节拍的系统(比如每 100ms 采集一次),直接把超时定在 3 个周期左右,既能容忍偶发的长耗时操作,又能保证故障在几百毫秒内被发现。
3.2 窗口看门狗:时间窗上下界的计算方法
窗口看门狗的计算稍微绕一点,因为它的计数器是 7 位的,始终从 0x7F 开始往下数,而且有个特殊规则:计数器的最高两位(bit7 和 bit6)都必须为 1 才算有效,也就是说计数值掉到 0x3F 以下就立刻复位,根本等不到数到零。这实际上是给了一个固定的“下限”。
以 APB 时钟 42MHz、预分频配置为 4096 为例,计数时钟是 42MHz / 4096 ≈ 10.25kHz,每个计数周期约 97.5 微秒。计数器从 0x7F(127)递减到 0x3F(63)共 64 个计数,对应 6.24 毫秒,这是上限——超过这个时间没喂狗就复位。窗口寄存器 W 设为 0x50(80),从 127 减到 80 是 47 个计数,对应 4.58 毫秒,这是下限——在这个时间点之前喂狗,同样复位。
所以合法的喂狗区间是 4.58ms 到 6.24ms 之间。如果你的流程正常耗时在 5ms 左右,这个窗口就设得合适;如果流程本身波动很大(有时候 3ms 有时候 6ms),那窗口看门狗的误报率会高得让你怀疑人生,这种场景就该老老实实用独立看门狗。
配置窗口看门狗的实操要点是:先确定流程的最小耗时,用它算出 W 的下界;再确定最大允许耗时,用它算出超时上界;两个值之间留个 20% 左右的余量。计算的时候要注意预分频值是 1、2、4、8 这样的 2 的幂次,有些芯片还会在分频之后再乘一个固定系数,这一层一定要翻手册确认,不然算出来的时间会整体差几倍。
3.3 复位原因判定:确认是不是被狗咬的
设备重启了,第一件事永远是问:是谁让它重启的?上电复位、掉电复位、外部复位引脚、软件复位、看门狗复位、时钟失效复位、非法访问复位——这些原因在芯片里都有对应的标志位,通常集中在一个复位状态寄存器里。
处理这个寄存器的正确顺序是:开机后第一时间读,读完立刻清。因为这些标志位是“或”的关系累积的,如果不及时清,下次重启时读到的还是上一次的残留值,分析就全乱了。常见的错误写法是在初始化流程最后才去读,中间可能已经发生了别的复位事件,标志被污染。
读出来之后,把这些信息记录下来,最好能存到非易失存储里,并且维护一个连续看门狗复位计数。这个计数非常有用:偶发一次可能是偶发干扰,连续三次以上基本可以确认是某个固定路径上的软件缺陷。我一般会在备份寄存器里开两个字节,一个存上次的复位原因,一个存连续看门狗复位的次数,超过阈值(比如 5 次)就在系统启动时进入一个降级模式,比如关闭非必要功能、只保留基础通信,避免陷入无休止的复位循环。
还要注意区分“看门狗复位”和“看门狗预警中断”。如果芯片支持预警中断,那在中断里可以先把关键现场存下来再等死,这样下次开机就能看到崩溃前最后一段日志,定位效率比盲猜高得多。
4. 喂狗这件事,写对比开起来更难
4.1 三个最常见也最致命的喂狗错误
第一个错误前面已经说过了:在高优先级中断里无条件喂狗。这是新手最容易犯、也是危害最大的一条。中断能正常跑,说明 CPU 还没完全挂,但业务逻辑是否正常它管不着。定时器中断、串口接收中断、滴答中断里喂狗,本质上都是在“替死掉的主循环打卡”。
第二个错误是喂狗点放在流程开头。比如一个循环里先喂狗再干活,看起来每次循环都喂了,实际上等于把超时窗口往后挪了一整个流程的耗时。如果流程本身就是卡住的那个环节,那它卡住之后的第一件事就是已经喂完了狗,你能指望狗在超时后复位,但那个“超时”是从上一次喂狗开始算的,中间被浪费掉的时间就是检测延迟。正确的位置是流程的末尾,先把活干完,确认干成了,再喂狗。
第三个错误是多线程/多任务环境下只在某一个任务里喂狗。跑 RTOS 的系统里,如果只在主任务里喂,其他任务死锁了没人知道;如果在每个任务里各喂一次,那又变成了“谁活着谁就能喂”,最坏情况下一个高频任务独自把狗喂到天荒地老,其他任务全挂了系统照样不重启。这就要说到下面这套架构了。
提示:任何“无条件喂狗”的代码都是危险的。喂狗之前必须有一个明确的判定,回答“系统当前处于健康状态吗”这个问题。
4.2 多任务系统怎么喂:健康度投票机制
我给团队定下来的标准做法是“任务心跳 + 汇总判定 + 单点喂狗”三层结构。
第一层,每个关键任务在自己的主循环里,每完成一轮完整工作就把自己的心跳计数加一,同时记录时间戳。注意是“完成一轮完整工作”,不是“进入循环”,这两个位置差之毫厘谬以千里。
第二层,一个独立的监控任务(优先级通常设得比较低,避免影响实时任务)周期性扫描所有心跳,检查每个任务距离上次更新是否超过了各自的容忍时间。任何一个超时,就判定系统不健康,同时记录是哪个任务、当时的栈水位、CPU 占用率。
第三层,只有监控任务在所有关键任务都健康的前提下,才执行一次真正的喂狗。这个喂狗点是全系统唯一的,位置集中、逻辑清晰、便于审计。
这个结构还有个额外好处:容忍时间可以按任务分别配置。通信任务的容忍时间可以设长一点(网络本来就有抖动),控制任务的容忍时间必须短(周期是固定的),采集任务介于两者之间。比起给整个系统设一个笼统的超时,这种细粒度控制要合理得多。
要注意监控任务本身的优先级设置。设太高会抢占实时任务的 CPU;设太低,在 CPU 满载时可能长时间得不到调度,导致它自己“失联”从而误判超时。我的经验是把它放在中等偏下的位置,并且给它的扫描周期留出充足余量,比如 10ms 扫描一次,容忍时间是 100ms,这样即使调度延迟几个周期也不会误报。
4.3 中断上下文、临界区与 DMA 场景的细节
有些场景确实需要特殊处理。比如一个必须原子完成的长流程,中间关中断或者持有互斥锁,这时候如果超时时间设得太紧,流程本身还没走完狗就叫了。解决办法不是中途偷偷喂一次,而是把这个流程的最坏耗时算进超时时间的设计里,或者把它拆成可以分段喂狗的若干步骤,每段结束时检查一次健康度再喂。
临界区里喂狗更要小心。关中断之后,如果临界区代码本身出了死循环,那连中断都进不来,狗是唯一能救你的东西,所以临界区里绝不应该出现喂狗动作。反过来,如果临界区执行时间不可避免地接近超时时间,那应该重新评估临界区的设计——通常说明临界区太大了,该拆分。
DMA 场景有个隐蔽的坑:某些低功耗芯片在进入深度睡眠时,如果 DMA 还在搬运数据,CPU 是停着的,主循环自然不跑,狗却在数。这种场景要么在进入睡眠前把超时时间临时拉长(有些芯片允许动态改重装载值,有些不允许,需要看手册),要么在 DMA 完成中断里唤醒 CPU 后立刻喂一次。还有一种更省事的做法是配置看门狗在调试暂停或者低功耗时冻结,但不是所有芯片都支持这个特性,得逐个确认。
另外提一句调试器暂停的场景。用仿真器单步调试的时候,CPU 停了,狗还在跑,几秒钟之后直接给你复位,非常影响调试体验。多数芯片提供了一个调试冻结位,在调试模块里配置一下就能让狗在 CPU 暂停时也暂停。这个位在量产固件里一定要关掉,否则一旦被意外置位,狗就永久失效了。
5. 落地实操:一条能复现的接入流程
5.1 寄存器级初始化与喂狗封装
下面这套代码是抽象过的通用形式,寄存器偏移和键值需要按各自芯片手册替换,但结构和顺序可以直接抄。核心思路是把所有对狗的裸操作封装成几个语义明确的函数,业务代码里只允许调用这些封装,绝不允许直接写寄存器。
/* 寄存器映射:地址与偏移按实际手册替换 */ #define WDT_BASE (0x40003000u) #define WDT_KR (*(volatile uint32_t *)(WDT_BASE + 0x00u)) /* 键寄存器 */ #define WDT_PR (*(volatile uint32_t *)(WDT_BASE + 0x04u)) /* 预分频 */ #define WDT_RLR (*(volatile uint32_t *)(WDT_BASE + 0x08u)) /* 重装载 */ #define WDT_SR (*(volatile uint32_t *)(WDT_BASE + 0x0Cu)) /* 状态 */ /* 键值约定:多数厂商沿用这套序列,以手册为准 */ #define WDT_KEY_ENABLE 0xCCCCu #define WDT_KEY_FEED 0xAAAAu #define WDT_KEY_UNLOCK 0x5555u初始化的时候,顺序一步都不能错:先写解锁键,再写预分频,再写重装载值,然后轮询状态寄存器等待更新完成标志,最后写启动键。中间任何一步顺序颠倒,配置都不会生效,而且不会报错——这是最坑的地方,写错了寄存器值读回来是对的,但狗的超时时间还是默认值,你按自己的计算去设计流程,结果狗提前咬了,查半天查不出来。
static void wdt_init(uint32_t pr, uint32_t rlr) { WDT_KR = WDT_KEY_UNLOCK; /* 1. 解除写保护 */ WDT_PR = pr; /* 2. 配置预分频 */ WDT_RLR = rlr; /* 3. 配置重装载值 */ /* 4. 等待更新同步完成,超时则返回错误 */ uint32_t guard = 0xFFFFu; while ((WDT_SR & 0x03u) != 0u) { if (--guard == 0u) { return; } } WDT_KR = WDT_KEY_ENABLE; /* 5. 启动,此后不可软件关闭 */ }喂狗封装更简单,但要在外面加一层健康度判定:
static volatile uint8_t g_wdt_feed_allow = 0u; void wdt_feed_if_healthy(void) { if (g_wdt_feed_allow) { WDT_KR = WDT_KEY_FEED; g_wdt_feed_allow = 0u; /* 一次授权只允许喂一次 */ } }g_wdt_feed_allow这个标志由健康度汇总逻辑置位,单次授权只能喂一次,这个“一次性”设计很重要——它能防止某段代码在循环里反复喂狗。看起来有点小题大做,但在实际项目里,正是这种防御性设计挡住过好几次无意的循环喂狗。
5.2 调试期与量产期两套配置
调试阶段和量产阶段的看门狗配置应该完全不同,这一点很多团队没做区分,结果要么调试时被狗咬得没法干活,要么量产固件里狗形同虚设。
调试期的做法:用条件编译隔开,调试构建下把超时时间拉长到 10 秒以上,并且启用调试冻结位;或者干脆在调试构建下不启动狗,但保留所有喂狗调用的代码路径,这样喂狗逻辑本身是被执行和验证过的,只是没有实际生效。千万别为了调试方便把喂狗代码用#if 0注释掉,那样等切到量产构建时,喂狗位置是否合理完全没被验证过,问题会一次性爆发。
量产期的做法:超时时间按前面算出来的最坏耗时乘以 3 到 5 倍来定,调试冻结位关闭,启动流程中尽早开启看门狗(但要在时钟、Flash、关键外设初始化完成之后,太早开会在初始化耗时长的芯片上直接导致复位循环)。另外建议在固件版本信息里记录当前配置的超时时间,现场出问题时方便快速核对。
| 配置项 | 调试构建 | 量产构建 |
|---|---|---|
| 超时时间 | 10 s 以上 | 正常流程最坏耗时 × 3~5 |
| 调试冻结位 | 开启 | 关闭 |
| 启动时机 | 初始化完成后 | 关键外设初始化完成后尽早启动 |
| 喂狗日志 | 记录每次喂狗位置 | 只在异常时记录 |
| 连续复位阈值 | 不启用 | 5 次后进入降级模式 |
5.3 低功耗与休眠唤醒的处理
低功耗场景是看门狗最容易出问题的地带,因为“系统不跑代码”和“系统死掉了”这两件事在狗看来是一模一样的。
第一种处理思路是让超时时间覆盖整个休眠时长。比如设备每 60 秒唤醒一次,那就把超时设到 90 秒,唤醒后第一件事马上喂狗,再干活。这种做法简单粗暴,缺点是休眠期间完全没有保护,如果设备在休眠中出问题只能等下次唤醒。对大多数周期性上报的设备来说够用。
第二种思路是休眠前先确认能把狗冻住。部分芯片支持在看门狗配置里打开一个位,让它在系统进入某种低功耗模式时停止计数。这个特性在手册里往往藏得很深,要专门去翻“低功耗模式下的外设行为”那一章。用这个特性之前一定要实测:进休眠、放置远超超时时间、再唤醒,看是否发生复位。纸上谈兵很容易漏掉某个前提条件。
第三种是用休眠定时器配合。有些芯片的低功耗定时器唤醒事件和看门狗共用复位逻辑,配置冲突会导致唤醒即复位。这种情况我遇到过,现象很迷惑:设备每次休眠 5 秒后唤醒,看起来正常,但日志里全是重启记录,因为复位是瞬间发生的,业务逻辑还没开始跑就被打断了。
唤醒之后的处理也有讲究。唤醒时系统时钟可能要重新配置、外设要重新初始化,这段时间通常比正常主循环长得多。所以唤醒后应该在最短路径上先喂一次狗,把时钟和外设初始化放在后面,别让初始化流程把超时时间吃光。
6. 现场排障速查与踩坑清单
6.1 问题速查表
下面这张表是我这些年积累的,按现象索引,大部分现场问题都能在里面找到对应项。
| 现象 | 高概率原因 | 排查动作 |
|---|---|---|
| 复位频繁,但每次间隔固定 | 某个固定流程耗时超过超时时间 | 在该流程前后打时间戳,实测耗时 |
| 复位间隔不固定,负载高时更频繁 | 超时时间余量不足 | 把超时时间放大一倍做对比验证 |
| 系统僵死但没复位 | 喂狗点在中断里,或喂狗判定失效 | 审查所有喂狗调用点,确认是否有关键路径未参与判定 |
| 一开机就复位循环 | 初始化耗时超过超时时间,或启动过早 | 延后启动时机,或在初始化中分段喂狗 |
| 调试时正常,脱机运行异常 | 调试冻结位在量产固件里没关,或调试器影响了时序 | 检查冻结位配置,脱机复现验证 |
| 唤醒后立即复位 | 休眠期间狗未冻结,唤醒后未及时喂狗 | 实测低功耗模式下的狗行为 |
| 升级固件时被狗咬 | Flash 擦写耗时长,超出超时时间 | 擦写前临时调整超时或分段喂狗 |
| 偶发复位,无法复现 | 电源抖动、时钟漂移、干扰 | 加复位原因记录和连续复位计数,长期观察 |
| 外部狗频繁动作但片内狗正常 | 外部狗超时设得比片内狗短 | 外部狗超时设置大于片内狗超时 |
6.2 几条用真金白银换来的经验
第一条:先有故障记录,再谈复位。在没搞清楚是谁在复位之前,任何“调大超时时间”的操作都是在掩盖问题。我现在的项目一律在启动时读复位原因、写备份寄存器、维护连续复位计数,这套机制的成本极低(几十行代码加几个字节的存储),但它在现场问题定位上的价值远超投入。很多问题靠日志能一次定位,没有日志就得来回跑几趟现场。
第二条:超时时间是设计参数,不是调试参数。见过太多项目把超时时间从 2 秒调到 5 秒、再调到 10 秒,每次都是因为“现场偶尔重启,调大点就好了”。这种做法等于放弃了看门狗的核心价值。正确的做法是实测每个关键流程的最坏耗时,加总之后去定超时时间,并且把这个数字写进设计文档。如果实测耗时和设计值差得远,那说明要么流程设计有问题,要么计算用的是标称频率而实际时钟有偏差。
第三条:复位不等于安全。前面提过一次,这里再强调。被狗咬复位之后,GPIO 回到默认态,外设全部重新初始化,如果这套设备正在控制某种物理动作,复位瞬间的输出变化可能就是事故。我现在的做法是在复位后的初始化最开始,立刻把控制输出侧配置成安全态,再做其他任何事情。这个顺序比什么都重要。
注意:看门狗的配置寄存器在多数芯片上带写保护。意外复位后如果发现超时时间变成了默认值,先检查是不是某个初始化流程漏了写保护解锁步骤。
第四条:永远实测。算出来的超时时间是一回事,实际跑出来是另一回事。我的习惯是写一个小函数,在启动时记录计数器清零时刻,在复位前(如果能捕获)记录时刻,或者更简单——用 GPIO 翻转配合示波器,实测从喂狗到复位的真实间隔。这个实测值通常会比计算值短 5% 到 15%,因为片内 RC 振荡器的实际频率往往偏高。按实测值定参数,心里才踏实。
第五条:把喂狗当成一次架构审查。每次给一个新项目定喂狗方案,我都会问自己三个问题:这个喂狗点能覆盖到所有关键路径吗?如果某个任务卡死,多久能发现?发现之后系统会进入什么状态?这三个问题的答案决定了整套机制是否真的有效。很多时候写完之后回头看,会发现原来的方案里有个明显漏洞——比如某个后台任务根本没纳入健康度判定,它挂掉了系统永远不会复位。这种漏洞只能通过反复审视发现,跑测试是跑不出来的。
最后分享一个小技巧:在固件里留一个手动触发看门狗复位的隐藏命令,通过串口或者调试接口下发,用来验证整条复位链路是否真的通。包括复位原因记录是否正确、复位后是否进入安全态、重启耗时多久,全部能在实验室里一次验证完。这条链路一旦没验证过,等真出问题的时候你会发现狗的复位信号根本没有连到该去的地方,那时候就晚了。