【STM32】 独立看门狗 IWDG 深度详解:从硬件结构到工程实战


STM32 独立看门狗 IWDG 深度详解:从硬件结构到工程实战

1. 基础概念

1.1 程序异常:单片机"死"法大全

很多人以为单片机异常就是"不跑了",实际上 MCU 的异常有一条清晰的恶化链:

cpp 复制代码
电磁干扰/噪声
    │
    ├─► Flash 位翻转(代码取指错误)──┐
    ├─► RAM 位翻转(变量值错乱)      │
    │                                ▼
    ├─► 栈溢出(局部变量踩坏返回地址)──► PC 跑飞
    ├─► 野指针写乱函数指针/返回地址 ──► 跳到随机地址
    │                                │
    ▼                                ▼
合法指令序列                    非法指令 / 非对齐访问
    │                                │
    ▼                                ▼
正常工作                      HardFault_Handler
                                  │
                    ┌─────────────┴─────────────┐
                    ▼                           ▼
        进 HardFault 后又死循环          进 HardFault 后做复位恢复
        (绝大多数情况)                (需要提前写好恢复逻辑)

为什么软件错误处理不可靠?

  • 跑飞后 PC 是随机值,你写的 if(error) Recover() 根本不会被执行;
  • HardFault_Handler 本身也可能因为栈已经坏了而二次跑飞;
  • 即使进了 HardFault,它自己也可能卡在 while(1) 出不来。

结论:

所有软件层面的"恢复机制",前提是"CPU 还能正确取指"。

一旦这个前提没了,只能靠一个完全独立于 CPU 的硬件来把整个芯片拍醒。

1.2 为什么看门狗是"最后防线"?

工业系统的容错是分层的:

层级 手段 能防什么 防不住什么
L1 软件层 输入校验、状态机、assert 逻辑错误、 非法输入 跑飞、栈溢出
L2 RTOS 层 任务监控、栈溢出检测、栈水位线 任务卡死、 栈溢出 内核本身被破坏
L3 处理器层 HardFault_Handler、 MPU、memfault 非法访问、除零 HardFault 自身卡死
L4 硬件层 看门狗 IWDG 一切 CPU 失控 时钟源本身失效(IWDG 用 LSI,仍独立)

看门狗在最底层------它不理解你的业务,它只看一件事:"你有没有在规定时间内做一次'我还活着'的动作"。这就是它不可替代的原因。

1.3 什么是看门狗?------一句话定义

看门狗是一个由独立时钟驱动的硬件递减计数器,软件必须周期性地"喂"它(重载计数器),一旦喂狗间隔超过设定阈值,硬件自动产生系统复位。

这个定义里有三个关键词,一个都不能少:

  1. 独立时钟------主时钟挂了它还在走;
  2. 递减计数器------硬件自主运行,不依赖 CPU 取指;
  3. 周期性喂狗------喂狗动作必须由"活着的主程序"主动发起。

1.4 看门狗的本质:把"程序是否正常"翻译成"时间是否到"

看门狗最精妙的地方在于它做了一次降维翻译:

复制代码
你的系统状态是一个极其复杂的问题:
  ├─ 通信是否正常?
  ├─ 传感器数据是否合理?
  ├─ 任务是否超时?
  ├─ 内存是否泄漏?
  └─ ...

看门狗把它压缩成一个布尔问题:
  └─ "最近 T 毫秒内,有没有发生过一次喂狗?"
      ├─ 有 → 认为正常
      └─ 没有 → 认为失控,复位

这是一种粗粒度但极高可靠度的抽象:

它放弃了"理解你"的能力,换来了"永远不会和你一起崩"的可靠性。

1.5 看门狗的种类

1.5.1 按位置分:内部看门狗 vs 外部看门狗芯片
对比 STM32 内部 IWDG 外部看门狗芯片(如 MAX6369、SP706)
成本 0(片内) 多一颗芯片
独立性 LSI 独立于主时钟,但同在一颗芯片 完全独立供电、独立时钟、独立复位引脚
可靠性 整芯片挂了它也挂 MCU 完全死机,狗还在跑
复位信号 内部复位线 直接拉 NRST 引脚
喂狗方式 寄存器写 0xAAAA GPIO 翻转
典型场景 消费电子、一般工业 高可靠工业、汽车、医疗

高可靠场合会"内外双狗":

内部 IWDG 防软件跑飞,外部狗防整片芯片死机。

