独立看门狗:复位诊断与可靠性设计

本篇是《Zig 裸机开发实战:STM32F103ZET6 全栈解析》系列第 18 篇。上一篇:内部 Flash 编程:参数持久化与页擦除,下一篇:低功耗模式:Sleep、Stop、Standby 与唤醒,目录见文末。

独立看门狗:复位诊断与可靠性设计

导读

第 17 篇末尾埋了一个伏笔:Flash 擦写的 40 ms 停顿能把看门狗喂死。这一篇就把看门狗本身讲透。IWDG(独立看门狗)的价值在一个"独"字——它由 LSI 独立时钟驱动,主时钟死了、程序跑飞了、甚至主电源抖了一下,它都照数不误;数到零就是一次硬件复位,唯一的活路是程序定时"喂狗"。microzig 未提供 HAL,本节寄存器直写(IWDG 在 HAL 缺席名单里,hal.iwdg 不存在),位定义依据 RM0008 §18。本篇的三件套是:IWDG 的寄存器与超时公式(给 0.1 s / 1 s / 10 s 三组现成参数)、rcc.get_reset_reason() 的复位诊断(区分上电、看门狗、软件复位)、以及用备份寄存器做"复位计数器"的实战例子——故意死循环,看芯片自己爬起来并记住自己摔倒过几次。

为什么需要看门狗与它的独立性

嵌入式产品的终局问题不是"功能对不对",而是"挂了怎么办"。跑飞的原因五花八门:外设死锁(等一个永远不来的 ACK)、内存踩踏(栈溢出毁掉返回地址,第 20 篇的主角)、电磁干扰翻转了寄存器位、代码里的逻辑死循环。共同点:CPU 还在跑,但已经不知道在跑什么。看门狗是针对这类故障的最后防线——它假设"只要喂狗停止,系统就不可信",用一次复位回到已知状态。

喂狗的本质是一场信任测试:主循环每个周期喂一次,若任何环节卡死(主循环阻塞、中断风暴饿死主循环、优先级反转),喂狗中断,硬件复位。这能兜住"整个软件瘫了"的故障,但兜不住"软件活着却在做错事"——所以看门狗是保险丝,不是质检员。

F103 有两只狗,性格迥异:

IWDG 独立看门狗 WWDG 窗口看门狗
时钟 LSI 约 40 kHz(独立,主时钟无关) PCLK1(36 MHz)/4096 再分频,主时钟相关
计数器 12 位递减 7 位递减
喂狗窗口 随时喂都行 只能在窗口期内喂,喂早了也复位
中断 无 有(EWI 早期唤醒中断)
低功耗下 Stop 模式继续计数 Stop 模式自动停止
用途 防"跑不动/跑飞" 防"跑太快"(时序超前检测)

IWDG 的独立性是它的全部价值:LSI 是片内 RC 振荡器,与 HSE/PLL 无关,主时钟树崩了它还活着;反过来,它的精度也继承了 RC 的"不精"——标称 40 kHz,实际 30–60 kHz(RM0008 数据),这意味着超时时间有 ±50% 的漂移,工程上要把 1 s 的狗当 0.5–2 s 的狗用。

先声明:microzig 未提供 HAL,本节寄存器直写。IWDG 外设极其简单——四个寄存器(RM0008 §18,基址 0x4000_3000):KR(键寄存器,偏移 0x00)、PR(预分频,0x04)、RLR(重装载值,0x08)、SR(状态,0x0C)。regz 字段名不确定,照第 17 篇的同款方案:基址 + 偏移的 *volatile u32,每个位写明出处。

代码案例:1 秒 IWDG + 故意死循环 + 复位计数器

const std = @import("std");
const microzig = @import("microzig");
const stm32 = microzig.hal;
const rcc = stm32.rcc;
const gpio = stm32.gpio;
const time = stm32.time;

pub const panic = microzig.panic;
pub const std_options = microzig.std_options(.{ .logFn = stm32.uart.log });
comptime {
    _ = microzig.export_startup();
}

