4. 软件看门狗实现:MCU内部看门狗配置与喂狗策略
看门狗这东西,说白了就是给程序请了个监工。你干活的时候得定期跟它说一声「我还活着」,要是哪天忘了,它就认定你死机了,直接帮你重启。我刚开始做嵌入式那会儿,总觉得这玩意儿多余,直到有一次产品在现场跑飞了,客户打电话来骂……嗯,从那以后,我再也不敢省看门狗了。
这一章咱们就拿 STM32 开刀,把独立看门狗(IWDG)和窗口看门狗(WWDG)的配置、喂狗策略、临界段保护这些硬骨头啃下来。
4.1 STM32 内部看门狗硬件架构
STM32 内部集成了两种看门狗:独立看门狗 IWDG 和 窗口看门狗 WWDG。它们俩的定位完全不同,我列个表给你看:
| 特性 | IWDG(独立看门狗) | WWDG(窗口看门狗) |
|---|---|---|
| 时钟源 | 内部低速 RC(LSI,约 40kHz) | 系统时钟(APB1) |
| 独立性 | 完全独立,主时钟挂了也能跑 | 依赖系统时钟,主时钟挂了它也挂 |
| 喂狗窗口 | 无窗口,只要不溢出就行 | 有上下限窗口,喂早了也不行 |
| 复位条件 | 计数器减到 0 | 计数器减到 0x3F 或喂狗不在窗口内 |
| 典型用途 | 主程序跑飞检测 | 任务超时检测,时序敏感场景 |
我个人习惯是:IWDG 做最后一道防线,WWDG 做任务级超时监控。两个一起用,效果更好。
4.2 IWDG 配置实战
先看 IWDG。它的时钟来自 LSI,频率大概 40kHz,但内部还有分频器。配置步骤其实就三步:
- 取消写保护:IWDG 的寄存器默认写保护,防止程序跑飞后误改。
- 设置分频系数和重装载值:决定超时时间。
- 启动看门狗:使能后就不能关了,除非复位。
超时时间计算公式:
超时时间 = (4 × 2^PR) × RLR / LSI_Freq
其中 PR 是分频系数(0~6),RLR 是重装载值(0~0xFFF)。举个例子,LSI=40kHz,PR=2(分频 16),RLR=1000:
超时时间 = (4 × 2^2) × 1000 / 40000 = 16 × 1000 / 40000 = 0.4 秒
下面是一段完整的 IWDG 初始化代码:
void IWDG_Init(uint16_t reload_val)
{
// 1. 取消写保护
IWDG->KR = 0x5555;
// 2. 设置分频系数:PR=2 → 分频16
IWDG->PR = 2;
// 3. 设置重装载值
IWDG->RLR = reload_val;
// 4. 等待寄存器更新
while (IWDG->SR & 0x01);
// 5. 启动看门狗(写入 0xCCCC)
IWDG->KR = 0xCCCC;
}
4.3 WWDG 配置实战
WWDG 比 IWDG 复杂一点,因为它有个「窗口」的概念。喂狗必须在计数器值降到窗口值之后、降到 0x3F 之前。喂早了算违规,也会触发复位。
配置步骤:
- 设置分频系数:决定计数器递减速度。
- 设置窗口值:喂狗的最早允许时间。
- 设置计数器初始值:喂狗的最晚允许时间。
- 使能 WWDG:并开启中断(可选)。
代码示例:
void WWDG_Init(uint8_t counter, uint8_t window)
{
// 1. 使能 WWDG 时钟
RCC->APB1ENR |= RCC_APB1ENR_WWDGEN;
// 2. 设置分频:CFR[8:7] = 01 → 分频 4
WWDG->CFR = (1 << 7);
// 3. 设置窗口值
WWDG->CFR |= (window & 0x7F);
// 4. 设置计数器初始值
WWDG->CR = (counter & 0x7F);
// 5. 使能看门狗
WWDG->CR |= (1 << 7);
}
4.4 喂狗策略:不是随便喂的
喂狗看起来简单——定时往寄存器写个值就行。但怎么喂、在哪喂、喂的频率,这里面门道不少。我总结了几种常见策略:
| 策略名称 | 喂狗位置 | 优点 | 缺点 |
|---|---|---|---|
| 主循环喂狗 | while(1) 末尾 | 实现简单 | 子任务卡死检测不到 |
| 任务级喂狗 | 每个关键任务完成后 | 能定位哪个任务卡死 | 代码侵入性强 |
| 多级看门狗 | IWDG + WWDG 配合 | 覆盖全面 | 配置复杂 |
| 心跳喂狗 | 定时器中断中 | 实时性好 | 主循环卡死仍能喂狗 |
我个人最推荐的是任务级喂狗 + 多级看门狗的组合。举个例子:
void main(void)
{
// 初始化
IWDG_Init(1000); // 超时 0.4 秒
WWDG_Init(0x7F, 0x40); // 窗口 0x40~0x7F
while(1)
{
Task_Sensor(); // 传感器采集
IWDG_Feed(); // 喂独立看门狗
Task_Process(); // 数据处理
IWDG_Feed();
Task_Comm(); // 通信处理
IWDG_Feed();
WWDG_Feed(); // 喂窗口看门狗(必须在窗口内)
}
}
这样每个任务完成后都喂一次 IWDG,如果某个任务卡死,IWDG 就会超时复位。而 WWDG 放在最后,确保整个循环的执行时间在窗口范围内。
4.5 临界代码段保护
临界代码段,就是那些不能被中断打断的代码。比如读写共享变量、操作外设寄存器、执行原子操作。在看门狗场景下,临界段保护有两个含义:
- 保护喂狗操作不被中断打断:防止喂狗时序错乱。
- 保护关键代码不被看门狗复位:防止执行时间过长导致超时。
对于第一种情况,可以用关中断的方式:
void IWDG_Feed_Safe(void)
{
__disable_irq(); // 关中断
IWDG->KR = 0xAAAA; // 喂狗
__enable_irq(); // 开中断
}
对于第二种情况,我建议在进入长时间操作前「预喂狗」,然后关闭看门狗中断(如果开了),操作完成后再恢复。但注意:IWDG 不能关闭,所以只能通过调整喂狗频率来覆盖。
4.6 知识结构图
下面这张图把本章的核心逻辑串起来了:
4.7 常见问题与避坑
- 调试时总被复位?——在调试器里把 IWDG 的启动代码注释掉,或者用调试器的「复位后暂停」功能,在喂狗前设断点。
- 喂狗了还是复位?——检查 WWDG 的窗口值,可能是喂早了。用逻辑分析仪抓一下喂狗时序。
- 低功耗模式下看门狗还跑?——IWDG 在待机模式下仍然运行,如果不需要,进低功耗前关闭(但关不掉,只能通过复位停掉)。
- 多任务系统怎么喂?——用任务级喂狗,每个任务完成后喂一次,同时监控任务执行时间。