4、看门狗软件设计:软件喂狗策略、多任务环境下的喂狗点设计、临界区与喂狗冲突处理

好,咱们接着聊看门狗。硬件选好了,电路也搭完了,接下来就是软件的事了。说实话,软件喂狗这块,坑比硬件多得多。我见过太多项目,硬件看门狗选得挺好,结果软件一跑,要么乱复位,要么该复位时不复位。嗯,今天我就把我在几个制动系统项目里踩过的坑,以及总结出来的经验,跟你好好说说。

4.1 软件喂狗策略:别傻乎乎地定时喂

很多人刚开始做看门狗,习惯开一个定时器,比如每100ms喂一次。这其实是个坏习惯。你想想看,如果主程序卡死了,但定时器中断还在跑,看门狗照样被喂得饱饱的,那还要它干嘛?

我个人习惯的喂狗策略,核心就一句话:只在确认系统健康后,才去喂狗。说白了,喂狗这个动作,必须跟系统的“心跳”绑定在一起,而不是跟某个定时器绑定。

核心原则:喂狗点 = 系统关键任务的“健康检查点”。

我在一个项目中遇到过这样的情况:一个传感器采集任务跑飞了,但主循环里的喂狗代码还在正常执行。结果就是,看门狗从来没复位过,但系统实际上已经半死不活了。从那以后,我定了个规矩——喂狗之前,必须先检查几个关键任务的运行状态。

常用的喂狗策略有这么几种:

  • 线性喂狗:在main循环的末尾喂一次。简单,但不够安全。
  • 分步喂狗:每个关键任务执行完后,喂一部分“狗粮”。比如一个8位的看门狗,任务A喂0x55,任务B喂0xAA,只有两个都执行了,狗才不饿。这能检测到某个任务挂掉的情况。
  • 带状态检查的喂狗:喂狗前,先检查所有关键任务的“健康标志位”。如果某个标志位没置位,说明任务挂了,那就故意不喂狗,让系统复位。

我个人最推荐第三种。代码写起来也不复杂,看个例子:

// 任务健康标志
volatile uint8_t task_heartbeat = 0;

// 任务A:采集轮速
void task_wheel_speed(void) {
    // ... 采集逻辑 ...
    task_heartbeat |= 0x01;  // 置位bit0
}

// 任务B:计算滑移率
void task_slip_ratio(void) {
    // ... 计算逻辑 ...
    task_heartbeat |= 0x02;  // 置位bit1
}

// 喂狗函数
void feed_watchdog(void) {
    // 检查所有任务是否都健康
    if (task_heartbeat == 0x03) {
        HAL_IWDG_Refresh(&hiwdg);  // 真正喂狗
        task_heartbeat = 0;        // 清空标志,等待下一轮
    }
    // 如果标志不对,就不喂狗,等着看门狗复位
}

4.2 多任务环境下的喂狗点设计

现在的制动系统,基本都跑RTOS了。FreeRTOS、uCOS、或者AUTOSAR的OS。多任务环境下,喂狗点的设计就更讲究了。

你想想看,如果每个任务都自己喂狗,那会出现什么情况?任务A喂一次,任务B喂一次,看门狗永远不超时。但任务A可能已经卡死了,只是任务B还在正常跑。这跟前面说的定时器喂狗是一个道理——失去了监控意义。

我建议的做法是:只在一个地方喂狗,而且这个喂狗任务必须是优先级最低的那个

为什么会这样?因为优先级最低的任务,只有在所有高优先级任务都执行完、或者都阻塞了,它才能得到CPU。如果它还能跑起来,说明系统整体调度是正常的。如果某个高优先级任务死循环了,低优先级任务永远得不到执行,看门狗自然就超时复位了。

我的经验:喂狗任务通常放在空闲任务(Idle Task)里,或者一个优先级为0的监控任务里。这个任务只做一件事——检查所有任务的健康状态,然后喂狗。

多任务环境下的喂狗点设计,我总结了一个表格,你可以参考:

任务类型 是否允许喂狗 理由
高优先级实时任务(如ABS控制) ❌ 不允许 一旦死循环,低优先级任务无法运行,看门狗会复位
中等优先级任务(如通信) ❌ 不允许 同上,且容易掩盖其他任务的问题
低优先级监控任务 ✅ 唯一喂狗点 它的运行本身就证明了系统调度正常
中断服务函数(ISR) ❌ 绝对不允许 中断里喂狗是最大的坑,后面会细说

4.3 临界区与喂狗冲突处理

这个问题,说实话,是很多嵌入式工程师容易忽略的。临界区,就是那些不允许被中断打断的代码段。比如操作共享变量、修改寄存器配置、或者执行一些时序敏感的指令。

进入临界区时,我们通常会关中断。但问题来了——如果喂狗操作恰好发生在临界区里,或者喂狗函数本身依赖中断,那就会出大问题。

我曾经在一个项目中遇到过这样的bug:某个临界区代码执行时间比较长,大约需要50ms。而看门狗的喂狗周期是100ms。正常情况下没问题,但有一次临界区里发生了异常,代码卡在了那里。因为中断被关了,看门狗的中断喂狗方式失效了,但临界区代码又出不来,结果就是——看门狗超时复位了。嗯,这次复位其实是正确的,但问题是,我们本来希望它能更早地检测到异常。

处理临界区与喂狗的冲突,我有几个建议:

  • 不要在临界区里喂狗。 这是铁律。喂狗操作应该放在临界区外面,确保它不会被关中断影响。
  • 如果临界区时间太长,考虑分段执行。 把一个大临界区拆成几个小段,每段之间开中断,让喂狗有机会执行。
  • 使用“软狗”辅助。 在临界区入口处,记录一个时间戳。如果退出临界区时,发现时间已经超过了某个阈值,就主动触发软件复位。这相当于在临界区内部加了一个“软看门狗”。

警告:千万不要在中断服务函数(ISR)里喂狗!我见过有人为了省事,在定时器中断里喂狗。结果主程序死循环了,中断还在跑,看门狗永远不复位。这种设计,等于把看门狗废掉了。

最后,分享一个我常用的临界区喂狗保护代码:

// 临界区入口
void enter_critical_section(void) {
    __disable_irq();  // 关中断
    critical_start_tick = get_system_tick();  // 记录进入时间
}

// 临界区出口
void exit_critical_section(void) {
    uint32_t elapsed = get_system_tick() - critical_start_tick;
    
    // 如果临界区执行时间超过了看门狗周期的80%,主动报警
    if (elapsed > (WDT_TIMEOUT_MS * 0.8)) {
        // 记录错误日志,或者触发软件复位
        system_error_handler(ERROR_CRITICAL_SECTION_TIMEOUT);
    }
    
    __enable_irq();  // 开中断
    // 注意:喂狗操作放在这里,不要在临界区内部喂
    feed_watchdog();
}

嗯,关于看门狗的软件设计,今天就先聊这么多。记住一句话:喂狗不是目的,监控系统健康才是。下一节,咱们聊聊看门狗的故障注入测试,这可是验证你的看门狗设计是否靠谱的关键一步。