1.5.2 STM32 内部:IWDG vs WWDG
对比维度 IWDG 独立看门狗 WWDG 窗口看门狗
时钟源 LSI 内部 RC(~32kHz) APB1(PCLK1,最高 45MHz)
时钟精度 ±10% 误差,温漂大 与主时钟同步,精度高
计数器位宽 12 位递减 7 位递减(T5:0 + 高位)
分频系数 4/8/16/.../256 固定 4096 分频(WDGTB)
超时范围 0.1ms ~ 32.8s 约 0.1ms ~ 58ms(很短)
喂狗时机 任意时刻都能喂 必须在窗口内喂,太早也复位
复位条件 计数器 < 0(减到 0) 计数器 < 窗口下限 或 > 上限
早期中断 无 有 EWI 提前唤醒中断
低功耗 停止/待机模式下仍计数 随主时钟停止
主要用途 防跑飞、死循环,粗粒度监控 时序精监控、防喂狗过早、中断预警
启动方式 软件启动或 option byte 硬件启动 软件启动

一句话:

IWDG 是"超时保安",WWDG 是"监考老师"------超时保安只看你最后交卷没有,监考老师连你答题节奏不对都管。

2. IWDG 原理(寄存器级)

2.1 IWDG 结构框图(完整版)

2.2 LSI:IWDG 的心脏,为什么它能独立?

LSI(Low Speed Internal)是 STM32 内部一个独立的 RC 振荡器:

  • 标称频率:32kHz(不同型号略有差异,F4 典型 32kHz,F1 是 40kHz,要查手册);
  • 精度:出厂校准,常温 ±5%,全温域 ±10%;
  • 供电:和主域同电源,但振荡电路独立于 HSE/HSI;
  • 启动:上电即起,不依赖任何系统配置。

LSI 的两个关键特性决定了 IWDG 的设计:

  1. HSI/HSE停了 LSI 照样走------这就是"独立"二字的物理基础;
  2. LSI 频率不准**------这就是为什么 IWDG 不要求高精度(它本来就只用来做"粗粒度超时")。**

进阶技巧:

可以用 TIM5 的内部测量模式,把 LSI 接到 TIM5 输入捕获,精确测出当前 LSI 实际频率,再修正 RLR 配置------这在需要精确超时的场合有用。

2.3 8 位预分频器细节

预分频系数不是随便选的,PR2:0 三位编码如下:

PR2:0 分频比 CK_CNT 周期(LSI=32k)
000 4 125 µs
001 8 250 µs
010 16 500 µs
011 32 1 ms
100 64 2 ms
101 128 4 ms
110 256 8 ms
111 256 8 ms(保留)

关键细节:写 PR 的时候必须等 PVU=0

IWDG_SR 的 PVU 位(Prescaler Value Update):

  • 软件写 PR 后,硬件把 PVU 置 1;
  • 硬件正在把预分频值同步到分频器(异步操作);
  • 同步完成后 PVU 硬件自动清零;
  • 在 PVU=1 期间写 PR 是无效的。
cpp 复制代码
while(IWDG->SR & IWDG_SR_PVU);   // 等上一次写 PR 生效
IWDG->PR = 4;

HAL 库已经帮你做了这个等待(IWDG_WriteAccessCmd + 等 SR),手写寄存器版千万别忘。

2.4 递减计数器(Down-counter)

结构:12 位,范围 0~4095。

行为:

事件 计数器动作
看门狗启动 从 RLR 载入初始值
每个 CK_CNT 边沿 减 1
喂狗(KR=0xAAAA) 立即从 RLR 重载当前值
减到 0 产生复位
读 IWDG_DR 返回当前计数值(只读)

读 DR 的用处:

可以观测计数器还剩多少,判断喂狗是否及时。

比如读出来总是接近 RLR,说明喂狗很勤;

读出来经常接近 0,说明任务卡得很险。

cpp 复制代码
uint16_t left = IWDG->DR;   // 当前计数器剩余值

2.5 最大喂狗时间间隔计算

实战算例(LSI=32kHz)
目标超时 PR RLR 实际值
50 ms 3(÷32) 25 4×8×25/32k = 25 ms
100 ms 3(÷32) 50 50 ms
500 ms 4(÷64) 125 4×16×125/32k = 250 ms
1 s 4(÷64) 250 500 ms
2 s 5(÷128) 250 4×32×250/32k = 1 s
5 s 5(÷128) 625 2.5 s
10 s 6(÷256) 1250 4×64×1250/32k = 10 s
30 s 6(÷256) 3750 30 s
三个工程坑
  1. LSI 实际频率要测:F1 的 LSI 是 40kHz 不是 32kHz,F4/F7 是 32kHz,G0/G4 又是别的------先查参考手册;
  2. 按最快 LSI 留余量:LSI 偏快 → 计数快 → 超时短。配置时按 f_LSI × 1.1 算;
  3. RLR 越大超时越长,但不要无脑最大:超时太长,跑飞后复位慢,可能损坏设备(比如电机还在转)。

