1. 引言
目前,嵌入式系统软件有VxWork、Linux、μC/OS-II等 [1],其本身所具有大量的代码,占用单片机大量的存储空间,不适用于8位的AVR微处理器。出于成本和技术上的考虑,AVR微处理器的软件开发还是基于处理器直接编写,没有配备多任务操作系统作为开发平台,也不需要将系统软件和应用软件完全分开处理。而在实际应用中,AVR微处理器通常会面临同时应付多任务、多外设的情况,则对它们的相互调度必不可少。
因此有必要针对8位微处理器设计一种简单的操作系统,在实际应用中生成少量代码,以节省单片机宝贵的存储空间,进而达到工程应用的目的 [2]。时间触发嵌入式系统就是这样简单的操作系统,它可以看作是一种任务调度器 [3]。设计人员能够通过仔细安排事件的可控顺序,保证系统一次只处理一个事件,有助于减少CPU的负荷,减少单片机存储器的占用量,提高程序流程的可预测性的同时,也简化程序结构。本文设计了基于AVR微控制器的时间触发多任务调度器,使用传递消息(message)的方式使得AVR微控制器在多个任务及外设间切换,并应用于实际。
2. 传统编程方式的局限性
嵌入式系统中,通常采用两种不同的调度方式:事件触发和时间触发 [4]。传统的事件触发方式,即目前工程中常用的超级循环结构,通常采用多级中断的方式实现,其发生时间具有随机性;而时间触发方式则不同,它是通过一个全局时钟进行程序驱动的,系统的行为不仅在功能上确定,而且在时间上也是确定的 [5]。
在工程实际应用中,当系统略微复杂时,采用传统的“超级循环”编程结构,也就是事件触发的方式,系统的中断数量较多,且功能稍微复杂时,就会使程序编写变得很复杂,并程序运行的可预测性迅速下降,甚至导致中断触发丢失,给产品开发带来灾难性的后果。下面以一个产品研发过程中面临的问题为例来进行说明。在设计一个用于采集某武器水下航行数据的测试装置时,其系统模块框图如图1所示,CPU的程序设计需完成的任务如下所示:
① 每10 ms通过A/D采集芯片进行一次深度数据采集,执行时间 < 2 ms;
② 每10 ms对I/O口线的电平进行一次记录,执行时间 < 1 ms;
③ 每10 ms通过串口接收外设发送的一帧报文,速率为115,200 bps,执行时间 < 2 ms;
④ 每100 ms CPU通过SPI总线与片外Flash通信,写入缓存的采集数据,执行时间8 ms;
⑤ 依据工作时序,随机接收485串口的报文并返回相应指令,速率为38,400 bps,执行时间 < 2 ms;
⑥ 依据工作时序,通过232串口向上位机发送测试装置工作状态信息,执行时间 < 2 ms;
⑦ CPU通过I2 C总线与时钟芯片通信,执行时间 < 0.1 ms;
⑧ 依据工作时序,输出100 ms点火脉宽信号,执行时间 < 0.1 ms;
⑨ 每100 ms看门狗喂狗任务,执行时间 < 0.1 ms。
以上任务中,任务③、⑤和⑥属于强实时性的,系统若不及时响应串口的收发任务,接收数据时会引起字节丢失,发送数据时会导致数据字节之间的时间间隔变大,导致接收方的数据帧定界错误。而其他任务在设定的周期内能得到执行就可以。以下介绍采用“超级循环”结构实现上述程序设计时遇到的问题。
为确保任务③、⑤、⑥响应的及时性,采用了UART中断触发方式,当UART完成一个字节的收或发后触发中断,在中断响应函数中将收到的数据保存在接收缓冲区,或者从串口发送缓冲区读取下一个待发送的字节放入UART发送数据。为确保任务④响应的可靠性,采用双缓存交替写入Flash的方式。因为当系统掉电时,系统只有不到10 ms的过渡时间,系统如果不能在这个时间内完成相关的操作,将会丢失测试数据,导致测试的无效。
若采用事件触发的结构进行编程,代码结构如下:
While(1)
{
任务①;
任务②;
……
任务⑨;
}
由于任务④的时间最长,在某些情况下,一个循环中会有①②③④⑤⑥⑦⑧⑨的任务,则整个循环的运行时间就会长于10 ms,这导致各项任务响应时间的要求无法满足,甚至会导致程序中某些任务事件的丢失,比如A/D采样任务,I/O口线电平记录任务。
3. 采用时间触发的模式编程
时间触发方式通过一个全局时钟进行驱动,系统的行为不仅在功能上确定,而且在时间上也是确定的,其编程的关键是设计一个基于时间触发方式的合作式任务调度器,在系统程序中尽量少用事件触发方式(减少系统中断的使用),使用任务调度器对各项任务的调度执行 [6]。时间触发编程模式的典型程序流程结构如图2所示。
初始化函数的主要用途是读取内存参数,并设置系统初始的软硬件参数;建立各任务参数函数则根据系统中的各项事件任务需求,建立并分配任务参数,例如用来产生驱动调度器的定时“时标”。任务刷新函数是调度器的中断服务程序,它由定时器的溢出激活。当任务刷新函数确定某个任务需要执行时,将这个任务的运行标识符置位,然后该任务将由任务调度函数执行。任务调度函数与任务刷新函数配合激活需要运行的任务。

