VxWorks是WindRiver公司开发的高性能实时嵌入式操作系统内核。在应用软件开发过程中经常会用到定时器。VxWorks下要实现定时功能有2个途径:一,借助taskDelay函数实现;二,使用VxWorks提供的看门狗(watchdog)。使用taskDelay函数实现定时器的缺点在于它是基于任务的,任务优先级会导致定时不准。看门狗基于系统时钟中断,定时精度大大优于前者,但是对用户的回调函数有诸多限制(如不允许使用semTake、printf等需要等待获取某种资源的函数,否则会引起死机)。另外,看门狗只触发一次回调函数,如果用户需要周期定时器就需要重新启动看门狗。
本文设计了基于看门狗机制的异步通用定时器,并根据实际需要设计了周期性定时和一次性定时两种定时器。异步是指定时器运行于任务中,对用户没有任何限制。异步通用定时器提供类似于Windows下定时器的操作接口,简单、方便。
2 VxWorks下的看门狗
VxWorks提供看门狗机制,允许将希望若干时间延迟后执行的用户函数连接到看门狗,在定时时间到达后由看门狗自动执行。看门狗机制由操作系统维持在系统时钟中断,连接到看门狗的函数同样运行在系统时钟中断服务程序中。如果操作系统由于种种原因(如在系统时钟中断前的中断或者内核状态),将不能立即执行的函数存放在tExcTask任务的队列中,则队列中的函数将以tExc-Task任务的优先级运行(通常为0)。操作系统对中断服务程序的各种限制同样适用于连接到看门狗的用户函数,如不能使用printf、semTake等。
对看门狗的操作函数有4个:创建看门狗函数,WDOGID wdCreate(void);启动看门狗函数,STATUS wdStart(WDOG_ID wdId,int delay,FUNCPTR pRoutine,intparameter);删除看门狗函数,STATUS WdDelete(WDOG_ID wdld);取消看门狗计时函数,STATUS wdCancel(WDOG_ID wdld)。
看门狗的简单使用如下:
首先创建看门狗,然后在启动看门狗时连接用户函数并设置延迟时间。上面程序中的interval即为延迟时间,单位为系统时钟的tick数。缺省情况下,系统时钟每秒的tick数为60。当interval为1时,即延迟1/60 s后执行usrFunc。系统时钟的tick数可以通过sysClkRateSet函数设置。
3 异步通用定时器的设计
3.1 设计思想
虽然看门狗提供的定时机制相对简单易用,但还有许多局限性:①定时时间的单位为tick数,而不是通常使用的s或者ms。②用户函数运行在系统时钟中断服务程序中,而不是运行在任务的上下文中。这给用户函数带来许多限制(比如用户函数中不能使用内存分配、获取信号量、printf打印输出等),在这些限制下某些功能可能就无法实现。③看门狗的触发是一次性的,而通常需要周期性的定时器。④相对于Windows下的定时器接口,看门狗接口不够简洁明了。异步通用定时器的设计基于看门狗,并在此基础上做进一步的封装,提供类似于Windows的使用方式。系统时钟每秒的tick数可以通过sysClkRateSet函数设置,一般设置为1 000,即每个tick代表1 ms。这样就可以提供分辨率为ms级的定时器,对大多数应用而言可以满足使用要求。每个定时器对应一个看门狗,同时对应一个任务,使得用户函数运行在任务中,而不是在中断中,这样可以避免操作系统对中断处理函数的种种限制。具体的做法是:在生成定时器时,启动看门狗开始定时,同时创建一个任务等待一个计数信号量(该信号量初始为空,任务处于PEND状态);当定时时间到达时看门狗释放该信号,激活任务,在任务中调用用户函数。这样做的优点在于,提高了效率,减轻了负载,减少了中断中的运算(仅仅是释放信号量);尽管多创建了一个任务,但是在定时器没有触发时任务仍处于PEND状态,对资源占用很小。
3.2 接口设计
提供类似于Windows的接口函数,定时器的唯一索引是id号,操作定时器均通过id完成。分为2种类型定时器:周期性定时器和一次性定时器。周期性定时器可以周期性地触发。一次性定时器则只触发一次,类似于倒计时定时器,触发后看门狗自动删除,相应的任务自动退出。在用户对定时器模块进行初始化后,用户可以在程序的任何地方调用定时器提供的接口。
3.3 具体实现
3.3.1 对看门狗的封装
基于程序设计上的考虑,将定时器的管理控制和看门狗的具体操作分开,对看门狗进行封装,CClkGenerator类封装了看门狗的所有操作,包括看门狗的创建、删除、取消和启动,保存定时器id、类型、定时周期等。值得注意的是:看门狗的回调函数并不是用户的回调函数,而是看门狗管理控制中提供的统一回调函数,回调函数中的参数为定时器的索引号。封装代码如下:
从类定义可以看出,用户并不能直接使用CClkGen-erator。也就是说,该类对用户而言是不可见的,屏蔽了对看门狗的直接操作,只有定时器管理控制模块才可以对其进行操作。
3.3.2 定时器管理与控制
定时器管理与控制模块负责模块初始化、多个定时器相关参数的存储管理、定时器任务的安全退出,以及用户接口的实现。
定时器的主要数据结构:定时器控制结构和存储结构。
使用C++标准模板库中的map实现对定时器的存储。第1个参数为定时器的索引号,第2个参数为定时器控制结构。使用map可以方便地实现基于定时器索引号的存储管理和索引号的查找。使用map的定时器存储示意图如图1所示。
用户在调用SetTimer函数时,创建一个初始状态为空的计数信号量timerArrv,同时生成一个任务timerTask等待该信号量,此时任务状态为PEND;实例化一个CClk-Generator对象,创建看门狗启动定时器。当定时器超时时,释放timerArrv信号量,解除阻塞在timerArrv上的任务,回调用户函数完成一个完整的定时过程。定时器的典型运行过程如图2所示。
图2中最底下的虚线指向启动看门狗后的中断处理流程。中间部分表示定时器任务运行过程,可见用户回调函数是运行在任务空间中。“回调函数释放信号量”到定时器任务semTake”的虚线表示释放信号量使任务解锁。
4 定时器的应用
定时器管理控制模块是用户的唯一接口,使用Single-ton模式。只要调用CTimerCtrl::GetTimerCtrl()就可以完成对异步通用定时器的初始化,除对定时器进行相关操作之外,还包括通过sysClkRateSet函数设置系统时钟每秒的tick数为1000。下面的例子包含2个定时器:一个是1 sN期性定时器;另一个是周期为5 s的一次性定时器。
结 语
从应用实例中可以看出,异步通用定时器的使用方法和Windows下的定时器没有太大区别,接口简单清晰。异步通用定时器可以应用于定时精度为ms的绝大部分应用程序中,对于精度要求高于ms的定时使用硬件辅助时钟中断更为合适,但是要注意操作系统对中断处理函数的限制。