2.6 喂狗动作键寄存器(KR)

KR 是 16 位只写寄存器,读它返回 0。

写入值 名称 动作
0xCCCC 启动狗 IWDG 开始计数,一旦写入无法停止
0xAAAA 重载 把 RLR 值载入递减计数器(喂狗)
0x5555 解锁 允许写 PR 和 RLR(写完自动重锁)
其他 无效 无动作

为什么用"魔法数字"而不是"置位"?

  • 如果设计成"写 KR.bit0=1 就是喂狗",跑飞后 PC 随机写寄存器,恰好写到 KR 且 bit0=1 的概率不低;
  • 用 0xAAAA 这种 16 位特定值,跑飞后随机写对的概率是 1/65536;
  • 这种"两步验证"思路和写保护是一脉相承的。

2.7 看门狗是如何实现复位的?

完整的硬件路径:

cpp 复制代码
递减计数器 = 0
    │
    ▼
内部 RCC 产生 IWDGRST 脉冲
    │
    ├──► 复位 CPU、所有外设、SRAM 内容
    │
    ├──► RCC_CSR 寄存器 IWDGRSTF 位置 1(记录复位原因)
    │
    ▼
从 0x08000000 重新取指执行

复位原因标志位全集(RCC_CSR):

标志位 复位源
LPWRRSTF 低功耗管理复位
WWDGRSTF 窗口看门狗复位
IWDGRSTF 独立看门狗复位
SFTRSTF 软件复位
PORRSTF 上电/掉电复位
PINRSTF NRST 引脚复位
BORRSTF BOR 复位

程序启动后第一件事:

cpp 复制代码
if(RCC->CSR & RCC_CSR_IWDGRSTF) {
    // 上次是被狗复位的 → 写日志/计数/进安全模式
    g_last_reset_cause = RESET_BY_IWDG;
}
RCC->CSR |= RCC_CSR_RMVF;   // 清除复位标志

调试必知:

在 Keil/J-Link 在线调试时,CPU 暂停(halt)期间 IWDG 还在跑,会把你复位掉。

解决:

配置 DBGMCU_CR 的 DBG_IWDG_STOP 位------调试时冻结 IWDG,不影响下板运行。

2.8 写保护(Write Protection)

机制:

  • PR 和 RLR 上电后默认处于写保护状态;
  • 必须先写 KR=0x5555 解锁,才能写这两个寄存器;
  • 写完一次后硬件自动重新上锁(不需要再写回去);
  • 要再次修改必须重新 0x5555。
cpp 复制代码
// 正确写法
IWDG->KR  = 0x5555;     // 解锁
while(IWDG->SR & IWDG_SR_PVU);
IWDG->PR  = 4;
while(IWDG->SR & IWDG_SR_RVU);
IWDG->RLR = 250;
IWDG->KR  = 0xAAAA;     // 喂一次
IWDG->KR  = 0xCCCC;     // 启动

2.9 为什么会存在写保护?

这是 IWDG 设计哲学的集中体现:

反例推演(如果没有写保护):

cpp 复制代码
程序跑飞 → PC 随机跳到某个地址 → 恰好执行了一条写寄存器指令
    │
    ├─ 恰好写到 RLR → RLR 被改成 4095 → 超时变成 30 秒
    ├─ 恰好写到 PR → PR 被改成 7 → 分频最大
    │
    ▼
看门狗超时被意外拉大 → 狗失效 → 系统永远卡死

写保护的三重意义:

  1. 配置是一次性的------看门狗参数只在开机时配一次,运行期不应该改;
  2. 两步解锁提高"误写门槛"------跑飞后要同时写对 KR=0x5555 再写对 PR/RLR,概率极低;
  3. 启动即不可逆------KR=0xCCCC 写了之后就关不掉,配合写保护,整个 IWDG 对"失控的软件"是封闭的。

一句话总结:写保护不是防你写错代码,是防"代码已经死了之后还能再作恶"。

3. 工程实战:寄存器版 + HAL 版

3.1 寄存器版完整初始化(超时 1 秒)

