1. VxWorks定时器基础:看门狗定时器原理与API详解
各位同学,咱们今天聊聊VxWorks里的定时器。说实话,定时器这东西,在嵌入式系统里就跟呼吸一样自然——你几乎找不到哪个项目不用它。我个人做过的项目里,从无人机飞控到工业PLC,从通信基站到医疗设备,定时器无处不在。
VxWorks里最基础、最常用的定时器机制,就是看门狗定时器(Watchdog Timer)。别被名字吓到,它其实没那么神秘。
1.1 看门狗定时器原理
看门狗定时器,说白了就是一个“倒计时闹钟”。你设定一个时间,比如10个tick,然后系统开始倒计时。时间到了,它就触发一个你预先注册好的回调函数。
为什么会叫“看门狗”这个名字?我猜最早做这个功能的人,大概是想表达“像看门狗一样,时间一到就叫唤”的意思。嗯,挺形象的。
在VxWorks里,看门狗定时器有几个关键特性:
- 一次性触发:默认只触发一次,不是周期性的。想重复用?得手动重新启动。
- 软定时器:它运行在系统tick中断的上下文中,不是独立线程。
- 轻量级:资源开销很小,创建和销毁都很快。
核心要点:看门狗定时器的回调函数是在中断上下文中执行的。这意味着你的回调函数必须非常快,不能做阻塞操作,不能调用可能引起阻塞的API(比如semTake、malloc等)。
我记得刚入行那会儿,有个同事在回调函数里调用了printf,结果系统直接挂了。排查了半天才发现是这个问题。你想想看,中断上下文里做IO操作,这不是给自己挖坑吗?
1.2 wdCreate — 创建看门狗
创建看门狗很简单,就一个函数调用:
WDOG_ID wdCreate(void);
这个函数没有参数,返回一个WDOG_ID类型的句柄。如果创建失败,返回NULL。
我个人的习惯是,创建后立即检查返回值:
WDOG_ID myWatchdog = wdCreate();
if (myWatchdog == NULL) {
/* 处理错误,比如打印日志或返回错误码 */
logMsg("Failed to create watchdog timer!\n", 0,0,0,0,0);
return ERROR;
}
嗯,这里要注意:看门狗定时器是系统资源,数量有限。我记得在某个老版本的VxWorks里,默认最多只能创建20个。虽然现在版本已经放宽了很多,但你还是得心里有数。
1.3 wdStart — 启动定时器
创建完看门狗,接下来就是启动它:
STATUS wdStart
(
WDOG_ID wdogId, /* 看门狗句柄 */
int tickDelay, /* 延迟的tick数 */
FUNCPTR routine, /* 回调函数指针 */
int parameter /* 传给回调函数的参数 */
);
参数说明:
- wdogId:wdCreate返回的句柄
- tickDelay:延迟多少个系统tick后触发。注意,最小值为1,设为0不会立即触发
- routine:回调函数,原型是 void func(int param)
- parameter:传给回调函数的参数,就一个int
举个例子:
void myCallback(int param) {
/* 注意:这里是在中断上下文! */
logMsg("Watchdog triggered! param = %d\n", param,0,0,0,0);
}
void startMyWatchdog(void) {
WDOG_ID wd = wdCreate();
if (wd != NULL) {
wdStart(wd, 100, myCallback, 42);
/* 100个tick后,myCallback(42)会被调用 */
}
}
小技巧:如果你需要传递多个参数,可以把它们打包成一个结构体,然后把结构体的指针强转成int传进去。当然,要确保这个指针在中断上下文里是有效的。
1.4 wdCancel — 取消定时器
有时候定时器启动了,但条件变了,不想让它触发了。这时候就用wdCancel:
STATUS wdCancel(WDOG_ID wdogId);
调用后,如果定时器还没触发,它就不会再触发了。如果已经触发了,那取消也没意义了。
我曾经在项目中遇到过一个bug:某个模块在卸载时没有取消看门狗,结果模块的内存已经被释放了,看门狗回调却还在执行,直接访问了非法地址。嗯,血泪教训啊。
警告:在删除看门狗之前,一定要先调用wdCancel!否则回调函数可能在删除后仍然触发,导致野指针访问。
1.5 wdDelete — 删除看门狗
用完了,就该释放资源:
STATUS wdDelete(WDOG_ID wdogId);
正确的使用流程是:
/* 先取消 */
wdCancel(myWatchdog);
/* 再删除 */
wdDelete(myWatchdog);
myWatchdog = NULL; /* 好习惯,防止悬空指针 */
1.6 定时器精度与系统tick的关系
这是很多新手容易迷糊的地方。看门狗定时器的精度,直接取决于系统tick的频率。
系统tick,就是VxWorks内核的心跳。默认情况下,一个tick是1毫秒(即系统时钟频率为1000Hz)。但这不是固定的,你可以通过修改config.h里的配置来调整。
| 系统tick频率 | 每个tick时长 | 看门狗精度 |
|---|---|---|
| 100 Hz | 10 ms | ±10 ms |
| 1000 Hz | 1 ms | ±1 ms |
| 10000 Hz | 100 μs | ±100 μs |
你想想看,如果你把tick频率设成100Hz,那一个tick就是10毫秒。这时候你调用wdStart(wd, 1, callback, 0),实际延迟是10毫秒,不是1毫秒!
我有个朋友(其实就是我自己)曾经犯过这个错:在100Hz的系统上,想实现5毫秒的定时,结果设了tickDelay=5,以为就是5毫秒。实际上等了50毫秒才触发。排查了半天才发现是tick频率的问题。
关键公式:实际延迟时间 = tickDelay × 每个tick的时长
所以,如果你需要精确的毫秒级定时,建议把系统tick频率设到1000Hz以上。但要注意,tick频率越高,CPU在中断处理上的开销就越大,这是个权衡。
1.7 避坑指南
最后,我总结几个实战中容易踩的坑:
- 回调函数里别做重活:中断上下文里,时间就是生命。能做的事:设置标志位、释放信号量、记录日志。不能做的事:malloc、semTake、taskDelay、printf。
- 注意tick溢出:tickDelay是int类型,最大能设到2^31-1。但如果你设得太大,比如接近这个上限,要考虑系统运行时间会不会导致tick计数器溢出。
- 多次启动同一个看门狗:如果你对同一个WDOG_ID连续调用wdStart,后面的调用会覆盖前面的。也就是说,只有最后一次设置的定时器会生效。
- 删除前一定要取消:这个我前面说过,但值得再强调一遍。我曾经因为这个bug,在生产环境上排查了整整两天。
好了,这一章的内容就到这里。看门狗定时器虽然基础,但它是VxWorks定时任务的基石。下一章我们会讲怎么用看门狗实现周期性任务,以及更高级的定时器机制。到时候见!