// ---------- IWDG 寄存器层(microzig 无 HAL,寄存器直写,RM0008 §18) ----------
const IWDG_BASE: usize = 0x4000_3000;
const iwdg_kr: *volatile u32 = @ptrFromInt(IWDG_BASE); // 键寄存器
const iwdg_pr: *volatile u32 = @ptrFromInt(IWDG_BASE + 0x04); // 预分频
const iwdg_rlr: *volatile u32 = @ptrFromInt(IWDG_BASE + 0x08); // 重装载
const iwdg_sr: *volatile u32 = @ptrFromInt(IWDG_BASE + 0x0C); // 状态

const KR_FEED: u32 = 0xAAAA; // 喂狗
const KR_UNLOCK: u32 = 0x5555; // 允许写 PR/RLR
const KR_START: u32 = 0xCCCC; // 启动(一旦启动不可停止)
const SR_PVU: u32 = 1 << 0; // 预分频更新中
const SR_RVU: u32 = 1 << 1; // 重装载值更新中

/// 启动 IWDG。LSI 标称 40kHz:timeout = (4 * 2^prer * rlr) / 40000 秒。
/// LSI 实际 30-60kHz,超时有 +/-50% 漂移,设计裕量必须覆盖。
fn iwdg_start(prer: u3, rlr: u16) void {
    iwdg_kr.* = KR_START; // 先启动(同时开启 LSI)
    iwdg_kr.* = KR_UNLOCK; // 解锁 PR/RLR 写权限
    iwdg_pr.* = prer;
    iwdg_rlr.* = rlr;
    while (iwdg_sr.* & (SR_PVU | SR_RVU) != 0) {} // 等配置同步完成
    iwdg_kr.* = KR_FEED; // 立刻喂一次,重置计数
}

// ---------- 调试期冻结:内核 halted 时暂停 IWDG(RM0008 §29.6) ----------
// DBGMCU 基址 0xE0042000,DBGMCU_APB1FZ 偏移 0x08:bit8 = DBG_IWDG_STOP
fn iwdg_freeze_when_halted() void {
    const dbgmcu_apb1fz: *volatile u32 = @ptrFromInt(0xE004_2008);
    dbgmcu_apb1fz.* |= 1 << 8;
}

// ---------- 复位计数器:存备份寄存器 BKP_DR1(备份域供电期间保留) ----------
fn bump_reset_counter() u16 {
    rcc.enable_clock(.PWR);
    rcc.enable_clock(.BKP);
    stm32.backup.set_data_protection(false);
    const n = stm32.backup.BackupData1[0].data +% 1;
    stm32.backup.BackupData1[0].data = n;
    stm32.backup.set_data_protection(true);
    return n;
}

pub fn main() !void {
    stm32.init(); // 把复位原因读进 hal.Reset_Reason(不调它永远是默认值!)

    _ = try rcc.apply(.{
        .SYSCLKSource = .PLL1_P,
        .PLLSourceVirtual = .HSE_Div_PREDIV,
        .PLLMUL = .Mul9,
        .APB1CLKDivider = .Div2,
        .RTCClockSelection = .LSI,
        .flags = .{ .HSEOscillator = true, .RTCUsed_ForRCC = true },
    });
    time.init_timer(.TIM4);

    rcc.enable_clock(.GPIOA);
    rcc.enable_clock(.USART1);

    const uart = stm32.uart.UART.init(.USART1);
    const tx = gpio.Pin.from_port(.A, 9);
    tx.set_output_mode(.alternate_function_push_pull, .max_50MHz);
    try uart.apply_runtime(.{ .clock_speed = rcc.get_clock(.USART1) });
    stm32.uart.init_logger(&uart);

    // ---- 复位诊断 ----
    std.log.info("reset reason: {s}", .{@tagName(microzig.hal.Reset_Reason)});
    const resets = bump_reset_counter();
    std.log.info("reset count (BKP_DR1): {d}", .{resets});

    if (microzig.hal.Reset_Reason == .independent_watchdog) {
        std.log.warn("previous run was killed by IWDG", .{});
    }

    // 调试期友好:probe-rs halt 时暂停狗,不影响量产行为
    iwdg_freeze_when_halted();

    // 1 秒超时:prer=4(÷64 -> 625Hz),rlr=624 -> 64*624/40000 = 0.998s
    iwdg_start(4, 624);
    std.log.info("IWDG armed, timeout ~1s", .{});

    // 主循环:每 200ms 喂一次狗 + 干活
    var ticks: u32 = 0;
    while (true) {
        iwdg_kr.* = KR_FEED;
        std.log.info("tick {d}, fed", .{ticks});
        time.sleep_ms(200);
        ticks += 1;

        // 演示故障注入:第 5 个 tick 后故意死循环
        if (ticks == 5) {
            std.log.err("simulating hang: no more feeding", .{});
            asm volatile ("cpsid i"); // 连中断都关掉,模拟彻底瘫痪
            while (true) {} // 1 秒后 IWDG 复位整机
        }
    }
}

