STM32寄存器白话手册写到第四章,我决定先把看门狗这个“小东西”单独拎出来讲透。原因很简单:我调试过的工控设备里,至少有三次“程序偶尔自动复位”的诡异故障,查到最后都落在看门狗头上——一次是喂狗位置放错了,一次是LSI时钟偏差比我预期的大,还有一次纯粹是调试器暂停太久,看门狗以为主程序挂了。你看,一个看起来只有几个寄存器的小外设,坑起来一点不含糊。
这一章适合谁?适合已经会用GPIO点灯、对中断和时钟树有基本概念的朋友。我会把STM32的两种看门狗——独立看门狗(IWDG)和窗口看门狗(WWDG)——从寄存器原理、参数计算到实际配置全部过一遍。读完你至少能做到三件事:不看示例代码,直接对着芯片手册配出符合需求的看门狗;程序异常复位后,能快速判断是不是看门狗干的;遇到“一调试就复位”这种怪问题,知道去改哪个寄存器。
1. 这一章聊什么:看门狗为什么值得单独写一讲
1.1 看门狗的本质:一台只能干一件事的硬件定时器
看门狗这个外设,说穿了就是一个递减计数器。程序正常运行的时候,必须每隔一段时间往计数器里“喂”一次值,把它重新装满。如果某段代码跑飞了、死循环了、或者硬件异常卡住了,计数器就会一路减到零,然后强制把整个芯片复位。
用生活里的例子类比:你给设备设置了一个查岗闹钟,程序每完成一段任务就打卡一次,闹钟时间到了还没打卡,系统就自动重启。这个机制在嵌入式场景下几乎是刚需——智能电表、摄像头、工控板卡这些设备一旦部署出去,没人天天守着按复位键,看门狗就是最后一道保命防线。
刚入行的朋友容易有个误解:觉得看门狗是“防止程序出错的”。其实它防的不是出错,是“出错之后没人恢复”。程序该有bug还是有bug,但看门狗可以保证bug导致的卡死状态不会一直持续,而是自动拉回重启流程,让设备有机会恢复运行。
1.2 为什么这个系列坚持用寄存器来写
现在很多开发环境里,配置看门狗就是调用一个库函数,填几个参数,几秒钟就完事。但库函数最大的问题,是它把“为什么”全藏起来了。
比如HAL库的独立看门狗初始化接口,你把超时毫秒数填进去,它内部帮你算好预分频和重载值。看着很方便,但如果你不清楚底层用的是哪个时钟、这个时钟准不准、寄存器写入有没有保护时序,一旦现场出现问题,你会发现自己连排查的着手点都没有。
我见过一个真实的排障场景:某设备偶尔复位,工程师用库函数配置了一个2秒超时的看门狗,结果现场实测1.3秒就复位了。他一度以为是芯片坏了,最后查出来是LSI(低速内部时钟)实际频率比手册标称值低了不少,导致超时时间缩水。这种问题,如果不从寄存器层面理解,光靠库函数的抽象接口,根本无从下手。
所以我一直坚持:寄存器操作不是炫技,而是让你真正掌握硬件的脾气。这个系列既然叫“白话手册”,就继续用直给的方式,把底层机制摊开讲。
1.3 这一章需要的基础,以及读完能解决什么问题
看门狗虽然是个独立外设,但牵涉到几个基础概念:时钟树里的LSI和PCLK1、中断、复位源标志。如果你已经点过灯、配过串口,这些概念至少都听过,那就够用了。
这一章要解决的问题很明确:
- 独立看门狗的超时时间到底怎么算,为什么我配的500ms实测差了很多?
- 窗口看门狗的“窗口”二字是什么意思,为什么喂早了反而会复位?
- 程序启动时怎么知道上一次是被谁复位的?
- 为什么用调试器一暂停,芯片就自己重启?
这四类问题几乎覆盖了我日常答疑里遇到的大部分看门狗相关提问。下面的内容就围绕这四个问题展开。
2. 独立看门狗(IWDG):寄存器不多,坑不少
2.1 时钟来源与超时计算公式
独立看门狗的关键词是“独立”。它独立在哪?答案是它的时钟源。
IWDG使用的时钟是LSI,也就是芯片内部的一个低速RC振荡器,不依赖系统主时钟。哪怕外部晶振挂了、主时钟配置错了,只要芯片还有电,LSI就照样跑,IWDG就照常工作。这个特性让它特别适合做“最后一道防线”。
以STM32F1系列为例,LSI的标称频率是40kHz。注意“标称”这两个字——RC振荡器的精度远不如晶振,实际频率可能落在30kHz到50kHz之间,不同芯片、不同温度、不同电压下都会有偏差。我后面会专门讲这个坑。
IWDG的超时时间计算公式如下:
Tout = 分频因子 * RLR / LSI频率其中分频因子由预分频寄存器IWDG_PR的低三位决定,对应关系如下表:
| PR[2:0] | 分频因子 | LSI为40kHz时,RLR=1000的超时时间 |
|---|---|---|
| 000 | 4 | 100ms |
| 001 | 8 | 200ms |
| 010 | 16 | 400ms |
| 011 | 32 | 800ms |
| 100 | 64 | 1600ms |
| 101 | 128 | 3200ms |
| 110 | 256 | 6400ms |
| 111 | 256 | 6400ms |
从表里能看出来,RLR最大是4095(12位),所以如果你想要一个很长的超时时间,主要还是靠加大预分频。
2.2 四个寄存器逐个拆解
IWDG一共就四个寄存器,地址在芯片手册里都能查到。我这里不背寄存器地址,重点讲每个寄存器的脾气。
IWDG_KR(键值寄存器)是第一个要认识的。这个寄存器很有意思,你对它写入不同的值,看门狗会执行不同动作:
- 写入
0x5555:解除对PR和RLR的写保护。也就是说,想改预分频和重载值,必须先“敲门”说暗号。 - 写入
0xAAAA:喂狗,行业黑话叫“重载”。硬件会把RLR的值重新装入计数器。 - 写入
0xCCCC:启动看门狗,从此计数器开始递减。
为什么要搞这种键值机制?因为看门狗本身就是防程序跑飞的。如果它自己的寄存器能被一段乱飞的代码随便乱写,那看门狗反而成了摆设。键值机制等于加了一道锁:程序异常时,碰巧把0x5555、0xAAAA、0xCCCC这几个特定值按顺序写进去的概率极低,从而保护了看门狗配置不被意外破坏。
IWDG_PR(预分频寄存器)前面提过了,低三位有效,复位值是0。注意一个细节:写PR之前必须先往KR写0x5555,写完之后,要等IWDG_SR里的PVU位清零,这次写操作才算真正生效。
IWDG_RLR(重载寄存器)低12位有效,范围0到4095。这里有个容易踩的坑:不要把RLR设成0。虽然理论上允许,但RLR为0意味着喂完狗之后计数器立即处于临界状态,稍微慢一点就会复位。而且初始化时RLR为0,启动看门狗后可能来不及喂第一次狗就触发了复位。
IWDG_SR(状态寄存器)里有两个位需要关注:
- PVU:预分频更新忙标志。写入PR后,硬件正在处理,此位置1,处理完成自动清零。
- RVU:重载值更新忙标志。写入RLR后同理。
有人会问:这两个标志要等多久?实际体验是微秒级,几乎瞬间就清掉。但哪怕再快,代码里也必须等,否则会出现配置丢失的偶发现象。我在不少项目里见过“初始化代码看着没问题,但重启后看门狗就不按预期跑”的情况,最后查下来都是没等状态位。
2.3 实际参数怎么选:从需求反推PR和RLR
配置IWDG的时候,需求往往是一句话:“我想要大约500ms超时。”那么PR和RLR怎么搭配?
首先确定分频因子。假设LSI按40kHz估算,目标500ms:
分频因子 * RLR = Tout * LSI = 0.5 * 40000 = 20000分频因子可选范围是4、8、16、32、64、128、256。为了满足12位RLR约束(最大4095),分频因子不能太小。如果选16分频,RLR需要1250,在范围内;如果选4分频,RLR需要5000,超出4095,不可行。
我习惯选256分频,这样RLR只需要78左右,余量很大:
RLR = 20000 / 256 ≈ 78.125,取78 实际超时 = 256 * 78 / 40000 = 0.4992秒选大分频的好处是,即使LSI的实际频率偏差导致超时时间浮动,相对误差的绝对值也比较可控。选小分频则意味着RLR很大,计算误差容易被放大。
这里必须多说一句:上面所有计算都基于“LSI=40kHz”这个假设。如果你做完初始化后实测超时时间偏差超过20%,不要怀疑公式,先去测一下这颗芯片的LSI实际频率。测量方法也不复杂:把LSI通过MCO引脚输出到示波器,或者用一个已知精度的定时器去数LSI的脉冲数。
注意:IWDG只能用LSI作为时钟源,这是硬件设计决定的,没有替代方案。所以如果你的项目对看门狗超时时间精度要求极高,不能依赖IWDG做精确计时,它的定位是“兜底”,不是“精确监控”。
3. 窗口看门狗(WWDG):一个被很多人用错的看门狗
3.1 窗口看门狗的“窗口”到底是什么意思
如果说IWDG是“在下一次检查之前什么时候打卡都行”,那么WWDG就是“打卡必须落在某个时间段内,早了晚了都不行”。
WWDG内部同样是一个递减计数器,但额外引入了一个窗口上限值W。具体规则如下:
- 当计数器值T大于W时,如果程序写CR寄存器喂狗,芯片立刻复位。
- 当计数器值T在W到0x40之间时,喂狗是合法的。
- 当计数器值T减到0x3F时,不管喂不喂,芯片都复位。
用大白话翻译一下:T是从高往低递减的,W就像一扇门。T还没走到门口(T > W)时,你喂狗就是破坏规矩,硬件直接翻脸;等T走进门里(T ≤ W)之后,有一段狭窄的安全区域可以喂狗,直到T掉出底边(0x3F)就是死路一条。
这个机制的精妙之处在于:IWDG只要保证“我还在运行”,至于你是正常跑还是卡在中途的某个循环里,它区分不了。而WWDG要求“你必须在规定的时间窗口内完成喂狗动作”,如果代码在某个分支里跑得太久,哪怕定时器中断还在正常触发,只要错过了窗口,照样复位。
3.2 WWDG_CR、WWDG_CFR、WWDG_SR详细解读
WWDG涉及三个寄存器。
WWDG_CR是控制寄存器,关键位如下:
- bit7
WDGA:看门狗激活位。写1启动,注意这个位一旦置1,就不能靠软件清0,只能通过系统复位回到0。 - bit6到bit0
T[6:0]:7位的递减计数器当前值,范围0到127。喂狗操作的本质就是重新把这些位写成你想要的初值。
WWDG_CFR是配置寄存器:
- bit9
EWI:提前唤醒中断使能。计数器减到0x40时触发中断,给你一个“最后补救”的机会。 - bit8到bit7
WDGTB[1:0]:预分频选择,00对应1、01对应2、10对应4、11对应8。 - bit6到bit0
W[6:0]:窗口上限值。
WWDG_SR是状态寄存器,核心就一个位:
- bit0
EWIF:提前唤醒中断标志,计数器到0x40时硬件置1,需要软件写0清除。
这几个寄存器的布局在不同系列芯片上略有差异,但功能逻辑基本一致。动手配置之前,一定先翻一下你所用型号的参考手册,把位段确认清楚。
提示:WWDG的时钟源不是LSI,而是PCLK1经过预分频后得到的。这意味着WWDG的精度受系统时钟影响,系统时钟不准,WWDG也不准。
3.3 超时与喂狗时机计算:别把窗口算成一条线
WWDG的计算比IWDG稍微绕一点。它的递减时钟周期是:
递减周期 = 4096 * 2^WDGTB / PCLK1为什么是4096?这是WWDG内部固定分频系数,芯片手册里写得很明确。
举个例子:PCLK1=36MHz,WDGTB=00,那么一个计数值耗时约0.1138ms。假设计数器初值T=0x7F(127),窗口值W=0x50(80),那么:
- 从T=127递减到T=80,耗时
(127-80)*0.1138≈5.35ms,这段时间内喂狗会复位。 - 从T=80递减到T=63(0x3F),耗时
(80-63)*0.1138≈1.93ms,这是合法的喂狗窗口期。 - 总超时时间是从T=127到T=63,约7.28ms。
看出来了吗?WWDG的整个生命周期只有几毫秒,窗口期更是只有不到2毫秒。这跟IWDG动辄几百毫秒的超时完全不是一个量级。
所以在实际项目里,WWDG不是用来“防跑飞”的,更多是用来监控某段关键代码的执行时间。比如一段临界区代码理论耗时3ms,你就可以配一个WWDG,窗口设在5ms到6ms之间,一旦代码执行超时,说明出了问题,立即复位。这种用法在通信协议处理、加密计算等对时序严格要求的场景下很常见。
还有一个容易踩的坑:窗口值W和计数器初值T的关系。启动时T写成了0x7F(127),W必须小于T。如果W等于或大于T,窗口在启动瞬间就打开了,甚至还没来得及执行任何代码就已经在窗口内,喂狗逻辑会变得非常奇怪。我见过有人把W也配置成0x7F,结果一启动喂狗就合法,整个窗口机制形同虚设。
4. 实操记录:完整演示IWDG配置、故障注入与复位源分析
4.1 准备工作:一个最小工程和一点耐心
我用手头一块STM32F103系列的开发板做演示,提前备好东西:
- 一块最小系统开发板,随便哪个牌子都行
- 一个USB转串口模块,用于打印日志
- 一个调试器,方便观察寄存器和单步执行
工程方面不需要厂商的图形化配置工具,直接用寄存器方式写。串口初始化代码这里就不贴了,用你熟悉的UART打印方式即可,波特率建议115200。
在开始之前,请把开发板的复位引脚、电源都确认好。我实际操作中有过一次教训:因为用了劣质USB转串口模块,地线没接好,串口打印偶尔乱码,让我误以为是看门狗复位信息没打出来,排查了半天。
4.2 代码实现:初始化、喂狗、故意不喂
第一步是配置IWDG,目标超时时间大约500ms。根据前面的计算,PR选择256分频(PR=6),RLR选择78。
#include "stm32f10x.h" #include <stdio.h> /* 串口打印函数这里省略,按你的平台习惯实现 */ static void BSP_IWDG_Init(void) { /* 1. 解除写保护 */ IWDG->KR = 0x5555; /* 2. 设置预分频为256,重载值为78 */ IWDG->PR = 6; IWDG->RLR = 78; /* 3. 等待寄存器更新完成 */ while (IWDG->SR & IWDG_SR_PVU); while (IWDG->SR & IWDG_SR_RVU); /* 4. 喂一次狗,让重载值立即生效 */ IWDG->KR = 0xAAAA; /* 5. 启动看门狗 */ IWDG->KR = 0xCCCC; } static void BSP_IWDG_Feed(void) { IWDG->KR = 0xAAAA; }这里有个顺序细节值得说:第三步等状态位,很多人会漏。如果不等,写入PR和RLR之后马上执行喂狗,寄存器更新可能还没完成,喂狗装载的还是旧值。尤其是RLR,旧值可能是复位默认值0,那就等于喂了个寂寞,计数器立刻溢出复位。
主函数的逻辑设计如下:正常循环每100ms喂一次狗,串口打印计数。当计数到10时,故意进入一个2秒的忙等循环,期间不喂狗,观察看门狗是否复位。
static void delay_ms(uint32_t ms) { /* 软件延时实现,这里不展开 */ } int main(void) { /* 初始化串口 */ USART1_Init(115200); /* 检查复位标志,稍后会分析 */ printf("Boot\r\n"); if (RCC->CSR & RCC_CSR_IWDGRSTF) { printf(" -> 上次复位来自IWDG\r\n"); } BSP_IWDG_Init(); printf("IWDG started, timeout ~500ms\r\n"); uint8_t count = 0; while (1) { BSP_IWDG_Feed(); printf("feed count=%d\r\n", count++); if (count == 10) { printf("now delay 2000ms, no feed...\r\n"); delay_ms(2000); /* 正常情况下到不了这里 */ printf("should not reach here\r\n"); } delay_ms(100); } }4.3 用复位标志精准区分复位来源
程序启动时第一时间读取RCC_CSR寄存器的复位标志,是排查复位问题的核心手段。STM32F103的RCC_CSR里,以下几个位值得关注:
IWDGRSTF:上一次复位是否由独立看门狗触发WWDGRSTF:上一次复位是否由窗口看门狗触发PORRSTF:上电复位标志PINRSTF:NRST引脚复位标志SFTRSTF:软件复位标志
判断完必须把标志清掉,否则下次判断时老标志还在,会造成误判。清除方法很简单:往RCC_CSR的RMVF位写1。
static void CheckResetSource(void) { if (RCC->CSR & RCC_CSR_IWDGRSTF) { printf("Reset source: IWDG\r\n"); } if (RCC->CSR & RCC_CSR_WWDGRSTF) { printf("Reset source: WWDG\r\n"); } if (RCC->CSR & RCC_CSR_PORRSTF) { printf("Reset source: Power-On\r\n"); } if (RCC->CSR & RCC_CSR_PINRSTF) { printf("Reset source: NRST Pin\r\n"); } /* 清除所有复位标志 */ RCC->CSR |= RCC_CSR_RMVF; }这个函数必须在系统启动的早期调用,越早越好。因为如果在判断之前执行了某些可能导致复位的操作,标志就会被覆盖,查不到真实来源。
4.4 实测结果和现象分析
按我实际的运行结果,串口输出如下:
Boot -> 上次复位来自IWDG IWDG started, timeout ~500ms feed count=0 feed count=1 ... feed count=9 now delay 2000ms, no feed...然后芯片复位,串口重新初始化后再次打印:
Boot -> 上次复位来自IWDG第一行Boot后面的“上次复位来自IWDG”说明什么?说明上一次芯片确实被看门狗复位了,而且我们的复位标志解析逻辑也工作正常。这个验证过程非常关键:你配置好了看门狗,还应当主动触发一次故障,确认它真的能干活。
我还做了一组对照实验:如果把延时从2000ms改成300ms,由于看门狗超时约500ms,300ms的延时不会触发复位,程序会继续往下打印should not reach here。这个对照能帮你确认超时时间的配置是否符合预期。
注意:实际工程里不要真的在主循环里放一个
delay_ms(2000)这种阻塞延时还指望看门狗不动作。这是演示用的故障注入,不是推荐写法。
5. 看门狗常见问题速查表与我的实战经验
5.1 喂狗超时时间不准:LSI误差是主要成因
很多人第一次实测IWDG超时就发现问题:配置500ms,实际可能只有400ms,或者达到600ms。原因就在LSI这个内部RC振荡器的离散度上。
RC振荡器的频率受工艺、温度、电压影响很大,同一批芯片、不同个体之间都可能差出百分之十几。如果项目对看门狗超时精度有明确要求,我的建议是:
- 把喂狗周期设定在超时时间的一半以下,留足误差余量。
- 用MCO引脚把LSI输出到示波器,实测当前芯片的LSI频率。
- 把实测频率代回公式,重新计算RLR。
我见过一个项目组被这个问题折磨了很久。他们的看门狗超时设成1秒,喂狗周期500ms,按理说余量够大。但LSI实际频率偏高,导致超时只有700ms,再加上代码里某些分支偶尔占用300ms,系统就偶发复位。后来把LSI实测频率代入计算,重新设置参数才稳定下来。
5.2 写寄存器不生效:八成卡在写保护或状态位
如果你发现IWDG_PR和IWDG_RLR写不进去,或者下次启动后配置丢失,先按这个顺序排查:
- 确认写PR和RLR之前,KR有没有写入0x5555。
- 确认写入后有没有等待SR的PVU和RVU位清零。
- 确认不是硬件问题——可以用调试器直接读PR和RLR的值,看写入是否真的生效。
有一次我在一个项目里发现:IWDG初始化函数放在一个被编译器优化掉的分支后面,代码看起来执行了,实际上PR和RLR根本没写。后来用调试器读寄存器才抓出来。所以遇到类似问题,不要猜,直接读寄存器。
5.3 调试时一暂停就复位:记得处理DBGMCU
这是新手最容易懵的情况:程序明明在调试器里正常跑着,你设了个断点,单步执行,结果芯片突然复位了。看门狗是“元凶”。
原因很简单:默认情况下,IWDG和WWDG在调试模式下依然在跑。调试器暂停CPU时,程序喂不了狗,看门狗计数器自然就把芯片复位了。
解决方法是配置DBGMCU模块,让调试冻结时看门狗也冻结:
DBGMCU->CR |= DBGMCU_CR_DBG_IWDG_STOP | DBGMCU_CR_DBG_WWDG_STOP;把这句话放在系统早起的初始化代码里。注意它只影响调试模式,正常运行时时,看门狗完全不受影响。
我在自己的工程模板里默认就把这两个位置1。因为调试过程中“程序跑到一半断了再继续”的场景太常见了,省得每次都要算着断点停留时间会不会触发看门狗。
5.4 看门狗形同虚设:喂狗位置和策略才是关键
看门狗配置对了,但如果喂狗的位置放错了,等于白配。
最常见的错误是把喂狗放在定时器中断里。这样做的直接后果:即使主程序卡死在某个死循环里,定时器中断依然照常触发,狗被喂得饱饱的,看门狗永远不动作。
正确的思路是:喂狗动作必须反映“主业务流程”的健康状态,而不是“中断系统”的健康状态。所以:
- 在主循环的关键节点、每个耗时操作之后喂狗。
- 不要在单个中断里喂狗,尤其不要在频繁触发的中断里喂狗。
- 如果用了实时操作系统,把喂狗放到空闲任务里,或者在各任务切换点喂。
另外还要注意喂狗周期和超时时间的比例。我一般把喂狗周期控制在超时时间的三分之一到二分之一之间。太频繁,看门狗失去意义;太靠近超时,一旦代码出现偶发延迟就会误复位。
5.5 启动WWDG就立刻复位:窗口初值的坑
窗口看门狗的项目中使用频率低于IWDG,但一旦用了,最常见的问题就是“一启动就复位”。
这个问题的根源通常是窗口值W和计数器初值T的关系没处理好。前面讲过,T初始应该是0x7F,W必须小于T。如果你的代码在写WWDG_CR时把T写小了,比如误写成0x20,那就完了——计数器一启动就低于0x3F这个死亡线,不等你喂狗直接复位。
还有一个隐蔽的坑:WWDG一旦使能,CR里的WDGA位置1,计数器立即开始递减。所以你必须保证在启动WWDG之后、进入主循环之前,代码执行耗时不会超过从初值到0x3F的总时间。如果初始化阶段有耗时操作,比如等待外部设备响应,就需要先初始化完其他外设,再最后启动WWDG。
我平时在代码里还会加一个保护逻辑:配置完WWDG后,先主动读一遍CR确认WDGA已经置位,再继续往下执行。虽然实际项目中这个位出错的概率很低,但多一步自检总比事后排查轻松。
5.6 实战中我常用的看门狗组合策略
聊到这一步,顺便分享一个我在多个项目里验证过的组合方案。
对于一台需要长期无人值守的设备,我的做法是:用IWDG做系统级的最后防线,超时时间设置在2到5秒;同时在关键业务模块里用软件定时器做“业务心跳”,配合WWDG监控某段关键代码的执行时间。
IWDG负责兜底:不管什么原因导致整个系统卡死,它都能拉回来。WWDG负责精准:关键代码执行超时立刻复位,不用等IWDG慢慢倒计时。两者配合,既能覆盖大面积卡死,又能覆盖特定场景的时序异常。
这个方案的代价是配置复杂度上升,调试时更容易触发意外复位。所以调试阶段一定要把DBGMCU的冻结位配上,并且把复位源检查放在启动最前面,否则你会被复位的各种“意外情况”逼疯。
这一章写到这里,看门狗在日常项目里的主要知识点和实践经验基本都覆盖了。最后说一个我自己养成的小习惯:在工程的启动代码里,尽早读取一次复位标志,存到全局变量里。后面整个系统运行期间,无论什么时候想知道“上次是怎么复位的”,直接查这个变量就行。如果配合备份寄存器里维护的复位次数计数器,遇到偶发性跑飞问题,能省下一大半排查时间。看门狗本身配置不算难,真正难的是让它在该动作的时候动作,不该动作的时候安静待着——这中间的取舍,才是实战经验和代码功力所在。