孙彦森,刘金栋,王喜龙,刘雨霞,张 静
(潍柴动力股份有限公司,山东潍坊 261061)
嵌入式软件开发过程中会用到大量的定时操作,往往通过操作系统提供的原生定时服务进行。例如,Linux操作系统提供了alarm、settimer 等系统调用用作定时处理[1-2],这些定时器都建立在信号触发和处理的基础上,有定时器个数和信号的限制,同时多个操作系统间的代码移植性较差,在实际应用中存在一定的局限性。因此,需要一种可以摆脱信号处理的束缚,且能够在多个操作系统间进行代码移植的定时器。
近年来,许多国内外机构和学者对定时器进行了相关研究。在不同的应用领域,定时器有着不同的应用场景,发挥着不同的作用。在电信应用领域,电子科技大学的周鹏[3]、李群[4]针对电信级Linux 强实时性的要求,设计并实现了新型高精度定时器,从Linux 内核的角度提高了定时精度;中国科学技术大学喻诗祥[5]设计了一种基于多核平台的用户态定时器,提出了一种共享内存策略,在保证定时器系统定时精度及误差的情况下,可减少系统性能损耗。在核聚变研究领域,东华大学的代路伟[6]结合EAST托卡马克中央定时系统的时间参数等因素,在分析积分器设备所需的功能基础上,搭建了基于ARM处理器的硬件平台,并实现了长时间高精度定时器。在雷达研究领域,南京电子技术研究所的柯小路等[7]以现场可编程门阵列为平台,提出一种通用软件化定时设计方法,可提升设计和调试效率;针对雷达定时控制,也有众多科研人员进行了相关定时研究并取得了一定成果[8-11]。在汽车电子领域,结合车载操作系统,定时器主要应用于车载智能终端开发[12-14]、车载信息娱乐系统开发[15]、车载网络实时管理等方面[16-18],具有广泛的应用。
针对汽车电子领域的车载智能终端,利用软件模块化设计思想,本文设计了一种通用定时器,可以作为嵌入式软件应用设计中一个模块,为系统中其他软件应用模块提供定时功能。本方案为一种相对定时器方案,其优点是可动态设定定时器数量和精度、支持在多个操作系统间进行移植、支持同时给其他多个模块提供定时功能。
定时器模块提供一个相对定时器,当其他模块需要使用定时器时,会调用设置接口设置定时器。定时器扫描模块会先查询是否有可用的定时器,如果有,则为该模块分配一个定时器。当定时器到时时,定时器扫描模块会以消息的形式发送给使用该定时器的模块,通知定时器已经到时。定时器到时后,该定时器就会被删除,及时释放所占用的定时器资源,进而分配给其他模块调用。软件定时器服务方式如图1 所示。
图1 软件定时器服务方式
定时器模块通过维护一个时钟滴答数组实现定时。
1.2.1 时钟滴答数组
定时器模块维护一个时钟滴答数组,设置数组大小为TIMER_MAX_TMCB_NUM,可灵活配置,本文取值30 000;数组中每一个组员代表一个时间段TIMER_SLEEP,可灵活配置,本文取值10 ms。数组中各个成员的位置按序为0、1、2、…、TIMER_MAX_TMCB_NUM-1,本文对应0~29 999。设置一个游标wScanPos 指向数组中的某个组员,代表当前的时间,每走过10 ms,游标会向前移动一个数组成员。例如,要设定一个30 ms 定时,游标需要前移3 个组员时,定时器才会超时,此时定时器模块会向需要定时的模块发送消息,告知已经到时。定时器就是挂载到该时刻的数组成员下,当游标移动到该数组成员时,就代表了该定时器到时。
如图2 所示,设定一个30 ms 的定时器。黄色代表当前游标的位置7,蓝色代表30 ms到时时的游标的位置9,即定时器需挂载到位置9 处。注意,当前游标所在的位置7 也需包含在内。这是因为游标开始时指向位置7,位置7 所代表的10 ms时间段并没有走过。
定时器模块每隔10 ms 移动一下游标,然后判断该游标下是否有定时器;如果有定时器,判断定时器是否已经到时;如果到时,就向使用该定时器的模块发送消息,通知该定时器已经到时。因为该游标所指的位置处可能不只一个定时器,定时器模块会将该游标下的到时的定时器通知使用这些定时器的模块。如图2 所示,位置9 处同时挂载了定时器1 和定时器2 两个定时器。
图2 时钟滴答数组
由于定时器要挂载在时钟滴答数组成员下,有可能某个数组成员下有多个定时器。为解决这个问题,采用双向链表的方式将定时器串联起来,每次申请定时器时,都会将这个新定时器放到链表头,然后重新组合链表。每个定时器都有两个变量wPreNode 和wNextNode,分别表示在双向链表中前一个定时器和后一个定时器。时钟滴答数组成员就指向这个双向链表的表头。
对于大于10 ms×30 000 的定时时间,数组会回到开头循环使用。在设置定时器时,需预先确定定时器在10 ms时钟滴答数组中到时的位置,定时时间大于10 ms×30 000 的,需要计算该定时器在10 ms数组上经过多少次循环才会到时。例如,设置一个定时器a,定时时间为10 ×60 000 ms,当前游标的位置在10 ms 数组的200位置,那么计算定时器a的过程如下:
dwDly =10 ×60 000/10
dwTime =200 +dwDly -1
dwTimerCounter =(dwDly-1)/30 000
wPos =dwTime%30 000
式中:dwDly为游标移动次数,计算值为60 000;dwTime为定时器在滴答数组中对应的位置,计算值为60 199;dwTimerCounter为游标循环次数,计算值为1;wPos为定时器到时对应的游标位置,计算值为199。
由此可知,定时器a 在10 ms 数组中对应挂载位置为199,游标需要第二次移动到位置199,定时器才到时。
1.2.2 定时器节点存储区
定时器节点存储区里面存储着定时器,包括空闲的定时器和正在使用的定时器,该存储区就是一个定时器的数组。定时器节点存储区示意图如图3 所示,这些定时器的编号按序为0、1、2、…、(TIMER_MAX_ TIMERS-1)。其中,TIMER_ MAX_ TIMERS 代表定时器的最大数量。
图3 定时器节点存储区
1.2.3 定时器空闲节点表
定时器空闲节点表用来记录空闲的定时器在定时器共享内存中的位置,它是一个数组,大小为(TIMER_MAX_ TIMERS +1)。空闲节点表有个两个游标wFreeHead、wFreeTail,分别指示空闲节点表中的开始位置和空闲节点表结束位置加1。
定时器模块刚启动时,没有定时器被申请,空闲节点表中记录的空闲定时器就是定时器共享内存的中的所有定时器。空闲节点表数组中按序记录着空闲定时器在定时器共享内存中的位置0、1、2、…、(TIMER_MAX_TIMERS-1)。wFreeHead指向空闲节点表的数组成员0,wFreeTail指向空闲节点表的数组成员TIMER_ MAX_TIMERS。当其他模块申请设置定时器时,定时器模块将空闲节点表中第wFreeHead 个数组成员所指向的定时器分配给该模块,然后wFreeHead加1;当其他模块释放定时器时,空闲节点的第wFreeTail个成员记录该释放的定时器的序号(即该定时器在定时器共享内存中的位置),然后wFreeTail加1。
如图4 所示,假设定时器0、1 被申请占用,wFreeHead将移到位置3,即虚线处。当wFreeHead 或wFreeTail大于TIMER_ MAX_ TIMERS 时,会将其置0,重新回到起始位置,即这两个游标在空闲节点表上是循环移动的。
图4 定时器空闲节点表
定时器软件整体上就是一个扫描时钟滴答数组的过程。该时钟滴答数组各个成员下挂载着定时器,游标在时钟滴答数组中移动,当游标移动到这个数组的某个成员时,查询该成员下是否有定时器、定时器是否到时,如果到时,就发送消息给使用该定时器的模块,告知它使用的定时器已经到时,同时释放该定时器,修改空闲节点表;否则,游标移动到下一个数组成员。当某个时钟滴答数组成员下有多个定时器时,会逐一判断这些定时器是否到时。
游标每隔一段时间TIMER_SLEEP 向前移动一次,TIMER_SLEEP时间大小可灵活设置的,通过延时函数来实现。例如,在Linux系统中可以通过usleep()函数实现。假设游标每走一步的时间为10 ms,则TIMER_SLEEP为10,函数设置为usleep(10 ×1 000)。如果设置一个50 ms的定时器,则需要移动5 次定时器才会到时。定时器扫描处理流程如图5 所示。
图5 定时器扫描处理流程
在定时器模块中申请一个定时器,若有可分配的定时器模块,则根据定时器定时的时间,确定定时器在时钟滴答数组中的挂载位置。若无,则告知无法设置定时器。
申请一个定时器时,首先需查询定时器空闲节点表,是否有空闲的定时器。若游标wFreeHead 等于游标wFreeTail,则表示没有空闲的定时器,无法申请一个定时器;否则,将空闲节点表成员wFreeHead 所指的定时器分配给该模块,并在该定时器上标记已被使用,同时游标wFreeHead 加1。然后,根据定时器的定时时间dwTime和当前时钟滴答数组的游标wScanPos,计算定时器到时时游标所在的位置,并将该定时器挂载在那个时钟数组成员下,重新组合该时钟滴答数组成员下的定时器链表。
计算定时器挂载位置的计算公式如下:
式中:dwTime 为要设定的定时时长;TIMER_SLEEP 为延时时长(休眠间隔);dwDly为游标移动次数。
式中:wScanPos 为定时器在滴答数组中当前位置;dwTime为定时器到时时在滴答数组中对应的计算位置。
式中:TIMER_MAX为时钟滴答数组的大小;dwTimerCounter为游标循环次数。
式中:wPos为定时器挂载在时钟滴答数组的实际位置。
由此可知,游标第(dwTimerCounter +1)次经过wPos位置时,该定时器到时。设置定时器的流程如图6所示。
图6 定时器设置处理流程
删除正在使用的定时器,并释放定时器资源,该定时器资源就可以分配给其他模块使用。
当删除一个定时器时,在该定时器上标记为已空闲,同时将空闲节点表成员wFreeTail 指向该定时器,并将游标wFreeTail加1,然后在该成员下的定时器链表中删除该定时器,重新组合链表。删除定时器的流程如图7所示。
图7 定时器删除处理流程
在Linux操作系统无其他任务运行的情况下,设置不同的定时时长和延时时长(休眠间隔),并对比分析定时误差,发现误差均在50 ms 以内,满足一般要求下的定时误差要求。需要说明的是,在不同的操作系统、不同的负载运行情况下,测试结果可能不同。定时器精度测试结果如表1 所示。在定时时长为1 000 ms,延时时长分别为10、20、30 ms时,定时误差均在40 ms 以内;在定时时长为2 000 ms,延时时长分别为20、30、50 ms 时,定时误差均在50 ms以内;在定时时长为3 000 ms,延时时长分别为20、30、50 ms时,定时误差均在40 ms以内。
可根据具体应用场景对定时时长的要求,结合系统处理能力以及负载情况,通过提前测试确认合适的延时时长,使得定时误差最小,确保定时器的性能达到最佳。
基于汽车电子领域的车载智能终端,针对一般秒级应用场景,应用软件模块化设计思想,本文提出了一种通用定时器方案,并进行了编码实现和精度测试。定时器模块作为嵌入式软件应用设计中的一个单独模块,可以为系统中其他软件应用模块提供定时功能。
本方案为一种相对定时器方案,可以动态设定定时器数量和精度,支持在多个操作系统间进行移植,支持同时给其他多个模块提供定时功能。经过测试和分析,所设计的定时器满足一般应用场景下的定时器误差要求。同时,可以根据系统处理能力以及负载情况,对定时精度进行优化和调整,使得定时器模块达到最佳性能。