4. wdStart函数详解:函数原型、延迟时间参数(tick数)、回调函数注册、回调函数参数传递
好,咱们接着聊看门狗的核心操作——启动它。上一章我们讲了怎么创建看门狗,但创建完它只是个空壳子,得用 wdStart() 把它真正跑起来。这个函数,说实在的,是看门狗使用中最容易出问题的地方。我见过不少同事,看门狗创建得好好的,一启动就翻车,十有八九都是 wdStart() 的参数没搞明白。
4.1 函数原型长什么样?
先看原型,心里有个底:
STATUS wdStart
(
WDOG_ID wdId, /* 看门狗ID,由wdCreate返回 */
int delay, /* 延迟时间,单位是tick */
FUNCPTR routine, /* 回调函数指针 */
int parameter /* 传给回调函数的参数 */
)
返回值是 STATUS 类型,成功返回 OK,失败返回 ERROR。嗯,这里要注意,wdStart() 本身不会阻塞,它只是把看门狗挂到系统的心跳队列里,然后立即返回。你调用完它,程序继续往下走,时间到了回调函数才被触发。
核心要点: wdStart 是异步的,它只管“登记”,不管“执行”。回调函数是在系统tick中断的上下文中执行的,不是在任务上下文中。
4.2 延迟时间参数——tick数到底怎么算?
第二个参数 delay,单位是 tick。这个 tick 是什么?说白了就是系统时钟中断的周期。比如你的系统时钟频率是 100Hz,那一个 tick 就是 10 毫秒。如果你想让看门狗在 1 秒后触发,delay 就填 100。
我个人习惯用 sysClkRateGet() 来获取当前系统的 tick 频率,然后做换算:
int ticksPerSecond = sysClkRateGet();
int delayInTicks = ticksPerSecond * 2; /* 2秒后触发 */
wdStart(myWd, delayInTicks, myCallback, (int)myArg);
为什么我不直接写死数字?因为不同板子的 tick 频率可能不一样。我在一个项目里吃过这个亏——开发板用的是 100Hz,现场设备用的是 60Hz,结果看门狗提前触发了,系统反复重启。排查了半天才发现是 tick 频率不一致。
避坑指南: delay 参数如果填 0,看门狗会在下一个 tick 立即触发。我曾经见过有人想“停用”看门狗,就传了个 0 进去,结果系统直接崩了。停用看门狗请用 wdCancel(),别耍小聪明。
另外,delay 是 int 类型,最大能填多少?理论上取决于你的 tick 频率和 int 的位数。32位系统下,最大正数是 2^31 - 1。假设 tick 频率是 100Hz,那最大延迟大约是 248 天。够用了吧?但要注意,如果你填了负数,wdStart() 会返回 ERROR,不会启动。
4.3 回调函数注册——routine 参数
第三个参数 routine,是函数指针。类型是 FUNCPTR,在 VxWorks 里它本质上就是 void (*)(int)。也就是说,回调函数必须接收一个 int 参数,返回 void。
举个例子:
void myWatchdogCallback(int userParam)
{
/* 看门狗超时了,赶紧处理 */
logMsg("Watchdog triggered! param = %d\n", userParam, 0, 0, 0, 0, 0);
}
注册的时候直接传函数名:
wdStart(myWd, 100, myWatchdogCallback, 0);
这里有个细节:回调函数是在中断上下文中执行的。这意味着你不能在里面调用那些会阻塞的函数,比如 taskDelay()、semTake() 带超时、malloc() 等。我刚开始做 VxWorks 时,在回调里调了个 printf(),结果系统直接死锁。后来才知道,printf() 内部用了信号量,中断里拿信号量?那不就是找死嘛。
小技巧: 回调函数里尽量只做轻量级操作,比如设置一个标志位、发一个消息到任务队列。真正复杂的处理,交给一个高优先级的任务去轮询这个标志位。
4.4 回调函数参数传递——parameter 参数
第四个参数 parameter,是传给回调函数的参数。类型是 int。你可能会问,就一个 int,够用吗?
说实话,一个 int 确实有点局促。但 VxWorks 的设计就是这样,它把参数设计成 int,是为了在 32 位系统上能直接传地址。你想想看,一个指针在 32 位系统上正好是 4 字节,跟 int 一样大。所以我们可以传结构体指针:
typedef struct {
int deviceId;
char* errorMsg;
int retryCount;
} MyDeviceInfo;
MyDeviceInfo devInfo;
devInfo.deviceId = 1;
devInfo.errorMsg = "Timeout on channel A";
devInfo.retryCount = 0;
wdStart(myWd, 200, myCallback, (int)&devInfo);
然后在回调里强转回来:
void myCallback(int param)
{
MyDeviceInfo* pInfo = (MyDeviceInfo*)param;
logMsg("Device %d timeout: %s\n", pInfo->deviceId, pInfo->errorMsg, 0, 0, 0, 0);
}
嗯,这里要注意一个坑:你传进去的指针指向的内存,在回调触发时必须是有效的。如果那个内存被释放了或者被覆盖了,回调里拿到的就是垃圾数据。我在一个项目中遇到过这种情况——传了一个局部变量的地址进去,结果函数返回后局部变量被回收,看门狗触发时直接访问了非法地址,系统 panic。
避坑指南: 传给 wdStart 的参数,如果是指针,请确保它指向的内存是全局的、静态的或者动态分配的(且不会被释放)。局部变量?想都别想。
如果你不需要传参数,直接传 0 就行。回调函数里可以忽略这个参数。
4.5 一个完整的例子
把上面这些串起来,写个完整的示例:
#include <vxWorks.h>
#include <wdLib.h>
#include <logLib.h>
/* 全局看门狗ID */
WDOG_ID gWdId = NULL;
/* 设备信息结构体 */
typedef struct {
int slotId;
char description[32];
} DeviceInfo;
/* 设备信息实例(全局,确保生命周期) */
DeviceInfo gDevInfo = {3, "Sensor array"};
/* 回调函数 */
void sensorWatchdog(int param)
{
DeviceInfo* pDev = (DeviceInfo*)param;
logMsg("Watchdog: Slot %d (%s) timeout!\n",
pDev->slotId, pDev->description, 0, 0, 0, 0);
/* 这里可以发消息给任务,做进一步处理 */
}
/* 启动看门狗 */
void startSensorWatchdog(void)
{
int tickRate = sysClkRateGet();
int delay = tickRate * 5; /* 5秒超时 */
gWdId = wdCreate();
if (gWdId == NULL) {
logMsg("Failed to create watchdog\n", 0, 0, 0, 0, 0, 0);
return;
}
if (wdStart(gWdId, delay, sensorWatchdog, (int)&gDevInfo) != OK) {
logMsg("Failed to start watchdog\n", 0, 0, 0, 0, 0, 0);
wdDelete(gWdId);
gWdId = NULL;
}
}
这个例子展示了完整的流程:创建、配置参数、启动。注意我把 gDevInfo 定义成了全局变量,就是为了避免生命周期问题。
4.6 关于 wdStart 的几点补充
- 重复启动:如果你对一个已经启动的看门狗再次调用
wdStart(),它会先取消之前的定时,然后用新的参数重新启动。这个行为在某些场景下很有用,比如“喂狗”操作——其实就是重新启动看门狗,重置计时。 - 中断上下文:再次强调,回调函数在中断中执行。不要在回调里做任何可能引起阻塞的操作。
- 参数类型:虽然
parameter是int,但在 64 位系统上,指针是 8 字节,int 是 4 字节。如果你在 64 位 VxWorks 上开发,传指针会有截断风险。这时候可以考虑用ULONG或者直接传一个索引值,通过全局数组来查找。
好了,wdStart() 就讲到这里。下一章我们聊聊怎么停止看门狗——wdCancel(),以及一些常见的“喂狗”模式。记住,看门狗用得好,系统稳如老狗;用不好,那就是给自己挖坑。