第二课:看门狗定时器实战

好,咱们直接进入正题。看门狗定时器(Watchdog Timer)在VxWorks里,说白了就是一个「倒计时炸弹」。你必须在它爆炸前喂狗,否则系统就重启。但今天我要讲的,不是那个系统级的看门狗,而是VxWorks提供的一个更灵活的工具——看门狗定时器库(wdLib)。

我个人习惯把它叫做「软看门狗」。它不重启系统,而是到期后执行一个你指定的回调函数。这个特性,用来做周期性任务调度器,简直不要太顺手。

2.1 创建周期性任务调度器

先说说思路。你想让一个任务每隔100ms执行一次,怎么做?

最笨的办法是在任务里写个死循环,调用taskDelay()。但这样有个问题:如果任务被阻塞了,或者优先级被抢占了,周期就不准了。看门狗定时器不一样,它依赖系统时钟中断,精度高得多。

来看代码:

#include <vxWorks.h>
#include <wdLib.h>
#include <sysLib.h>

WDOG_ID wdId;
int counter = 0;

void myPeriodicTask(void)
{
    counter++;
    printf("第 %d 次执行\n", counter);
    
    // 重新启动看门狗,实现周期性
    if (wdStart(wdId, sysClkRateGet() / 10, (FUNCPTR)myPeriodicTask, 0) != OK)
    {
        printf("看门狗重启失败\n");
    }
}

void initScheduler(void)
{
    wdId = wdCreate();
    if (wdId == NULL)
    {
        printf("创建看门狗失败\n");
        return;
    }
    
    // 启动,100ms后执行第一次
    wdStart(wdId, sysClkRateGet() / 10, (FUNCPTR)myPeriodicTask, 0);
}

这里有个关键点:wdStart()的第二个参数是tick数。我用了sysClkRateGet() / 10,假设系统时钟是1000Hz,那就是100个tick,对应100ms。你想想看,如果系统时钟改了,这个代码依然能自适应,是不是很优雅?

我的小技巧: 别把回调函数写得太复杂。看门狗回调是在中断上下文执行的,你敢在里面调用printf或者malloc,系统分分钟给你脸色看。我一般只在回调里发个信号量,或者设置一个标志位,真正的处理逻辑交给任务去做。

2.2 实现超时处理回调函数

回调函数的设计,其实挺有讲究的。我见过不少新手,把回调函数写得跟普通函数一样,结果踩了坑。

看门狗回调的原型是:

void callback(WDOG_ID wdId, int parameter);

注意,它只有一个参数,就是你在wdStart()里传的那个。如果你想传多个参数,怎么办?

嗯,这里有个土办法。我习惯用一个全局结构体,把需要的数据都塞进去,然后传结构体的指针:

typedef struct {
    int taskId;
    char taskName[32];
    int timeoutCount;
} TaskInfo;

TaskInfo g_taskInfo;

void timeoutHandler(WDOG_ID wdId, int parameter)
{
    TaskInfo *pInfo = (TaskInfo *)parameter;
    
    // 检查超时次数
    pInfo->timeoutCount++;
    if (pInfo->timeoutCount > 3)
    {
        printf("任务 %s 连续超时 %d 次,需要处理!\n", 
               pInfo->taskName, pInfo->timeoutCount);
        // 这里可以触发错误处理逻辑
    }
}

我曾经在一个项目中,用这个机制实现了「心跳检测」。每个任务定期喂狗,如果某个任务连续3次没喂,我就认为它挂了,然后自动重启它。这个方案后来成了那个产品的标配功能。

2.3 多定时器并发管理

一个看门狗不够用?那就创建多个。VxWorks支持同时创建多个看门狗定时器,只要系统内存够。

但问题来了:多个定时器同时跑,你怎么管理它们?

我建议用数组或者链表来管理:

#define MAX_TIMERS 10

typedef struct {
    WDOG_ID wdId;
    BOOL inUse;
    char name[16];
    int interval;  // 周期,单位tick
} TimerEntry;

TimerEntry timerPool[MAX_TIMERS];

STATUS createTimer(char *name, int interval, FUNCPTR callback, int arg)
{
    int i;
    for (i = 0; i < MAX_TIMERS; i++)
    {
        if (!timerPool[i].inUse)
        {
            timerPool[i].wdId = wdCreate();
            if (timerPool[i].wdId == NULL)
                return ERROR;
            
            timerPool[i].inUse = TRUE;
            strncpy(timerPool[i].name, name, 15);
            timerPool[i].interval = interval;
            
            wdStart(timerPool[i].wdId, interval, callback, arg);
            return OK;
        }
    }
    return ERROR;  // 池子满了
}

你看,这样管理起来就清晰多了。每个定时器有自己的名字、周期和状态。调试的时候,打印一下这个数组,哪个定时器在跑、哪个是空闲的,一目了然。

注意: 多个看门狗的回调函数是在同一个中断级别执行的。如果你的回调函数里做了耗时操作,会影响其他定时器的精度。我一般建议回调函数执行时间不要超过100微秒。

2.4 定时器资源泄漏排查

这是个大坑。我见过太多人,创建了看门狗不删除,结果系统跑着跑着就崩了。

为什么会这样?因为每个wdCreate()都会分配内存。如果你在循环里不断创建,又不删除,内存迟早被吃光。

排查资源泄漏,我有三板斧:

  1. wdShow()查看当前看门狗数量——VxWorks Shell里直接敲这个命令,能看到当前有多少个看门狗在活动。
  2. 代码审查时重点检查wdCreate()wdDelete()的配对——我习惯在创建时记录日志,删除时也记录日志,跑一段时间后grep一下,看数量是否匹配。
  3. 使用内存检测工具——比如Wind River的MemScope,或者自己写一个简单的内存分配计数器。

我曾经接手过一个项目,系统运行72小时后必死。排查了三天,最后发现是一个任务在异常处理分支里忘了调用wdDelete()。每次异常发生,就泄漏一个看门狗。72小时,刚好把内存耗光。

那之后,我给自己定了个规矩:所有看门狗的创建和删除,必须成对出现,而且要在同一个函数层级。比如在init()里创建,就在cleanup()里删除,绝不跨函数。

避坑指南: 我曾经在回调函数里调用wdDelete()删除自己,结果系统直接挂掉。原因是看门狗回调执行时,定时器本身还在活跃状态,你删它等于「自杀」。正确的做法是:在回调里设置一个标志,让外部任务来删除。

好了,这一课的内容就这些。总结一下:看门狗定时器是个好东西,但用不好就是定时炸弹。记住三点——回调要轻量、管理要规范、删除要彻底。下一课,咱们聊聊POSIX定时器,那又是另一番天地了。