逐段解析

四个寄存器与三把钥匙:IWDG 的全部

IWDG 的交互模型是"命令式"的:PR/RLR 这两个配置寄存器平时写不进去,必须先用 KR 发 0x5555 解锁;KR 的另两个值 0xCCCC(启动)与 0xAAAA(喂狗)是动作命令。注意"启动后不可停"——IWDG 一旦 0xCCCC 就只能靠复位(系统复位或备份域复位)关掉,这正是它的可靠性来源:恶意或失控的代码无法把它关掉。

配置写入后不是立即生效:内部同步逻辑要把 PR/RLR 从 APB 时钟域搬到 LSI 时钟域,搬运期间 SR 的 PVU/RVU 置位,此期间再写会被忽略。所以 iwdg_start 里等 SR & (PVU | RVU) == 0 的忙等不是仪式——跳过它,随机出现的"配置没生效"会让你怀疑人生。

超时公式与三组现成参数:

timeout = (4 × 2^prer × rlr) / 40000  秒      (LSI 标称 40 kHz)
prer 分频 计数频率 rlr 超时
2 16 2500 Hz 249 16 × 249 / 40000 ≈ 0.0996 s
4 64 625 Hz 624 64 × 624 / 40000 ≈ 0.998 s
6 256 156.25 Hz 1563 256 × 1563 / 40000 ≈ 10.0 s

RLR 是 12 位(0–4095),LSI 40 kHz 下最长超时约 26 s(prer=6、rlr=4095)。再次强调 LSI 的 ±50% 漂移:0.1 s 的狗实际可能是 0.06–0.15 s,喂狗周期要按最坏情况(最快的狗,即最短超时)设计——喂狗间隔必须 小于超时下限,而超时上限决定你容忍的故障恢复时间。1 s 的狗、200 ms 的喂狗间隔,比值 5:1,是常用搭配。

复位诊断:get_reset_reason 与一个隐藏 bug

RCC 的 CSR 寄存器里有一组"复位原因标志"(RM0008 §7.3.2):IWDGRSTF、WWDGRSTF、PORRSTF、SFTRSTF、PINRSTF、LPWRRSTF——对应复位发生时置位,只能软件清除,所以跨复位保留。rcc.get_reset_reason()(HAL 的 rcc 模块里逐字存在的函数,签名 pub fn get_reset_reason() ResetReason)读这些标志,映射成 ResetReason 枚举,五个成员逐字是:low_power、window_watchdog、independent_watchdog、POR_or_PDR、NRST。

HAL 还有个便利封装:stm32.init()(即 hal.init())只做一件事——把复位原因读进全局变量 microzig.hal.Reset_Reason。但这里有一个上游示例自己都踩过的坑(rtc.zig 示例的隐藏 bug):hal.init() 不一定执行——它靠 microzig_main 里"用户根文件没定义 init 就调 hal.init()"的钩子机制,可一旦你在根文件里定义了自己的 pub fn init()(很多人有时钟配置的习惯),HAL 的版本就被覆盖,Reset_Reason 永远是初值 .POR_or_PDR。防御性写法就是本例:在 main 开头显式调一次 stm32.init(),幂等无害,从此不依赖任何隐式钩子。

诊断输出的价值在于闭环:reset reason: independent_watchdog + reset count: 3,一句话讲清"它又狗死了、这是第三次"。量产固件里这两个值应该上报日志/遥测——看门狗复位在产品现场是静默发生的,没有诊断记录你永远不会知道。

bump_reset_counter 用的是备份域(backup)寄存器 BKP_DR1:HAL 暴露为 BackupData1[10] 与 BackupData2[31] 两组 16 位寄存器,由 VBAT 或 VDD 经备份域供电,系统复位不清零(只有备份域复位或掉电才失)。写入前要关写保护(set_data_protection(false))并使能 PWR/BKP 时钟——第 19 篇的 Standby 模式与 RTC 也共用这个备份域,这里是第一次见面。另有一个精确性细节:HAL 的 init() 不自动跑的问题同样适用于"读原因"的时机——必须在任何一次真正使用 Reset_Reason 前完成调用,本例放在 main 第一行就是这个原因。

