目前,嵌入式系统的硬件核心大致有两大类:一类是功能强大的嵌入式微处理器,使用这类产品的系统一般功能强大,多数使用嵌入式操作系统,往往与无线通信、互联网访问以及多媒体处理等复杂而强大的功能联系在一起;另一类是微控制器,它通常以某一种微控制器内核为核心,芯片内部集成ROM、RAM、定时器、串行口等各种必要功能和外设。出于成本和技术上的考虑,这类系统的软件开发还是基于处理器直接编写,没有配备多任务操作系统作为开发平台,也不需要将系统软件和应用软件完全分开处理。但在实际的应用中,很多时候也会面临同时应付多种外设、处理多个任务的要求,这就需要安排一个调度器来完成多任务的处理。
本文设计并实现了一种基于时间触发的多任务调度器。该调度器使用传递消息(message)的方式使得控制器在多个任务之间进行切换。因为消息和任务一一对应,一个消息触发一个任务,所以本文对两者不做详细区分。
1 嵌入式软件的两种触发方式
嵌入式系统中,通常采用两种本质上不同的调度方式:事件触发和时间触发。事件触发方式往往使用多级中断实现,其发生时间具有随机性;而时间触发方式则不同,它是通过一个全局时钟进行驱动的,系统的行为不仅在功能上确定,而且在时间上也是确定的。
1.1 事件触发方式存在的问题
如果多个中断源在随机的时间间隔内产生中断,则需要处理同时发生的多个事件。这样不但增加了系统复杂性,而且降低了对事件触发系统在所有情况下行为的预计能力。实际上,在同时有几个有效中断源的情况下,几乎不可能创建代码来正确处理所有可能的中断组合。中断事件不会丢失是存在于绝大多数嵌入式系统开发人员头脑中的一种错误观念,这往往给所开发的产品带来灾难性的后果。事件触发系统的开销是人们经常忽略的另一个问题。Alexander Metzner专门讨论了这种问题并得出结论:一个包含27个任务、采用RM调度算法的事件触发系统,CPU的实际利用率仅为18%。
1.2 时间触发方式的优点
Kopetz首先提出:使用基于时间触发的合作式调度器会使得系统有非常好的可预测性。因此,在某些与安全相关的应用系统中选用时间触发方式,设计人员能预先安排可控的顺序,保证一次只处理一个事件,提高系统的可靠性并减轻CPU的负荷。
2 时间触发调度器的设计
调度器的设计主要包括3个方面:消息队列、定时器和周期性任务调度。在调度器的实现中,将定时器的设置分离出来,并且定义不依赖于编译器的数据类型,通过修改这一部分可以轻松地将该调度器移植到多种硬件平台上使用。
2.1 消息队列的设计
图1中,消息队列MsgQue[]和定时队列TmrQue[]是调度器的核心数据结构。为了减少时钟中断中对它们的处理时间,还设置了2个队列——就绪索引队列RdIdx[]和定时索引队列TmrIdx[]。这4个队列都由静态数组实现。
消息队列存放应用程序发送的单次消息和延时处理的消息。消息的数据结构是:
定时队列TmrQue[]和定时索引队列TmrIdx[]一一对应。其中,定时队列中存放定时消息的延时时间;而相对应的TmrIdx[]项则指向定时消息在消息队列中的位置。
要发送消息时,使用函数vdStrtTmrTsk(INT16UTmValue,struct Msg*pOutMsg),将pOutMsg指向的消息结构放入队列MsgQue[]中。具体的做法是:从数组的第一项开始查找,找到空闲项放入新消息并将该项的状态设置成BUFF-USED;然后将此消息项对应的索引值放入RdIdx[]的第一个空闲项中等待调度。如果发送的是延时消息,则要使用vdStrtTmrTsk(INT16U TmValue,structMsg*pOutMsg)将延时时间放入TmrQue[]中,并使用对应的TmrIdx[]项指向对应的消息。
图1中MSG5对应的任务正在执行,MSG9是刚到期的定时消息,当前任务结束后就可以处理该消息。MSG7是未到期的定时消息,其他2个都是已就绪待处理的消息。
2.2 定时器的设计
调度器必须先设定一个默认的时间片,这并不是件简单的事。时间片过长会导致系统对交互行为的响应表现欠佳;时间片太短又会明显地增大调度器处理耗时,而留给任务运行的时间却很短。根据V850处理器在车载音响上的实际需要,选择4 ms作为时间片。
在V850处理器中使用TM0定时器来实现4 ms定时功能,可以计算出CR70的初值为156,程序实现如下:
在定时器的中断服务程序中,扫描定时队列TmrQue口。如果有延时到期的任务,则将其从定时队列中删除并放在就绪索引队列RdIdx[]中去。对定时器相关的操作涉及具体的平台,在不同平台上移植调度器时需要修改这一部分。
2.3 周期性任务的处理方法
对于该系统,周期长度必须是4 ms的整数倍。在每次时钟中断以后执行下面的函数,通过将要周期性执行的任务放入函数数组TskPatt[]()中就可以执行周期为8 ms、16 ms、32 ms、64 ms等周期性任务。
3 任务的调度
调度器的算法使用FCFS算法,就绪索引队列RdIdx[]按顺序存储要处理的消息的索引。这里对延时消息做特殊处理,如图1所示,消息MSG9的延时时间刚到,它的索引被插入到当前消息索引的后面(也就是位置RdIdx[1]),它就可以在下一次调度中得到处理。
任务调度由wucExecTsk(void)函数来完成。它取出MsgQue[RdIdx[0]]对应的消息,以该消息的目的模块ID为索引,使用存放各个模块人口函数的函数数组TskTb1[](),就可以将该消息分发到相应的处理模块。
因为该调度器是合作式的,所以每个任务处理函数都必须显示地调用退出任务的函数,否则该任务会永远的执行下去。因此,每个模块的人口函数都调用退出任务的API:
在vdExtTsk()中,将当前任务在消息数组MsgQue[]中对应的数据项置成BUFF_EMPTY。同时,将就绪索引队列里的数据都向前移动,覆盖当前消息的索引,原来的RdIdx[1]就变成当前任务的消息索引,参与下一轮调度。
4 应用实例
车载音响系统是一个复杂的嵌入式系统,它的微控制器要处理大量的外围设备,如图2所示。为了便于开发,将程序按照硬件的功能划分模块,各个模块之间通过传递消息的方式来完成多任务的处理。使用上面介绍的调度结构既方便了程序的设计和维护,又解决了多个任务之间的调度问题。
针对这个应用,模块入口函数数组TskTb1[]如表1所列,使用函数数组的方式可以增强程序的扩展能力。如果有新的外设,只需在这里添加对应的模块人口,并完成相应的模块就可以增加系统的功能。
系统的周期性任务如表2所列。系统中按键使用的是矩阵键盘,4 ms时间太短不足以检测出键值,这里是通过每次扫描一行的方式来实现的。
系统在NEC公司V850系列微控制器的开发平台上用C语言实现,调度器在车载音响系统中很好地发挥了作用,系统的交互行为良好,输入、输出都感觉不到延迟。该系统已经应用在某型号的汽车上。
结 语
在工程中采用事件触发模式很大程度上会增加系统的复杂性;而商业实时操作系统往往价格昂贵,并且需要很大的操作系统开销。本文设计并实现了基于时间触发的调度器,它通过传递消息的方式完成多任务的切换,可以满足实时、简单、可预测性等工程要求。这种设计还使得系统易于开发和维护,应用于车载音响系统中取得了很好的效果。