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,但内部还有分频器。配置步骤其实就三步:

  1. 取消写保护:IWDG 的寄存器默认写保护,防止程序跑飞后误改。
  2. 设置分频系数和重装载值:决定超时时间。
  3. 启动看门狗:使能后就不能关了,除非复位。

超时时间计算公式:

超时时间 = (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;
}
注意:IWDG 一旦启动,任何软件都无法关闭,只能通过系统复位或电源复位来停止。所以调试的时候要小心,别让看门狗在断点处把你复位了。

4.3 WWDG 配置实战

WWDG 比 IWDG 复杂一点,因为它有个「窗口」的概念。喂狗必须在计数器值降到窗口值之后、降到 0x3F 之前。喂早了算违规,也会触发复位。

配置步骤:

  1. 设置分频系数:决定计数器递减速度。
  2. 设置窗口值:喂狗的最早允许时间。
  3. 设置计数器初始值:喂狗的最晚允许时间。
  4. 使能 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);
}
小技巧:WWDG 可以在计数器降到 0x40 时触发提前唤醒中断(EWI)。我经常用这个中断来做「预喂狗」——在中断里记录日志,然后喂狗,这样即使主循环卡住,也能留下最后一条线索。

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 临界代码段保护

临界代码段,就是那些不能被中断打断的代码。比如读写共享变量、操作外设寄存器、执行原子操作。在看门狗场景下,临界段保护有两个含义:

  1. 保护喂狗操作不被中断打断:防止喂狗时序错乱。
  2. 保护关键代码不被看门狗复位:防止执行时间过长导致超时。

对于第一种情况,可以用关中断的方式:

void IWDG_Feed_Safe(void)
{
    __disable_irq();    // 关中断
    IWDG->KR = 0xAAAA; // 喂狗
    __enable_irq();     // 开中断
}

对于第二种情况,我建议在进入长时间操作前「预喂狗」,然后关闭看门狗中断(如果开了),操作完成后再恢复。但注意:IWDG 不能关闭,所以只能通过调整喂狗频率来覆盖。

重要:关中断的时间不能太长,否则会影响系统实时性。一般建议不超过 10 微秒。如果临界段代码执行时间较长,考虑用信号量或互斥锁替代关中断。

4.6 知识结构图

下面这张图把本章的核心逻辑串起来了:

软件看门狗实现知识结构图 软件看门狗实现知识结构 IWDG(独立看门狗) WWDG(窗口看门狗) IWDG 配置 取消写保护 → 设分频/重装载 → 启动 超时 = (4×2^PR)×RLR / LSI WWDG 配置 设分频 → 设窗口值 → 设计数器 → 使能 喂狗必须在窗口内,否则复位 喂狗策略 主循环喂狗 | 任务级喂狗 | 多级看门狗 | 心跳喂狗 推荐:任务级喂狗 + IWDG/WWDG 组合 临界代码段保护 关中断保护喂狗 | 预喂狗覆盖长时间操作 关中断时间 < 10μs,避免影响实时性

4.7 常见问题与避坑

  • 调试时总被复位?——在调试器里把 IWDG 的启动代码注释掉,或者用调试器的「复位后暂停」功能,在喂狗前设断点。
  • 喂狗了还是复位?——检查 WWDG 的窗口值,可能是喂早了。用逻辑分析仪抓一下喂狗时序。
  • 低功耗模式下看门狗还跑?——IWDG 在待机模式下仍然运行,如果不需要,进低功耗前关闭(但关不掉,只能通过复位停掉)。
  • 多任务系统怎么喂?——用任务级喂狗,每个任务完成后喂一次,同时监控任务执行时间。
我的经验:在产品开发阶段,我会把看门狗的超时时间设得长一些(比如 2~3 秒),方便调试。等功能稳定了,再逐步缩短到目标值(通常 100~500ms)。这样既能保证开发效率,又能确保最终产品的可靠性。