调试与看门狗的天然冲突:DBG_IWDG_STOP

开发期最烦人的事:在调试器里设个断点,停下思考人生——IWDG 不认识"断点",计数器照走,你松开断点不到一秒芯片复位了,断点、单步全部变成薛定谔状态。解法是 DBGMCU 的冻结位:DBGMCU_APB1FZ(地址 0xE0042008,RM0008 §29.6 的调试外设寄存器)的 bit8 = DBG_IWDG_STOP——置 1 后内核 halted 期间 IWDG 计数器暂停。WWDG 同理是 bit9。

三句话讲清定位:这个位只影响调试会话(halt 状态),量产固件正常运行时行为完全不变;它在复位后清零,所以要在每次 main 里设置(示例就是这么做的);它冻结的是"halt 期间",运行中的狗照跑——别指望它能当暂停键用。有些团队用 build 选项区分 Debug/Release,只在 Debug 固件里调 iwdg_freeze_when_halted()——更干净,本篇为了单文件示例无条件开启。

喂狗策略:主循环喂 vs 任务监督者

示例是最朴素的"主循环喂":主循环活着就喂。它防的是主循环级瘫死,但有个盲区——主循环活着,不代表所有子系统都活着(某个任务死锁了,主循环若无其事地继续喂)。升级版是任务监督者模式:

  • 每个关键任务维护一个"我还活着"的标志(或心跳计数);
  • 喂狗函数先检查所有标志,全部健康才喂,任何一个缺席就不喂、让狗复位;
  • 标志由各任务自行置位、由喂狗者清除(防止陈旧标志放行)。
var task_alive: [4]bool = @splat(false); // 各任务周期性置位

fn feed_watchdog() void {
    for (task_alive) |alive| {
        if (!alive) return; // 有任务缺席:故意不喂,等复位
    }
    iwdg_kr.* = KR_FEED;
    task_alive = @splat(false); // 清零,下一周期重新报告
}

这个结构的纪律是"喂狗权集中、活命权下放"——任何一个环节都无法单独骗过看门狗。代价是喂狗周期必须大于最慢任务的周期,否则慢性子任务被误杀。多任务系统里再进一步就是 WWDG 的窗口期机制:喂早了也复位,专治"主循环跑得太快、某段代码被跳过"的时序异常——两只狗搭配是高可靠产品的常见配置。

运行现象

烧录后串口输出(前几秒):

reset reason: POR_or_PDR
reset count (BKP_DR1): 1
IWDG armed, timeout ~1s
tick 0, fed
tick 1, fed
tick 2, fed
tick 3, fed
tick 4, fed
simulating hang: no more feeding

一秒后芯片复位,重新启动后继续:

reset reason: independent_watchdog
reset count (BKP_DR1): 2
previous run was killed by IWDG
...
  • 无限循环:每次跑到 tick 5 死循环 → 约 1 秒后复位 → 计数器 +1。备份寄存器证明"摔倒记录"跨系统复位持久(只有备份域复位或断 VBAT 才清零)。
  • 喂狗间隔实验:把 sleep_ms(200) 改成 1200——超时 1 s、喂狗 1.2 s,狗每次都在喂之前到点,芯片无限复位循环,reset count 飞涨。这就是"喂狗周期 < 超时"的血泪演示。
  • LSI 漂移实测:用秒表量复位间隔,1 s 参数的实际值通常在 0.7–1.4 s 之间——RC 时钟的物理事实。
  • 调试器验证 DBG_IWDG_STOP:probe-rs attach 后 halt 内核,等 5 秒再 resume——不发生复位(halted 期间狗暂停);注释掉 iwdg_freeze_when_halted() 再试,resume 后 1 秒内必复位。
  • 换参数 (6, 1563) 重烧:死循环到复位要等约 10 秒——同一只狗的不同档位。

常见问题

Q1:看门狗复位后程序又开始死循环,如何区分"复发性故障"与"复位后初始化失败"?

