VxWorks系统提供灵活多样的定时器机制,有多种接口可以方便地实现延时,文章列举几种常用的延时方法,并对这几种延时方式的适用条件和注意事项作了说明。

嵌入式系统中,一个任务往往需要在特定的延时之后去执行一个指定的动作,比如等待外设以确保数据可靠,控制扬声器发声时间,串口通信超时重发等等。这就需要利用定时器机制来计量特定长度的时间段。VxWorks作为实时嵌入式系统,提供多样的定时接口函数。以下列举一些常用的定时方式及其注意事项。

taskDelay

taskDelay(n) 使调用该函数的任务延时n个tick(内核时钟周期),该任务在指定的时间内主动放弃CPU,除了taskDelay(0) 专用于任务调度(将CPU交给同一优先级的其他任务)外,任务延时经常也用于等待某一外部事件,作为一种定时/延时机制。在没有中断触发的时候,taskDelay能很方便实现又不影响系统整体性能。比如需要写数据至EEPROM,EEPROM需要一个内部擦除时间(最大擦除时间为10ms)。以下文章所提及的一个tick都假设为16.67ms(1/60s),我们可以简单地调用taskDelay(2)以保证数据擦写完成,按理说taskDelay(1)就足以保证,为什么需要taskDelay(2)呢?

VxWorks Timer taskDelay

这也是taskDelay使用的一个缺陷,使用时需要注意。taskDelay(n)表示任务延时至第n个系统时钟到来的时刻,如图1所示:如果在A时刻调用taskDelay(1)仅仅延时5ms,而在B时刻taskDelay(1)就是刚好一个tick周期。可见要10ms的延时就必须taskDelay(2)才能保证。taskDelay有接近-1个tick的误差存在,taskDelay(n)实际上是延时(n-1)tick ~ n tick 之间的时间。延时精度为1/n,延时1s就是taskDelay(60)的误差极限为1.6%,而taskDelay(1)的误差极限将是100%。

使用taskDelay需注意的另外一点是:即使经过n个tick,调用延时的任务也不保证返回执行状态,可能有更高或相同优先级的任务占用了CPU。

watchDog

VxWorks提供了一个通用的看门狗定时器机制,利用提供的函数,任何任务都可以创建一个看门狗定时器,经过指定的延时后,实现在系统时钟ISR的上下文中运行指定的程序。需要注意的是,看门狗定时触发的程序是在中断级别上执行,而不是在任务的上下文。因此,看门狗定时挂接的程序编写有一定的限制,这个限制条件与中断服务程序的约束是一样的。比如,不能使用获取信号量的语句,不能使用像printf()这样的I/O 系统函数。

通过wdCreate( )可以创建一个看门狗定时器。调用wdStart()启动定时器,延时参数同taskDelay一样以tick为单位,同时还要指定定时完成后要调用的程序。如果应用程序同时需要多个看门狗函数,应使用wdCreate( )产生多个独立的看门狗ID。因为对于给定的看门狗ID,通过wdStart()只能关联一个看门狗函数。在指定的tick计数到达之前,要取消一个看门狗计时器,可以调用wdCancel()实现。每调用一次wdStart(),看门狗定时器只执行一次。对于一些要求周期性执行的应用程序。要获得该效果,定时器函数本身必须通过递归调用wdStart()来重新启动定时器。

如果利用看门狗定时器实现延时,存在与taskDelay一样的精度上的缺陷,以tick为基准。并且看门狗关联的函数受的限制很大,也是使用不便的一个方面。不过启动看门狗的任务不会被阻塞,因为wdStart()调用立即返回并继续执行。

sleep/nanosleep

sleep() 和nanosleep() 是VxWorks提供的延时函数接口,sleep以秒为单位,nanosleep提供更精确的延时,传参是时钟的结构体,参数可以精确到ns,但实际上只能做到大于或等于这个时间。因为sleep或nanosleep函数延时的时间基准仍是tick,调用此函数的任务处于任务延时状态,这些点与taskDelay() 一致。不同的地方是,taskDelay() 是用于任务调度,taskDelay(0)有他自身的含义,sleep(0) 是没有意义的。前面提过,taskDelay(n) 延时时间(n-1) tick ~ n tick,而sleep/nanosleep保证实际延时时间大于或等于设定的时间参数。这一点我们可以通过编写一个测试程序试验证明。代码如下:


void testTimer(int sec, int nsec)       // 测试函数,输入参数为秒和纳秒
{
    struct timespec tm;         // 声明一个时钟结构体,包括秒和纳秒成员
    tm.tv_sec = sec;            // 根据传参赋值
    tm.tv_nsec = nsec;
    nanosleep(&tm, NULL);       // 执行延时程序
}

在tornado的windshell环境下执行该函数和输出结果如下:


-> sp testTimer,0,1000*1000*16; i
  NAME        ENTRY       TID    PRI   STATUS      PC       SP     ERRNO  DELAY
