一、为什么需要看门狗?

嵌入式系统运行环境复杂,电磁干扰、电源波动、指针越界、堆栈溢出都可能使 CPU 偏离正常执行流,陷入死循环或跑飞。此时程序无法响应外部事件,系统“假死”。看门狗的本质是一个独立计数器,若程序未在规定时间内“喂狗”(刷新计数器),看门狗将强制复位 MCU,让系统重新运行。

STM32 提供两种看门狗:

  • IWDG(独立看门狗):由内部低速时钟(LSI,约 40kHz)驱动,独立于主时钟,即使主时钟失效也能工作。适合需要绝对可靠性的场景。
  • WWDG(窗口看门狗):由主时钟(APB1)驱动,喂狗时间必须在窗口期内,过早或过晚都会复位,能检测到程序时序异常。

二、看门狗工作原理

1. IWDG 原理

IWDG 是一个 12 位递减计数器,时钟源为 LSI。启动后计数器从预装载值开始递减,减到 0 时产生复位。程序必须在计数器减到 0 之前重新装载(喂狗)。

关键寄存器:

  • KR(键寄存器):写入 0x5555 解除写保护,写入 0xAAAA 喂狗,写入 0xCCCC 启动看门狗。
  • PR(预分频器):设置分频系数,决定计数时钟。
  • RLR(重装载寄存器):设置计数初值。

超时时间计算:

[ T_{out} = \frac{RLR \times 4 \times 2^{PR}}{LSI_{freq}} ]

例如 LSI=40kHz,PR=4(分频 64),RLR=500,则 ( T_{out} = 500 \times 64 / 40000 = 0.8s )。

2. WWDG 原理

WWDG 是一个 7 位递减计数器,时钟来自 PCLK1。它有两个边界:

  • 窗口上限:只能在计数器的值小于某个窗口值时喂狗,太早喂狗(计数值大于窗口值)会复位。
  • 下限(0x40):计数器减到 0x3F 时复位,所以必须在减到下限之前喂狗。

窗口配置的意义在于:如果程序因为跑飞导致循环时间异常变短,提前喂狗也会触发复位,从而检测逻辑错误。

三、IWDG 配置步骤与代码

1. 配置步骤

  1. 0x5555 到 KR 解锁 PR 和 RLR。
  2. 设置 PR 和 RLR 值。
  3. 0xAAAA 到 KR 重新装载(喂狗)。
  4. 0xCCCC 到 KR 启动 IWDG(启动后无法关闭)。

2. 代码示例

// 使用 STM32 HAL 库,初始化 IWDG,超时 1 秒
void IWDG_Init(void)
{
    IWDG_HandleTypeDef hiwdg;
    hiwdg.Instance = IWDG;
    hiwdg.Init.Prescaler = IWDG_PRESCALER_64;   // 分频 64
    hiwdg.Init.Reload = 625;                    // 625 * 64 / 40000 ≈ 1s
    HAL_IWDG_Init(&hiwdg);
}

// 喂狗函数
void IWDG_Feed(void)
{
    HAL_IWDG_Refresh(&hiwdg); // 写 0xAAAA 到 KR
}

注意:hiwdg 需定义为全局变量或静态变量,以便在喂狗时使用。如果使用寄存器操作,喂狗语句为 IWDG->KR = 0xAAAA;

四、WWDG 配置步骤与代码

1. 配置步骤

  1. 计算窗口值和分频。例如 PCLK1=36MHz,分频 4096,计数器时钟 ≈ 8.8kHz。
  2. 设置 CFR 窗口值(例如 80)。
  3. 设置 CR 计数初值(例如 127)和启动位 WDGA。
  4. 使能中断(可选)提前通知喂狗。

2. 代码示例

void WWDG_Init(void)
{
    __HAL_RCC_WWDG_CLK_ENABLE();
    
    WWDG_HandleTypeDef hwwdg;
    hwwdg.Instance = WWDG;
    hwwdg.Init.Prescaler = WWDG_PRESCALER_DIV8;   // 8 分频
    hwwdg.Init.Window = 80;                       // 窗口值
    hwwdg.Init.Counter = 127;                     // 计数初值
    hwwdg.Init.EWIMode = WWDG_EWI_ENABLE;         // 开启提前唤醒中断
    HAL_WWDG_Init(&hwwdg);
}

// 窗口看门狗中断回调(可选,用于喂狗警告)
void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg)
{
    // 此处可以喂狗,但必须保证计数值小于 Window 且大于 0x3F
    HAL_WWDG_Refresh(hwwdg);
}

注意:WWDG 喂狗窗口很窄,必须在计数器的值从 127 递减到 80 之间喂狗,否则复位。实际中建议在中断回调中喂狗,保证时序精确。

五、程序跑飞恢复策略

看门狗只能复位系统,但复位后如何快速恢复工作状态?建议采取以下策略:

  1. 存储关键状态:在外部 Flash 或备份寄存器保存运行模式、参数、日志等,复位后恢复。
  2. 区分复位原因:通过 RCC_GetFlagStatus 检查复位标志(IWDGRSTF / WWDGRSTF),判断是否因看门狗复位,并记录错误计数。
  3. 分层喂狗:不要在 main 循环顶部一次性喂狗,建议在主循环和关键任务中分别喂狗,防止部分任务卡死而主循环仍能喂狗。
  4. 延迟复位后的重试:如果连续多次看门狗复位,说明系统存在严重故障,应进入安全模式,停止高风险外设,并报警。

示例:复位原因判断

void Check_Reset_Source(void)
{
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST))
    {
        // 独立看门狗复位
        __HAL_RCC_CLEAR_RESET_FLAGS();
        System_ErrorLog('W');
    }
    else if (__HAL_RCC_GET_FLAG(RCC_FLAG_WWDGRST))
    {
        // 窗口看门狗复位
        __HAL_RCC_CLEAR_RESET_FLAGS();
        System_ErrorLog('V');
    }
}

六、注意事项

  • IWDG 一旦启动不可停止,除非复位 MCU。因此调试时建议使用条件编译,避免频繁复位。
  • LSI 时钟精度不高,超时时间有 ±十几% 误差,喂狗周期要留足够裕量(建议小于超时值的 70%)。
  • 喂狗不能放在中断里(除非是专门用于喂狗的中断),否则主循环卡死时中断仍可喂狗,失去保护作用。
  • WWDG 的窗口值必须大于 0x3F,且计数器初值必须大于窗口值,否则配置无效。
  • 不要在所有任务都执行完后才喂狗,应分散喂狗,防止关键任务卡死但非关键任务正常。
  • 若使用低功耗模式,需在进入 STANDBY 前关闭看门狗,或确保唤醒后能及时喂狗。

七、总结

STM32 看门狗是防止程序跑飞的有效工具。IWDG 简单粗暴,适合独立性强、对时间精度要求不高的场景;WWDG 提供窗口检测,适合对任务时序有严格要求的系统。实际开发中,应结合复位原因记录、状态保存和分层喂狗策略,才能构建真正健壮的嵌入式系统。记住:看门狗的最终目的是让系统“快速恢复”,而不是“永不重启”。

// 最终推荐:在主循环中每隔一段时间喂狗,并配合任务看门狗
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    Check_Reset_Source();
    IWDG_Init();
    
    while (1)
    {
        Task_LED();       // 任务1
        IWDG_Feed();      // 喂狗1
        Task_KeyScan();   // 任务2
        IWDG_Feed();      // 喂狗2
        Task_Display();   // 任务3
        IWDG_Feed();      // 喂狗3
    }
}