复位诊断的第一手信息是 ResetReason:连续的 .independent_watchdog 说明喂狗路径有问题(要么故障还在、要么喂狗逻辑在复位后没跑起来)。区分手段:① 复位计数器配合"阶段标记"——把"初始化已通过第 N 步"写入另一个备份寄存器,看狗死在哪个阶段;② 临时版固件不启动看门狗,打印关键初始化日志,确认能走到死循环点;③ 用调试器 attach(不 reset)读取 RAM/备份寄存器现场。若复位后日志连第一行都没打出来,问题在启动路径(时钟、电源、.data 搬运),而不是看门狗本身。

Q2:IWDG 一旦启动就停不下来,开发时每次改代码都要等狗复位,太烦了怎么办?

三个手段:① DBG_IWDG_STOP(本篇的 iwdg_freeze_when_halted)解决"调试器 halt"场景,但运行中的狗照跑;② 开发构建不启动看门狗——用 build 选项控制 iwdg_start 的调用,量产版必开;③ 若已经被狗缠上且想立即摆脱:连上调试器全片擦除重烧,复位后从头开始、第一件事不启动狗。顺带一提:IWDG 在 Stop/Standby 低功耗模式下继续计数(这正是"独立"的一部分),低功耗应用要么用足够长的超时覆盖睡眠期,要么改用 WWDG(Stop 下自动停)——第 19 篇会再遇到这个选择。

Q3:喂狗函数里加"所有任务健康才喂"的条件,会不会反而造成误复位?

要加,这正是看门狗价值的最大化。误复位的风险来自两个工程错误:喂狗周期没有覆盖最慢任务(慢性子被误杀)、标志清除时序错误(陈旧标志放行僵尸任务)。纪律:喂狗周期 > 所有被监督任务的最坏周期之和的 1.5–2 倍;标志由任务在周期开头置位、喂狗者检查后清除(本篇代码的顺序);新增任务时同步登记进监督表。没有监督的"裸喂"只能防主循环级瘫死,防不了"局部死锁但主循环健康"——后者才是真实系统故障的主要形态。

Q4:喂狗代码跑到了,但某个中断把数据弄坏了,看门狗会救吗?

不会——看门狗只测"喂狗路径的活性",不测数据完整性。中断破坏数据属于"软件活着做错事",看门狗是盲的。补救分层:数据层用 CRC/魔数自校验(第 17 篇的配置三件套);关键状态机加"非法状态即主动复位"路径——主动复位比被狗咬体面,因为可以先把故障现场写进备份寄存器再复位;WWDG 的 EWI 中断(复位前一拍触发)是最后的机会窗口,可以在中断里保存现场再赴死。看门狗的定位永远是"最后防线",前面的防线(边界检查、错误处理、状态机防御)一样不能少。

Q5:LSI 时钟不准,能不能校准?超时精度要求高怎么办?

F103 的 LSI 没有硬件校准通道(没有"测 LSI 频率再反调"的外设回路),只能接受 30–60 kHz 的现实。工程对策:① 超时设计按最坏值包络(30 kHz 时超时 ×1.33,60 kHz 时 ×0.67),喂狗周期与故障恢复预期都落在区间内;② 精度敏感的窗口监控改用 WWDG(PCLK1 驱动,误差与主晶振同级,±20 ppm 量级);③ RLR 是只写的,剩余计数值不可读,运行中也无法"测狗"。结论:IWDG 的精度定位就是"量级正确",它的卖点是独立性,不是准头——两个诉求都重要就两只狗各司其职。

Q6:get_reset_reason() 读到的原因会几种同时为真吗?诊断后要不要清标志?

会。RCC CSR 的复位标志是累积的:POR 之后又发生 IWDG 复位,PORRSTF 与 IWDGRSTF 同时置位(POR 标志从上电起从未清除)。get_reset_reason 内部按优先级取一个返回,所以它给的是"最近一次复位的原因",语义正确但要理解背后的累积性。工程实践:诊断记录完成后主动清标志(写 CSR 的 RMVF 位,寄存器直写)——不清的话,上电一次后 POR 标志永远挂着,后续诊断的"唯一性"下降。本篇示例没清,因为演示周期里"最近原因"已够用;量产代码建议"读 → 记 → 清"三连。

面试考点