---------- ------------ -------- --- ---------- -------- -------- ------- -----
tExcTask   _excTask      2318d10   0 PEND         412cd8  2318c14       0     0
tLogTask   _logTask      23131e0   0 PEND         412cd8  23130e4       0     0
tWdbTask   _wdbTask      230e920   3 READY        412cd8  230e7c8       0     0
s2u24      _testTimer    23092b8 100 DELAY        412cd8  23091b4       0     1
value = 0 = 0x0
-> sp testTimer,0,1; i
  NAME        ENTRY       TID    PRI   STATUS      PC       SP     ERRNO  DELAY
---------- ------------ -------- --- ---------- -------- -------- ------- -----
tExcTask   _excTask      2318d10   0 PEND         412cd8  2318c14       0     0
tLogTask   _logTask      23131e0   0 PEND         412cd8  23130e4       0     0
tWdbTask   _wdbTask      230e920   3 READY        412cd8  230e7c8       0     0
s2u25      _testTimer    23092b8 100 DELAY        412cd8  23091b4       0     1
value = 0 = 0x0

可见延时1ns和16ms的结果都是一样的,都相当于taskDelay(1)。

高精度时钟sysTimeStamp

sysTimeStamp()也称作时间戳,是通过系统时钟实现的,刚开始也觉得费解,系统时钟的定时周期就是tick,怎么实现高精度时钟呢?通过走读BSP底层代码发现,sysTimeStamp其实是通过读取该定时器的当前计数值来获取高精度的定时。通过sysTimestampFreq)函数可以得到系统时间戳的频率,它往往反映的是CPU定时器的基准频率。当然如此高的分辨率只能是一个理想值,对于不同的系统不一定都能实现。毕竟该时间戳的实现方式就有一个致命的弱点:通过查询方式。系统时钟定时中断是以tick为单位的,进一步提高分辨率读取定时器计数值(CPU的一个特殊功能寄存器),只能是查询方式实现。如下代码示例。


void msDelay(int ms)
{

    int t, t1, t2;
    t1 = sysTimestamp();        // 记录上一轮时间戳
    do {
        t = 0;                  //计数清零
        while (t < (sysTimestampFreq() / 1000)) // 时间戳小于1ms
        {
            t2 = sysTimestamp();        // 读取当前时间戳
            if (t2 > t1)
                t += (t2 - t1); // 根据前后时间戳比较算出经历时间
            else
                t += t2;        // 发生定时中断清零,应该为t+=(t2+计数最大值-t1),这里简化计算
            t1 = t2;            // 当前时间戳保存到下一轮计算
        }
    } while (ms--);             // 循环ms次
}

这种方式定时比较占用系统资源,但是能方便实现并只适用于短时间的定时。为确保定时准确,应该在锁定中断情况下调用sysTimestamp,否则应该考虑使用sysTimestampLock函数。

辅助时钟

辅助时钟是利用目标板上CPU的另一个定时器(除了系统时钟之外)中断实现的,它可以灵活配置实现高分辨率的定时,而且容易实现ms级甚至us级定时。VxWorks也提供一系列与系统时钟相同的操作接口,用户可以方便挂接自己的中断处理函数,时钟分辨率的高低取决于硬件定时器的精度和用户中断函数的长短。要将辅助时钟作为精确的延时机制(比如毫秒级延时),我们可以通过这种方式实现。初始化程序先调用SysAuxClkRateSet()函数设置辅助时钟中断周期为1ms(一般在config.h文件中对中断频率作了限定,在AUX_CLK_RATE_MIN和AUX_CLK_RATE_MAX之间,如果需要可以对此宏定义修改),通过SysAuxClkConnect() 将用户处理函数连接到辅助时钟中断上,用户处理函数可以为SemGive(semTimer)释放一个同步信号量。编写一个msDelay(int ms)作为其他任务调用接口,函数代码如下:


void msDelay(int ms)
{
    int i;
    sysAuxClkEnable();          //启动辅助定时器
    for (i = 0; i < ms; i++)    //循环ms次
        semTake(semTimer);      //等待定时中断释放信号量
    sysAuxClkDisable();         //关闭辅助定时器
}

这种方式能实现十分精确的定时,调用延时的任务处于任务阻塞状态。但是使用上任存在缺陷,不能实现多个任务同时调用,并且需要CPU的一个时钟资源,如果没有多余的时钟,这一方法就不能实现。

VxWorks Timer Spy

另外需要注意一点:Tornado的调试工具Browser ->Spy Chart的实现原理是利用辅助定时器产生中断,并记录当前被中断的任务,抽样数据反映各任务CPU占用率的情况。因此如果调试程序中使用了辅助定时器,使用Spy Chart时,定时处理函数会被重新挂接,原有定时挂接的程序将得不到进行。反过来,如果在Spy Chart运行之后挂接辅助定时处理函数,Spy Chart的运行将出现问题。试验发现,运行Spy Chart后重现挂接辅助定时处理函数,Spy Chart即使选中自动刷新,各任务状态也不会更新,如图2所示。

VxWorks提供的定时接口(不一定专门用于定时,可以间接实现)远不只这些,具体使用哪种方式,应根据其精度要求,资源状态和优先级要求而定。