嵌入式看门狗设计:能重启,不代表系统就可靠
一、深度引言:看门狗不是万能保险
看门狗是嵌入式系统的常见保护手段。主循环或任务定期喂狗,系统卡死就自动复位——很多项目把看门狗当成最终兜底,觉得"至少能重启恢复"。问题是:为什么卡死?重启后同类条件会不会再次卡死?重启过程中数据是否损坏?用户状态是否丢失?如果这些问题没有答案,看门狗只是把问题从"卡死"变成"反复重启",可靠性并没有实质提升。
更严重的是错误设计:在定时器中断里无条件喂狗。中断优先级高于任务,即使所有业务任务已经死锁,定时器中断仍然正常运行,看门狗永远不会触发复位。这种设计让看门狗完全失效,比没有看门狗更危险——因为开发者以为有保护,实际上保护是假的。
能重启只是第一步。可靠系统还要知道什么时候不该喂狗、重启后怎么定位原因、如何避免复位循环。
二、原理剖析:独立看门狗 vs 窗口看门狗与故障分类
两类看门狗的机制差异
flowchart TD A[系统运行] --> B{看门狗类型} B -->|独立看门狗 IWDG| C[计数器独立递减<br/>不依赖 CPU 时钟] C --> D[到期→全系统复位<br/>任何场景都能兜底] B -->|窗口看门狗 WWDG| E[只能在窗口期内喂狗<br/>早喂或晚喂都复位] E --> F[到期→全系统复位<br/>检测超时和过早执行] D --> G[用途:全局兜底<br/>系统彻底失控时恢复] F --> H[用途:精确时序监控<br/>任务必须在窗口内完成]独立看门狗(Independent Watchdog, IWDG):计数器由独立低速时钟(LSI,通常 32-40kHz)驱动,不依赖 CPU 主时钟。即使 CPU 主时钟故障、PLL 失锁、系统总线异常,IWDG 仍然正常递减。到期触发全系统复位。IWDG 的定位是"最后的兜底"——当软件和硬件都可能出现严重故障时,IWDG 仍然能触发复位。
窗口看门狗(Window Watchdog, WWDG):计数器由 CPU 主时钟(APB)驱动,要求喂狗必须在指定窗口内完成。窗口起点是计数器值降到某个阈值之前(不能太早喂),窗口终点是计数器降到 0x3F 之前(不能太晚喂)。过早喂狗(任务执行比预期快很多,可能跳过了关键步骤)或过晚喂狗(任务执行超时)都会触发复位。WWDG 的定位是"精确时序监控"——检测任务是否在预期时间窗口内正常完成。
故障分类与恢复策略
flowchart TD A[系统异常] --> B{故障类型} B -->|局部任务异常| C[单任务死锁/超时<br/>其他任务正常] C --> D[软件看门狗降级<br/>禁用异常任务<br/>不触发全系统复位] B -->|全局系统异常| E[多任务死锁<br/>CPU 时钟异常<br/>总线故障] E --> F[硬件看门狗复位<br/>全系统重启<br/>记录崩溃信息] B -->|启动阶段异常| G[初始化失败<br/>模型加载失败<br/>外设挂死] G --> H[启动失败计数<br/>超阈值进入安全模式<br/>等待远程修复]故障分类是看门狗设计的核心。不是所有异常都需要全系统复位:
- 局部任务异常:一个传感器采集任务死锁,推理和上传任务仍然正常。这种情况应该由软件看门狗监控,做降级处理(禁用异常任务、降低功能等级),而不是触发全系统复位。重启可能丢掉其他正常任务的状态,得不偿失。
- 全局系统异常:多任务死锁、CPU 时钟异常、总线故障。软件层已经无法自我恢复,需要硬件看门狗触发全系统复位。
- 启动阶段异常:设备启动后如果立即卡死,看门狗会不断重启形成复位循环。必须设置启动失败计数,超过阈值后进入安全模式(禁用高风险外设、回滚模型版本、降低推理频率、等待远程修复)。
两层看门狗职责不要混淆:软件层能恢复就先恢复,软件层自己也失效时,再交给硬件复位。
三、代码实现:任务心跳与喂狗绑定
任务心跳结构
// ===== 任务心跳结构 ===== // 每个关键任务定期更新心跳,喂狗任务检查所有 required 任务 typedef struct { uint32_t last_tick; // 任务最后一次更新心跳的时间戳 uint32_t max_interval_ms; // 允许的最大更新间隔 bool required; // 是否是喂狗的必要条件 bool alive; // 当前是否在允许间隔内更新了 } task_heartbeat_t; // 定义关键任务的心跳参数 // 注意:每个任务的超时阈值不同,不能用同一个值 static task_heartbeat_t task_list[] = { // 传感器任务:10ms 周期,允许 50ms 超时 { .max_interval_ms = 50, .required = true, .alive = false }, // 推理任务:100ms 周期,允许 500ms 超时 { .max_interval_ms = 500, .required = true, .alive = false }, // 上传任务:5s 周期,允许 10s 超时 { .max_interval_ms = 10000, .required = false, .alive = false }, }; #define TASK_COUNT (sizeof(task_list) / sizeof(task_list[0])) // 任务更新心跳的调用 void task_update_heartbeat(int task_id) { task_list[task_id].last_tick = get_tick_ms(); task_list[task_id].alive = true; }喂狗逻辑:健康检查绑定
// ===== 喂狗逻辑:只在健康检查通过后喂狗 ===== // 最差设计:定时器中断里无条件喂狗——业务任务死锁也不触发复位 void watchdog_feed_task(void) { uint32_t now = get_tick_ms(); bool all_required_alive = true; // 检查所有 required 任务的心跳 for (int i = 0; i < TASK_COUNT; i++) { if (task_list[i].required) { uint32_t elapsed = now - task_list[i].last_tick; if (elapsed > task_list[i].max_interval_ms) { // required 任务超时,停止喂狗 task_list[i].alive = false; all_required_alive = false; printf("Task %d timeout: elapsed=%dms, max=%dms\n", i, elapsed, task_list[i].max_interval_ms); } } } if (all_required_alive) { // 所有 required 任务正常,喂狗 IWDG_Feed(); } else { // 有 required 任务超时,停止喂狗,等待看门狗复位 // 或者触发软件降级 printf("Watchdog feed skipped: required task timeout\n"); degrade_system(); // 降级处理 } }复位记录与崩溃信息
// ===== 复位记录结构 ===== // 看门狗复位如果不记录原因,现场只会看到"设备重启了" // 至少要保存复位原因、关键任务心跳、最近错误码、固件版本和运行时长 typedef struct { uint32_t magic; // 校验标识,防止未初始化数据误读 uint32_t reset_reason; // 复位原因(IWDG / WWDG / POR / BOR / SW) uint32_t uptime_sec; // 上次运行时长 uint32_t last_error; // 最后一个错误码 uint32_t firmware_version;// 固件版本号 uint32_t model_version; // 模型版本号 uint32_t free_heap_min; // 运行期间最低堆余量 } reset_record_t; #define RESET_RECORD_MAGIC 0xWDOG2024 #define RESET_RECORD_ADDR 0x0800F000 // Flash 最后一个页,断电不丢失 // 重启后先读取并上报记录,再清除状态 void process_reset_record(void) { reset_record_t *record = (reset_record_t *)RESET_RECORD_ADDR; if (record->magic == RESET_RECORD_MAGIC) { printf("Last reset: reason=0x%X, uptime=%ds, last_error=0x%X\n", record->reset_reason, record->uptime_sec, record->last_error); printf("Firmware: v%d, Model: v%d, min_free_heap=%dKB\n", record->firmware_version, record->model_version, record->free_heap_min / 1024); // 上报到远程平台 upload_reset_record(record); // 清除记录,防止下次误读 record->magic = 0; } }启动失败计数防复位循环
// ===== 启动失败计数:防止复位循环 ===== // 设备启动后如果立即卡死,看门狗会不断重启 // 设置失败计数阈值,超过后进入安全模式 #define BOOT_FAIL_THRESHOLD 3 #define BOOT_FAIL_RECORD_ADDR 0x0800E000 typedef struct { uint32_t magic; uint32_t boot_fail_count; } boot_fail_record_t; void check_boot_fail_count(void) { boot_fail_record_t *rec = (boot_fail_record_t *)BOOT_FAIL_RECORD_ADDR; if (rec->magic == BOOT_FAIL_MAGIC) { rec->boot_fail_count++; if (rec->boot_fail_count >= BOOT_FAIL_THRESHOLD) { printf("Boot fail count=%d, entering safe mode\n", rec->boot_fail_count); enter_safe_mode(); // 禁用高风险外设,回滚模型,等待远程修复 // 安全模式下不喂看门狗,但延长超时时间 } } else { // 正常启动,初始化失败计数 rec->magic = BOOT_FAIL_MAGIC; rec->boot_fail_count = 0; } } // 正常运行一段时间后清除失败计数 void clear_boot_fail_on_stable(void) { // 运行 30 秒无异常,认为启动成功 boot_fail_record_t *rec = (boot_fail_record_t *)BOOT_FAIL_RECORD_ADDR; rec->boot_fail_count = 0; }四、边界分析:看门狗超时设置与测试验证
超时时间的精确设定
看门狗超时时间不是越短越好。太短会误杀高负载场景(推理耗时 200ms,看门狗设 100ms 就会误复位),太长又失去保护意义(系统卡死 5 秒才复位,用户体验很差)。
设定依据要结合最坏执行时间:
- 推理任务最大耗时:INT8 模型推理 + 后处理,实测最大值 × 1.5
- Flash 写入耗时:OTA 写 Flash 期间不能喂狗,要留出写入窗口
- 网络阻塞耗时:HTTP 上传超时 3s,看门狗至少覆盖这个时间
- 看门狗喂狗周期:至少比超时时间短 50%
watchdog_timeout_config: iwdg_timeout_ms: 5000 # 独立看门狗:全局兜底,稍长 wwdg_window_ms: [50, 200] # 窗口看门狗:精确时序监控 feed_period_ms: 100 # 喂狗周期:远小于超时时间 consider_flash_write: true # Flash 写入期间暂停喂狗 consider_network_timeout: true # 网络阻塞期间延长窗口看门狗测试验证清单
看门狗要纳入测试——主动制造故障验证系统行为:
- 主动死锁:让推理任务进入死循环,验证系统是否停止喂狗并触发复位
- 任务阻塞:模拟传感器任务阻塞,验证降级策略是否生效
- 内存耗尽:逐步分配内存直到堆余量不足,验证内存安全阈值是否触发模型拒绝加载
- 推理线程卡住:让 NPU 推理超时,验证看门狗是否检测到超时
- 复位记录验证:触发复位后检查崩溃记录是否正确保存和上报
- 复位循环验证:连续制造启动阶段故障,验证失败计数和安全模式是否生效
watchdog_layers: software_watchdog: task_health_and_degrade # 软件看门狗:监控+降级 hardware_watchdog: last_resort_reset # 硬件看门狗:最后兜底 report_before_reset: best_effort # 复位前尽力上报信息 boot_fail_count: 3 # 启动失败阈值 safe_mode_on_fail: true # 超阈值进入安全模式五、总结
嵌入式看门狗设计要把喂狗和任务健康绑定,不能用定时器中断无条件喂狗——那会让看门狗完全失效。
独立看门狗(IWDG)用于全局兜底,不依赖 CPU 时钟;窗口看门狗(WWDG)用于精确时序监控,检测任务执行是否在预期窗口内。故障要分类:局部任务异常由软件看门狗做降级,全局系统异常由硬件看门狗做复位,启动阶段异常用失败计数防止复位循环。
复位必须记录原因、运行时长、错误码和版本信息,远程平台才能统计故障模式。超时时间要结合最坏执行时间设定,考虑 Flash 写入和网络阻塞的窗口需求。
能重启不等于可靠。能判断系统不健康、能留下线索、能恢复服务,才是看门狗真正的价值。