Q1:IWDG 与 WWDG 的机制差异?分别防什么故障?

参考答案:IWDG 由 LSI(约 40 kHz,独立于主时钟)驱动 12 位递减计数器,随时可喂、超时即复位、启动后不可停、Stop 模式下继续计数——防"跑不动/跑飞"类全局瘫死,且主时钟崩溃也有效。WWDG 由 PCLK1/4096 再分频驱动 7 位计数器,喂狗必须在窗口期内——太早(计数尚未递减到窗口值)也复位,防"跑太快/时序超前/某段代码被跳过";它有 EWI 早期唤醒中断,可在复位前一拍抢救现场。搭配用法:IWDG 兜底全局活性,WWDG 监控执行节奏。WWDG 依赖主时钟,主时钟失效时它自己也失效——这就是 IWDG 不可替代的地方。

Q2:写出 IWDG 的超时公式,并给 1 秒超时的参数。为什么实际超时会偏离计算值?

参考答案:timeout = (4 × 2^prer × rlr) / 40000 秒,LSI 标称 40 kHz。1 秒:prer = 4(分频 64,计数频率 625 Hz)、rlr = 624 → 64 × 624 / 40000 ≈ 0.998 s。偏差来源:① LSI 是 RC 振荡器,实际 30–60 kHz(±50%),这是主因;② 计数器离散化(±1 计数,通常可忽略);③ PVU/RVU 配置同步期的写入延迟。工程准则:喂狗周期按超时下限(×0.67)设计,故障恢复预期按上限(×1.33)沟通,两者之间留出"检测 + 收尾 + 复位"的裕量。

Q3:看门狗与调试器为什么"天然冲突"?DBG_IWDG_STOP 的原理与边界?

参考答案:调试器 halt 内核后 CPU 停止,但 IWDG 计数器挂在 LSI 上、与 CPU 状态无关,照常递减——断点停留超过超时就复位,断点调试几乎不可用。DBGMCU_APB1FZ(0xE0042008)的 bit8(DBG_IWDG_STOP)置位后,内核 halted 期间冻结 IWDG 计数;bit9 对应 WWDG。边界三条:只在 halt 状态生效,正常运行不变;复位后清零,需在 main 重设;量产建议用构建选项只在 Debug 版启用。这是"调试便利性与被调对象的物理真实性"之间用硬件开关做的显式取舍。

Q4:什么是任务监督者模式的喂狗?相比主循环喂狗多了什么保障?

参考答案:主循环喂狗只证明"主循环没死";任务监督者要求每个关键子系统周期性置位自己的存活标志,喂狗者检查全部标志齐全才喂并清除标志——任何任务缺席,狗被故意饿死并触发复位。新增保障:局部死锁/饿死(主循环健康但某子系统卡住)被捕获;单个任务无法独自伪造健康(喂狗权集中)。实施要点:喂狗周期 > 最慢任务的 1.5–2 倍防误杀;标志"任务置位、喂狗者清除"防陈旧标志;监督表随功能增长同步维护。这是从"活性监控"到"分布式健康检查"的升级。

Q5:复位原因标志(RCC CSR)的行为特点?如何用它做可靠的复位诊断?

参考答案:特点:① 标志由硬件在对应复位时置位、只能软件清除(RMVF),因此跨复位累积——POR 标志会在上电后一直挂着;② 多个标志可同时置位,"最近原因"需要按优先级解读。可靠诊断:上电读 get_reset_reason()(或 hal.Reset_Reason,前提是 hal.init() 已执行——它靠 init 钩子机制,根文件定义了自己的 init 就必须显式调 stm32.init());把原因与自定义计数器写入备份域寄存器(系统复位不清零);上报后写 RMVF 清标志,让下一轮诊断干净开始。备份域的耐久性是关键——普通 RAM 在系统复位后内容不定,不能当诊断存储。

Q6:Flash 擦写(约 40 ms CPU 停顿)与 1 秒 IWDG 并存的系统,喂狗设计要注意什么?