cpp 复制代码
void IWDG_Init(void)
{
    IWDG->KR  = 0x5555;        // 解锁写保护
    while(IWDG->SR & IWDG_SR_PVU);
    IWDG->PR  = 4;             // 64 分频 → CK_CNT = 500Hz → 2ms
    while(IWDG->SR & IWDG_SR_RVU);
    IWDG->RLR = 500;           // 500 × 2ms = 1s
    IWDG->KR  = 0xAAAA;        // 第一次喂狗
    IWDG->KR  = 0xCCCC;        // 启动看门狗
}

void IWDG_Feed(void)
{
    IWDG->KR = 0xAAAA;
}

3.2 HAL 库版

cpp 复制代码
IWDG_HandleTypeDef hiwdg;

void MX_IWDG_Init(void)
{
    hiwdg.Instance       = IWDG;
    hiwdg.Init.Prescaler= IWDG_PRESCALER_64;   // 64 分频
    hiwdg.Init.Reload    = 500;                 // 重装载 500
    HAL_IWDG_Init(&hiwdg);
}

// 喂狗
HAL_IWDG_Refresh(&hiwdg);

3.3 喂狗策略

cpp 复制代码
int main(void)
{
    // 1. 检测复位原因
    if(RCC->CSR & RCC_CSR_IWDGRSTF) {
        g_watchdog_reset_count++;   // 记录看门狗复位次数
        RCC->CSR |= RCC_CSR_RMVF;
    }

    IWDG_Init();

    while(1) {
        Task1();    // 最长 200ms
        Task2();    // 最长 300ms
        Task3();    // 最长 400ms

        // 只有所有任务都正常跑完才喂狗
        // 任何一个 Task 卡死,就不会走到这里 → 复位
        IWDG_Feed();
    }
}

禁忌:

千万别把喂狗放在定时器中断里。

中断能进只说明"CPU 还在响应中断",不代表主程序正常------中断里喂狗等于自欺欺人。

面试题

Q1. 独立看门狗的本质是什么?

A:

一个由独立 LSI 驱动的硬件递减计数器,软件定期喂狗重载,无人喂狗则减到 0 产生系统复位。

核心是"独立时钟 + 不依赖 CPU 取指的硬件计数"。


Q2. IWDG 为什么用 LSI 而不用 HSE?

A:

HSE 主晶振停振时,看门狗必须还能工作才能复位系统。

用独立的 LSI 保证:

主时钟挂了、主程序跑飞了,看门狗自己还在走。

独立性是可靠性的根基。


Q3. IWDG 和 WWDG 的核心区别?

A:

IWDG 任意时刻喂都行、超时就复位、LSI 独立时钟、无中断;

WWDG 必须在窗口时间内喂(太早也复位)、APB1 高精度时钟、有早期唤醒中断、超时时间短。

IWDG 防跑飞,WWDG 监控时序。


Q4. 写 IWDG 配置寄存器前为什么要写 0x5555?

A:

PR 和 RLR 默认写保护,写 KR=0x5555 解锁。

目的是防止程序跑飞后随机修改 RLR/PR 导致看门狗超时被意外拉大而失效------两步操作大幅降低误写概率。


Q5. KR 写 0xCCCC、0xAAAA、0x5555 分别做什么?

A:

0xCCCC 启动看门狗(一旦写入无法关闭);

0xAAAA 喂狗(把 RLR 重载到计数器);

0x5555 解锁 PR/RLR 写保护。


Q6. IWDG 超时时间公式?PR=4、RLR=1000、LSI=32kHz,超时多久?

A:

T = 4×2^PR×RLR / f_LSI = 4×16×1000/32000 = 2 秒。


Q7. 写 PR/RLR 时为什么要等 SR 的 PVU/RVU?

A:

IWDG 时钟是 LSI,和 APB 总线异步。

写 PR/RLR 后硬件需要几个 LSI 周期把新值同步过去,PVU/RVU 就是"同步进行中"标志。

等它清零再写下一个,避免异步冲突导致配置错乱。


Q8. 看门狗复位后怎么知道是被狗复位的?

A:

读 RCC_CSR 的 IWDGRSTF 位。

该位为 1 表示本次复位由 IWDG 引起。

程序可据此记录异常日志、进入安全模式,然后用 RMVF 位清除标志。


Q9. 为什么喂狗不能放在中断里?

A:

中断能进只说明 CPU 还响应中断,不代表主程序正常。

主程序死循环但中断还活着时,中断里喂狗会让看门狗永远被喂,等于白装。

喂狗必须放在"主循环正常跑完"的位置。


