第4章:软件看门狗实现:MCU内部看门狗配置、喂狗策略、多任务环境下的看门狗管理
各位工程师朋友,咱们接着聊看门狗。上一章讲了硬件看门狗的原理,这一章咱们深入软件层面——说白了,就是怎么把MCU内部那个看门狗外设用起来,让它真正干活,而不是摆设。
我见过不少项目,看门狗是开了,但喂狗喂得毫无章法。结果呢?该复位的时候不复位,不该复位的时候乱复位。嗯,这比没看门狗还可怕。
4.1 MCU内部看门狗配置——别让初始化变成埋雷
先说说配置。不同MCU的看门狗外设大同小异,但有几个关键参数你必须搞清楚:
- 时钟源:是独立RC振荡器还是系统时钟分频?独立RC通常更可靠,但精度差。
- 超时时间:从最后一次喂狗到复位触发的时间窗口。
- 窗口模式:有些MCU支持早期喂狗触发复位,防止你喂得太勤快。
- 调试模式行为:仿真暂停时,看门狗是否继续跑?
我个人习惯,在项目初期就把看门狗配置封装成一个独立模块。别等到最后才加,那时候改起来牵一发动全身。
核心原则:看门狗配置必须在系统时钟稳定之后、任务调度启动之前完成。我曾经见过有人把看门狗初始化放在main函数第一行,结果时钟还没稳,看门狗已经开始倒计时了——系统反复复位,连调试器都连不上。
来看一个典型的STM32 IWDG配置代码:
// 独立看门狗初始化
void IWDG_Init(uint8_t prescaler, uint16_t reload) {
// 1. 使能写访问
IWDG->KR = 0x5555;
// 2. 配置预分频器(4分频 ~ 256分频)
IWDG->PR = prescaler;
// 3. 设置重装载值(12位,最大4095)
IWDG->RLR = reload;
// 4. 等待寄存器更新
while(IWDG->SR & IWDG_SR_PVU);
while(IWDG->SR & IWDG_SR_RVU);
// 5. 启动看门狗(写入0xCCCC)
IWDG->KR = 0xCCCC;
}
注意:IWDG一旦启动,除非系统复位,否则无法停止。所以调试阶段我建议先用窗口看门狗(WWDG)或者软件模拟看门狗,等逻辑稳定了再切到硬件IWDG。
4.2 喂狗策略——不是随便找个地方喂就完事
喂狗看似简单,写个寄存器就行。但喂狗的位置、频率、条件,直接决定了看门狗能不能起到保护作用。
我总结了几种常见的喂狗策略,你根据实际情况选:
| 策略名称 | 实现方式 | 适用场景 | 风险 |
|---|---|---|---|
| 主循环喂狗 | 在while(1)末尾喂狗 | 单任务、简单轮询系统 | 子任务死锁但主循环仍在跑时失效 |
| 定时器喂狗 | 在定时器中断中喂狗 | 需要精确时间窗口的系统 | 中断正常但主逻辑卡死时无法检测 |
| 分步喂狗 | 多个关键节点各喂一部分 | 多任务、复杂流程 | 实现复杂,容易遗漏 |
| 心跳喂狗 | 独立任务周期性喂狗 | RTOS多任务环境 | 需要任务间通信机制 |
我个人最不推荐的是「定时器喂狗」。为什么?你想想看,中断还能正常触发,但主循环可能早就卡死在某个死循环里了。这种喂狗方式,说白了就是掩耳盗铃。
我的经验:在医疗设备中,我通常采用「分步喂狗 + 主循环喂狗」的组合策略。每个关键任务完成后设置一个标志位,主循环检查所有标志位都置位后才喂狗。这样任何一个任务卡死,看门狗都会复位系统。
4.3 多任务环境下的看门狗管理——RTOS的喂狗艺术
到了RTOS环境,事情就复杂了。你不可能让每个任务都去喂狗,那样会乱套。但你又不能只让空闲任务喂狗——万一高优先级任务卡死了,空闲任务还在跑,看门狗永远不会复位。
我常用的方法是「看门狗任务 + 任务监控表」:
// 任务监控表结构
typedef struct {
TaskHandle_t taskHandle; // 任务句柄
uint32_t lastHeartbeat; // 上次心跳时间戳
uint32_t timeoutMs; // 超时阈值
uint8_t status; // 0:正常 1:超时
} TaskMonitor_t;
// 看门狗监控任务
void WatchdogTask(void *param) {
while(1) {
// 遍历所有被监控任务
for(int i = 0; i < TASK_MONITOR_COUNT; i++) {
uint32_t now = GetSysTick();
if((now - monitorTable[i].lastHeartbeat) > monitorTable[i].timeoutMs) {
// 任务超时,记录错误但不立即复位
LogError("Task %s timeout!", pcTaskGetName(monitorTable[i].taskHandle));
monitorTable[i].status = 1;
}
}
// 所有任务都正常?喂狗!
if(AllTasksAlive()) {
IWDG_Feed();
}
vTaskDelay(pdMS_TO_TICKS(100)); // 每100ms检查一次
}
}
每个被监控的任务,需要在它的主循环里定期更新自己的心跳时间戳:
void SensorTask(void *param) {
while(1) {
// 任务主体逻辑
ReadSensor();
ProcessData();
// 更新心跳
monitorTable[SENSOR_TASK_INDEX].lastHeartbeat = GetSysTick();
monitorTable[SENSOR_TASK_INDEX].status = 0;
vTaskDelay(pdMS_TO_TICKS(50));
}
}
关键点:看门狗任务本身也要被监控。我通常让看门狗任务的优先级设为中等,既不会被高优先级任务完全抢占,也不会影响关键实时任务。另外,看门狗任务的栈空间要留足——我遇到过因为栈溢出导致看门狗任务自己先挂了的惨案。
4.4 避坑指南——我曾经踩过的雷
做医疗设备这些年,看门狗相关的坑我踩了不少。挑几个典型的说说:
- 调试阶段忘记关闭看门狗:单步调试时,看门狗一直在跑。你刚设个断点喝口水,回来发现系统复位了。我现在的做法是:在调试配置里加个宏,DEBUG模式下看门狗初始化直接跳过。
- 喂狗函数放在中断里:中断频繁触发,看门狗永远喂得饱饱的。但主循环早就卡死了。这种bug最难查,因为看门狗不会复位,你只能靠串口日志慢慢分析。
- 窗口模式配置错误:有些MCU的窗口看门狗要求喂狗时间必须在窗口内。太早喂或者太晚喂都会触发复位。我刚开始用的时候没仔细看手册,结果系统每隔几秒就复位一次,查了两天才发现是喂狗喂得太勤快了。
- 低功耗模式下看门狗还在跑:进入休眠前没暂停看门狗,结果休眠期间看门狗超时复位,设备莫名其妙重启。这个在电池供电的医疗设备里特别坑。
最后提醒一句:看门狗是最后一道防线,不是第一道。别指望看门狗能解决所有问题。该做的异常处理、状态检查、数据校验,一个都不能少。看门狗只负责在你没兜住的时候,把系统拉回来。
好了,这一章就到这里。下一章咱们聊聊「看门狗在IEC 62304中的认证要求」,这个在医疗设备认证里是必考题,到时候我会结合我实际过认证的经验来讲。