多年嵌入式编程工程师经验分享:换个角度来编程
1 传统编程结构的局限性
当不使用RTOS时,嵌入式软件通常采用两种传统的编程结构进行编程,一种叫“前后台结构”或者叫“超级循环结构”,本质上是事件触发的编程方式,另一种叫时间触发的编程方式,Michael J.Pont 的“基于时间触发的编程模式”即属于此。
(1)每半秒对前显示屏的显示数据进行一次刷行。
(2)每0.1秒对DI/DO进行一次刷新。
(3)每0.2秒对键盘进行一次扫描。
(4)每半秒对测量数据进行一次重新采集和计算。
(5)异步串行口与上位机使用Modbus通信,速率最高19200bps。
(6)CPU通过IIC总线与时钟芯片和EEprom通信。
(7)CPU通过SPI总线与LED数码管及采集芯片通信。
(8)CPU要对所采集的6路信号进行FFT变换。
(9)当系统掉电时,CPU要能快速响应以把当前的电度底数写入EEprom中。
使用“前后台方式”进行编程时,为保证任务(5)的及时性,使用了UART中断,当UART完成一个字节的收发后产生中断,在中断程序中将接收到的字符保存在接收缓冲区或从发送缓冲区取下一个待发字符装入UART进行发送,对Modbus协议的处理可以单独用一个任务在中断外处理,这保证了中断程序的简短。为保证任务(9)响应的及时性,也必须为它安排一个中断。因为当系统掉电时,系统只有不到10ms的过渡时间,系统如果不能在这个时间内完成相关的操作,系统电压将跌落至有效电压以下而丧失工作能力。
while(1)
{
任务(1);
任务(2);
………
任务(8);
}
由于任务(8)执行一次要几秒钟的时间,整个超级循环执行一次至少大于任务(8)需要的时间,也就是说这个超级循环循环一次要几秒钟时间,将满足不了各任务响应时间的要求。
while(1)
{
任务(1);
任务(2);
………
Switch (子任务状态)
{
case 子任务状态1:
子任务1;
break;
case 子任务状态2:
子任务2;
break;
…………
case 子任务状态n:
子任务n;
break;
}
}
这样,就需要把一个耗时几秒的FFT运算任务拆分成几百个耗时10ms左右的子任务,这显然是不可接受的。
“时间触发编程模式”的核心是建立一个基于时间触发的合作式的任务调度器,在系统中尽量减少事件触发(减少中断的使用),系统通过任务调度器完成各任务的调度执行,下面是“时间触发编程模式”的典型程序结构:
/*--------------------主函数-----------------------*/
Void main(void)
{
SCH_Init();//设置调度器
SCH_Add_Task(任务函数名,任务调度延迟,任务调度周期);//将任务加入调度器的任务队列
SCH_Start();//刷新任务队列
while(1)
{
SCH_Dispatch_Tasks(); //执行任务调度器
}
}
/*-------------------定时中断函数---------------------*/
Void SCH_Update(void) interrupt
{
//刷新任务队列
}
系统中每个任务都定义了优先级、任务循环周期和任务延迟时间,系统时器中断程序SCH_Update()按设定的节拍对任务队列进行刷新,在超级大循环中只执行任务调度器SCH_Dispatch_Tasks(),根据任务队列的状态安排任务的执行。这种编程结构避免了超级大循环结构循环时间随代码量的增加而线性增加的问题,但是,由于任务是不可剥夺的,一旦任务启动执行,任务调度器只有在当前任务完成后才有机会执行,这就要求每个任务占用CPU的时间不能太长,否则将影响整个系统的响应速度。所以,FFT运算在这种编程模式下还是必须进行有效的拆分,否则就必须提高CPU的档次或使用可剥夺型的抢先式RTOS,这势必造成系统成本的增加。那么有没有更好的解决办法呢?
根据多年嵌入式系统编程的经验,通常嵌入系统的任务可以划分成3种类型:
(1)及时型任务;
(2)周期型任务;
(3)背景型任务;
(1)任务分3类,1类任务优先级最高,3类任务优先级最低;
(2)高优先级的任务可中断低优先级任务的执行,同级的任务之间不可相互剥夺。
(3)实际设计中为提高系统的可预测性,应尽量减少1类任务的数量及1类任务的执行时间。
(4)为降低系统资源的占用,系统不给任务划分单独的堆栈空间。
(1)在系统中设计一个定时中断函数,该函数的功能就是执行周期性任务的调度,该定时中断在所有中断中优先级最低;
(2)在系统中设计另一个定时中断函数,该函数的功能是刷新周期型任务的任务管理队列,为任务调度提供支持,本定时中断函数的优先级在系统中次低;
(3)周期型任务就是一个函数,该函数入口的第一个操作是开中断(问:这个中断指的是触发及时性任务的中断,那么在周期性任务外,是开还是关?如果是开,),允许任务执行期间被中断以便响应及时型任务。
(4)背景型任务就是在主函数超级循环中执行的代码,该代码可随时被及时型和周期型任务中断,当系统没有及时型任务和周期型任务时才循环执行背景型任务的代码。
/*--------------------主函数-----------------------*/
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;//任务返回
}