Figure 2. Program structure diagram of time-triggered embedded system
图2. 时间触发嵌入式系统程序结构图
3.1. 消息队列
消息队列是调度器的核心,它是用户自定义的数据类型,包括了每个任务所需要的信息 [7]。尽量将其存储在DATA区,以供快速存取。消息队列的结构如下。
typedef data struct
{void(code*pTask)(void);//指向任务的指针
uint16 Delay; //延时间隔
uint16 Period; //连续运行的间隔
char Runme;//任务运行标志
}s_task;
s_task SCH_task_array[SCH_MAX_TASKS];
3.2. 调度器初始化函数
该函数执行各种重要操作,如准备调度器队列,但它最主要的用途是设置定时器,用来产生驱动调度器的时标。本文所选用AVR系列的ATmega1280微控制器具有六个定时器(两个8位,四个16位),任一个都能用来驱动调度器,权衡考虑选用定时器1。
void SCH_Init_T1(void)
{逐个删除各个任务;停止定时器1;设置定时器1;使能定时器1方式;启动定时器1;
}
在设置定时器1时需要对调度器任务刷新的时间间隔进行设置,即定时器1的定时间隔。根据经验,一个较为可取的时间间隔是略大于一次典型的任务执行所需要的时间,使大多数进程在一个时间片内完成。经反复尝试,时间间隔选择在1~5 ms之间执行效率比较高,这样既可满足响应速度的要求又能把任务执行的时间降到最小,在本文中选择2 ms。
3.3. 调度器任务添加函数
该函数用来将任务添加到消息队列,以保证条件满足时被调用,函数如下所示:
uint8SCH_Add_Task(void(code*pFunction)(),const uint16 delay,const uint16period)
{定义静态变量i;循环判断任务队列是否有空间;若无,报错返回;否则,添加任务;}
3.4. 调度器任务刷新函数
调度器任务刷新函数即为定时器1触发的中断服务程序,通常采用CTC方式触发。在该函数中,当某个任务需要执行时,其运行标志Runme置1,然后该任务被调度程序执行,初始化函数中定时器的设置决定了它的调用频率。该函数结构如下。
#pragma vector = TIMER1_CAPT_vect//AVR中断服务程序
{定义变量;检测是否有任务;任务需要运行,Runme+1;
任务还没准备好运行,Delay-1;
}
3.5. 任务类型的划分
对章节1里的任务事件进行分析,根据特点可以将这些嵌入式系统的任务事件划分为以下3类。
1) 及时型任务。该类任务属于事件触发型的,此类事件一旦发生,在规定的时间内系统必须对其响应,针对该种任务,通常采用中断方式来完成。③、⑤、⑥属于及时型的任务。
2) 周期型任务。该类任务是周期型的时间触发式的,在规定的周期内系统必须确保执行此类任务,通过时间触发编程模式可以较好地实现这类任务的需求。①、②、⑨属于周期型的任务。
3) 背景型任务。这类任务不是实时型的,对实时性要求并不是很高,系统在程序执行过程中可随时中断该类任务进而执行前两类任务。这类任务需要系统在充分利用系统资源的基础上尽最大速度完成即可。④、⑦、⑧属于背景型的任务。
根据上述任务分类可知,及时型任务优先级是最高的,周期型任务优先级次之,背景型任务优先级是最低的,优先级高的任务可随时中断优先级低的任务的执行,同等级别的任务之间不可以相互中断执行。
3.6. 程序结构
为确保优先级高的任务退出后,优先级低的任务执行环境的复原,可以参考中断的执行机制用如下方法进行设计:
在系统程序中设计定时中断函数I,对周期性任务进行调度,在系统全部中断中这个定时中断的优先级是最低的。
在系统程序中再设计另一个定时中断函数II,对周期型任务的任务管理队列进行刷新,支持任务调度,在系统中全部中断中这个定时中断函数的优先级次低。
周期型任务通常是一个函数,在这个函数入口的首要操作是打开中断,允许该类任务运行期间被中断打断,响应及时型任务。
背景型任务指的是在系统主函数的超级循环流程中执行的代码,该类程序可随时被及时型与周期型任务中断,当系统程序中不需要执行及时型任务与周期型任务时,才允许循环执行背景型任务的程序。
通过上述分析,采用“时间触发编程模式”的系统程序结构如下所示:
//主函数
void main(void){
SCH_Init();//设置系统调度器
SCH_Add_Task(任务函数名称,任务调度延迟设计,任务调度周期等);//将系统任务加入调度器的任务队列中
SCH_Start();//刷新系统任务队列
while(1) {
背景型任务1;
……
背景型任务n;
}
}
//优先级次低的定时中断函数
void SCH_Update(void)interrupt{
//刷新系统任务队列
}
//优先级最低的定时中断函数
void SCH_Dispatch_Tasks(void)interrupt{
//调度系统周期型任务
}
//周期型任务的典型结构
void SCH_Cycle_Task1(void){
打开中断;
执行任务;
return;//任务返回
}
4. 应用分析
如上文所述,某型武器水下航行数据测试装置是一个复杂的嵌入式系统,其微控制器需要处理大量的外围设备,如图1所示。为了便于开发,将系统程序按照硬件的功能分别划分模块,各个软件模块之间通过传递消息的方式来完成多任务的处理。使用上述介绍的调度器既方便了程序的设计和维护,又解决了多个任务之间的调度问题。针对该型测试装置的应用,模块入口函数数组TskTbl[]如表1所列,使用函数数组的方式可以增强程序的扩展能力。如果增加新的外设,只需在这里添加相应的模块入口,并完成对应的模块代码就可以增加系统的功能。

Table 1. System function module division
表1. 系统功能模块划分
该系统在Atmel公司AVR系列单片机的开发平台上用C语言实现,调度器在某型测试装置系统中很好地发挥了作用,经应用证明,系统的测试程序流畅,保存的数据完好。
5. 结束语
通过上述分析可知,在内存资源较少的单片机小型嵌入式系统采用“时间触发编程模式”进行编程,与采用RTOS进行编程相似,设计人员设计好系统中的任务后,就可以专心设计每个任务,每个任务占用处理器的时间间隔可以由系统程序进行管理,减少各个任务相互之间的耦合,使得产品的程序设计、改动与优化变得简洁明晰。采用“时间触发编程模式”较好地解决了某测试装置产品设计过程中面临的复杂设计困扰,实际应用证明,这种方法简洁有效,稳定可靠,适用于对成本和稳定性均有要求的小型嵌入式系统。