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时,就调用对应的回调函数。
流程大概是这样的:
- 你调用
wdStart(),系统把定时器插入到链表中。 - 每个Tick到来,系统时钟中断服务程序(ISR)遍历链表。
- 找到计数值为0的定时器,执行回调函数。
- 执行完后,定时器自动从链表中移除。
注意: 回调函数是在中断上下文中执行的。这意味着:
- 不能调用可能阻塞的函数(如 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定时器,那个更灵活,但也更复杂一些。