Q10. IWDG 最大超时是多少?怎么算?

A:

PR=6(÷256)、RLR=4095、LSI=32kHz:

T = 4×64×4095/32000 ≈ 32.8 秒。

超过这个时间需要外部分频或外部狗芯片。


Q11. LSI 有 ±10% 误差,配置时怎么处理?

A:

LSI 偏快 → 计数快 → 实际超时偏短。

配置时按 f_LSI × 1.1(最快情况)反推 RLR,留足余量;

要求精确时用 TIM5 输入捕获实测 LSI 频率再修正。


Q12. IWDG 启动后软件能关闭吗?

A:

**不能。**写 KR=0xCCCC 启动后,只能靠系统复位停止。

这是故意设计------防止跑飞后软件自救把狗关了。

除非 option byte 配置为硬件看门狗(上电自动启动),否则复位后狗默认关闭。


Q13. 什么是写保护?为什么需要它?

A:

PR/RLR 默认上锁,写 KR=0x5555 解锁一次,写完自动重锁。

目的:

程序跑飞后 PC 随机写寄存器时,要同时写对 0x5555 再改配置,概率极低,防止看门狗参数被意外破坏。


Q14. IWDG 在低功耗模式(停止/待机)下还跑吗?

A:

跑。LSI 在停止/待机模式下继续振荡,IWDG 继续计数。

如果进入低功耗前没配好喂狗,醒来时可能已被复位。

低功耗场景要算好超时时间。


Q15. 调试时为什么会莫名其妙被复位?怎么解决?

A:

调试器 halt CPU 时 IWDG 还在计数,没人喂狗就复位。

解决:

写 DBGMCU_CR 的 DBG_IWDG_STOP 位,调试 halt 期间冻结 IWDG。

下板运行不影响。


Q16. IWDG 的 12 位计数器最大计数值是多少?

A:

2^12 - 1 = 4095。


Q17. 喂狗时机怎么选?超时 2 秒,多久喂一次合适?

A:

喂狗间隔设为超时时间的 1/31/2,即 500ms1s。

留余量防偶发任务延迟。

喂狗点必须放在"所有关键任务都跑完"的位置,不能早喂。


Q18. 为什么 IWDG 计数器是递减而不是递增?

A:

递减到 0 的检测最简单------所有位全 0 即触发,不需要比较器。

递增到阈值需要和 RLR 做比较,硬件成本高。


Q19. IWDG 和外部看门狗芯片有什么区别?高可靠场合怎么选?

A:

内部 IWDG 免费但和 MCU 同生死;

外部狗芯片独立供电独立时钟,MCU 完全死机也能拉 NRST。

高可靠场合内外双狗:内部防软件跑飞,外部防整片 MCU 挂死。


Q20. 手撕:寄存器版初始化 IWDG,超时约 4 秒(LSI=32kHz)。

A:

cpp 复制代码
IWDG->KR  = 0x5555;
while(IWDG->SR & IWDG_SR_PVU);
IWDG->PR  = 5;             // 128 分频 → CK_CNT=250Hz → 4ms
while(IWDG->SR & IWDG_SR_RVU);
IWDG->RLR = 1000;          // 1000×4ms = 4s
IWDG->KR  = 0xAAAA;
IWDG->KR  = 0xCCCC;
// 喂狗:IWDG->KR = 0xAAAA;

相关推荐
Zw-awa1 小时前
一盏 LED 不够用了:让同一盏灯说出不同的话
stm32
运维小斌1 小时前
麒麟 V10 部署 OceanBase:故障速查与验收清单
linux·数据库·oceanbase·麒麟
LMT流氓兔1 小时前
第五单元——第9课:防止同一次操作重复执行
java·服务器·数据库·算法·ai·langchain
AMASENSYS艾马斯1 小时前
变频器过载能力参数解读与工程应用
驱动开发·嵌入式硬件·其他·硬件工程·精益工程
code2cat1 小时前
Java高级篇之JDBC:连接数据库,完成查询、更新与事务
java·开发语言·数据库
*长铗归来*1 小时前
MySQL新学知识(三)MySQL 事件(EVENT)
数据库·mysql
cu1431 小时前
细谈IT6563的具体功能与其应用
c语言·c++·单片机·嵌入式硬件
Warolitbos2 小时前
【数据库】SQL进阶15题,窗口函数/索引/事务一页速查
数据库·sql·学习·mysql·面试
BioRunYiXue2 小时前
使用docker分析高通量测序数据
java·数据库·人工智能·spring cloud·docker·tomcat·html