参考答案:40 ms 停顿 < 1 s 超时,理论安全,但要把"停顿 × 波动"计入喂狗周期预算:擦除恰好跨喂狗点时,实际间隔 = 计划间隔 + 40 ms,仍远小于 0.67 s 的超时下限,安全。真正的陷阱组合是小超时的狗(如 100 ms)+ Flash 擦写:2 KB 页擦除约 40 ms、8 个半字编程约 0.3 ms,加起来逼近 100 ms 狗的超时下限,必死或勉强。对策:拉长狗的超时、擦写前显式喂狗、或把擦写拆成多次小操作(按半字编程天然分片,页擦除是原子的拆不了)。更系统的做法:建立"最坏临界区时长"台账(关中断、Flash 擦写、总线长事务),任何超时参数变更都对着台账复核——看门狗参数与系统最坏延迟是强耦合的设计对。

小结

  • microzig 无 IWDG HAL,寄存器直写:基址 0x40003000,KR/PR/RLR/SR;KR 三把钥匙 0xCCCC 启动、0xAAAA 喂狗、0x5555 解锁配置;PR/RLR 写后等 SR 的 PVU/RVU 清零。
  • 超时 = 4 × 2^prer × rlr / 40000 秒;三组参数:0.1 s (prer=2, rlr=249)、1 s (4, 624)、10 s (6, 1563);LSI 实际 30–60 kHz,超时 ±50% 漂移,喂狗周期按最坏情况设计。
  • IWDG 启动后不可停,Stop 模式下继续计数;调试 halt 卡死调试 → DBGMCU_APB1FZ(0xE0042008)bit8 = DBG_IWDG_STOP,main 里每次设置。
  • 复位诊断:显式调 stm32.init()(否则 Reset_Reason 永远是默认值)+ get_reset_reason() 读 RCC CSR 累积标志;诊断后写 RMVF 清标志。
  • 备份域寄存器(BKP_DR1)跨系统复位持久,是复位计数器/故障现场的理想载体;写前关保护 + 使能 PWR/BKP 时钟。
  • 喂狗策略:主循环喂防全局瘫死,任务监督者(全员健康才喂)防局部死锁;喂狗周期 < 超时下限。
  • WWDG 补位:窗口期限制防"跑太快",EWI 中断留最后遗言;两狗搭配是高可靠配置。
  • 下一篇低功耗:Sleep/Stop/Standby 三档省电的等级、唤醒路径,以及"Stop 醒来串口乱码"的头号大坑。

系列目录

  1. 系列总览:Zig 裸机开发的定位、生态与平台全景
  2. 开发环境搭建:Zig 0.17、microzig 与 probe-rs
  3. 工程结构剖析:build.zig、MicroBuild 与固件产物
  4. 第一个固件:启动流程、向量表与 GPIO 点灯
  5. 寄存器级编程:PAC、MMIO 与 volatile 语义
  6. GPIO 子系统:输入输出模式、上下拉与端口映射陷阱
  7. 时钟树配置:HSE、PLL 与总线分频
  8. 中断系统:NVIC、优先级分组与向量注册
  9. USART 串口通信:轮询收发与日志重定向
  10. 定时器基础:时基、PWM 与呼吸灯
  11. 输入捕获:频率与占空比测量
  12. 中断驱动串口:环形缓冲区与临界区设计
  13. DMA 传输:从外设搬运到内存
  14. I2C 总线:SSD1306 OLED 显示实战
  15. SPI 总线:W25Q64 Flash 读写与 ID 识别
  16. ADC 采样:单次、扫描与内部温度传感器
  17. 内部 Flash 编程:参数持久化与页擦除
  18. 独立看门狗:复位诊断与可靠性设计
  19. 低功耗模式:Sleep、Stop、Standby 与唤醒
  20. 内存布局与链接脚本:512 KB Flash 与 64 KB RAM 规划
  21. Zig 语言进阶:comptime、错误处理与零成本抽象
  22. 并发模型:中断与主循环的协作与临界区
  23. 协作式调度器:状态机与时间片轮转
  24. 实时性分析:中断延迟、抖动与 WCET
  25. RTOS 内核实现(上):任务控制块与上下文切换
  26. RTOS 内核实现(下):信号量、消息队列与优先级继承
  27. USB 设备开发:CDC 虚拟串口
  28. CAN 总线:bxCAN 过滤器与环回自测
  29. 调试与测试:probe-rs、RTT 与硬件在环
  30. 收官:分层架构、可移植性与 Zig 生态展望
posted @ 2026-10-27 08:53  navysummer_hardware  阅读(20)  评论(0)    收藏  举报