3. Tick定时器详解:wdStart()与wdCancel()函数、看门狗定时器的工作原理、定时器回调函数

好,咱们接着聊Tick定时器。说实话,在VxWorks里干活,Tick定时器是我用得最多的机制之一。它不像系统时钟Tick那样默默无闻,也不像POSIX定时器那样“重”,它就是一个轻量级、精准、好用的工具。今天我就把看门狗定时器(Watchdog Timer)的底裤给你扒干净。

3.1 看门狗定时器到底是什么?

你想想看,名字叫“看门狗”,但它跟硬件看门狗不是一回事。硬件看门狗是怕系统死机,软件看门狗是怕任务超时。说白了,它就是一个基于系统Tick的定时器,你设定一个时间,时间到了,它就会调用你注册的回调函数。

我习惯叫它“一次性定时器”。因为它触发一次就结束,不会自动重载。如果你想循环触发,那得在回调函数里重新启动它。

核心特点:

  • 基于系统Tick,精度受Tick周期限制
  • 一次性触发,用完即止
  • 在系统时钟中断的上下文中执行回调
  • 轻量级,开销极小

3.2 两个核心函数:wdStart() 与 wdCancel()

这两个函数,一个启动,一个取消。咱们一个一个说。

3.2.1 wdStart() —— 启动定时器

函数原型长这样:

STATUS wdStart
    (
    WDOG_ID   wdId,      /* 看门狗定时器ID */
    int       delay,     /* 延迟的Tick数 */
    FUNCPTR   routine,   /* 回调函数指针 */
    int       parameter  /* 传给回调函数的参数 */
    )

参数不多,但每个都有讲究:

  • wdId:你得先通过 wdCreate() 创建一个看门狗定时器,拿到这个ID。
  • delay:延迟多少个Tick。比如系统Tick周期是10ms,你填50,那就是500ms后触发。
  • routine:回调函数。注意,这个函数会在中断上下文执行,所以不能阻塞
  • parameter:传给回调函数的参数。通常传一个整数,或者传一个结构体指针。

我的经验: 回调函数里千万别调用 printf() 或者 malloc() 这类可能引起阻塞的函数。我曾经在一个项目里这么干过,结果系统直接挂了。后来排查了半天才发现是回调函数里调用了 semTake(),导致中断上下文里发生了任务切换——嗯,这是个经典错误。

3.2.2 wdCancel() —— 取消定时器

有时候你启动了定时器,但条件提前满足了,不想等它触发。这时候就用 wdCancel():

STATUS wdCancel
    (
    WDOG_ID wdId    /* 要取消的看门狗定时器ID */
    )

调用后,定时器就被取消了。如果定时器已经触发,那调用也没意义了。所以一般会在条件判断里先检查一下。

3.3 看门狗定时器的工作原理

咱们深入一点,看看它底层是怎么跑的。

每个看门狗定时器其实就是一个数据结构,挂在系统的一个链表上。系统时钟中断每次到来时,都会遍历这个链表,把每个定时器的计数值减1。当计数值减到0时,就调用对应的回调函数。

流程大概是这样的:

  1. 你调用 wdStart(),系统把定时器插入到链表中。
  2. 每个Tick到来,系统时钟中断服务程序(ISR)遍历链表。
  3. 找到计数值为0的定时器,执行回调函数。
  4. 执行完后,定时器自动从链表中移除。

注意: 回调函数是在中断上下文中执行的。这意味着:

  • 不能调用可能阻塞的函数(如 semTake()、msgQSend() 等)
  • 不能执行耗时操作,否则会影响系统实时性
  • 不能访问可能被中断嵌套破坏的共享资源

3.4 定时器回调函数的设计要点

回调函数是看门狗定时器的灵魂。设计得好,系统稳如老狗;设计得不好,系统崩得稀碎。

我总结了几条铁律:

原则 说明 我的建议
短小精悍 回调函数执行时间要短 只做必要的状态标记或数据更新
不可阻塞 不能调用任何可能阻塞的API 用信号量或消息队列通知任务处理
不可重入 注意中断嵌套问题 加锁要小心,最好用中断锁
参数传递 只能传一个整数参数 传结构体指针时确保内存有效

来看一个实际例子:

/* 回调函数 */
void myTimerCallback(int param)
{
    /* 这里不能阻塞!不能调用printf! */
    /* 只是设置一个标志位 */
    *((int *)param) = 1;
}

/* 任务中启动定时器 */
void myTask(void)
{
    WDOG_ID wdId;
    int flag = 0;
    
    wdId = wdCreate();
    if (wdId == NULL)
    {
        /* 创建失败处理 */
        return;
    }
    
    /* 启动定时器,500个Tick后触发 */
    wdStart(wdId, 500, myTimerCallback, (int)&flag);
    
    /* 任务继续做其他事 */
    while (1)
    {
        if (flag)
        {
            /* 定时器触发了,处理业务逻辑 */
            flag = 0;
            /* ... */
        }
        taskDelay(1);
    }
}

你看,回调函数里只设置了一个标志位,真正的业务逻辑放在任务里处理。这样既保证了实时性,又避免了中断上下文里的复杂操作。

3.5 避坑指南

做嵌入式这么多年,我在看门狗定时器上踩过的坑,说出来都是泪。分享几个典型的:

我曾经犯过的错误:

  • 忘记取消定时器: 任务退出时没调用 wdCancel(),结果定时器还在跑,回调函数访问了已经释放的内存——直接死机。
  • 回调函数里调用 taskDelay(): 中断上下文里调用 taskDelay(),系统直接 panic。我当时还纳闷为什么一跑就挂。
  • 多个定时器共享同一个回调参数: 两个定时器都用同一个变量作为参数,结果一个触发了,另一个还没触发,变量被改了——逻辑全乱。

所以我的建议是:

  • 每个定时器用独立的参数,别共享。
  • 回调函数里只做最轻量的事,复杂逻辑交给任务。
  • 任务退出前,一定要取消所有定时器。

3.6 什么时候用看门狗定时器?

我总结了几种典型场景:

  • 超时检测: 等待某个事件,如果超时了就做异常处理。
  • 周期性任务: 虽然是一次性的,但可以在回调里重新启动,实现周期执行。
  • 延迟执行: 想延迟一段时间再执行某个操作,用看门狗定时器最方便。
  • 硬件操作超时: 比如等待外设响应,超时了就复位外设。

嗯,看门狗定时器就聊到这儿。它虽然简单,但用好了能解决很多实际问题。下一节咱们聊聊POSIX定时器,那个更灵活,但也更复杂一些。