引言:目前嵌入式RTOS的种类繁多,但在具体学习某种RTOS之前,有些概念却是通用的,抓住这个要点便开了个好头!
文章向导
* 单片机与前后台系统
* 嵌入式实时操作系统是什么?
* 不可不知的细小知识点
* 基于时间触发模式的编程思想
一、单片机与前后台系统
单片机程序通常由一个while循环和中断机制组成,while循环是后台,而中断则为前台。具体执行流程如下:
void main()
{
Peri_Init(); //外设初始化
while(1)
{
Task(); //程序主要部分
}
}
void Interrupt_Handler()
{
....//中断服务程序
}
在不考虑实时性的要求时,这种方式是简单且足够的,但当CPU处理的事情越来越多且越发复杂时,使用前后台系统方式显然没法保证实时性。为了更好地理解这里谈及的“实时性”,笔者将以一个实际产品中的设计问题为例来进行说明。
假设CPU需完成下述任务:
1) 每500ms刷新一次显示屏的数据;
2) 异步串行口与上位机进行Modbus通信,速率最高达19200bps;
3) CPU要对采集的6路信号进行FFT运算;
4) 当系统掉电时,CPU要及时将当前数据写入EEprom中。
上述任务中,2)和4)都属于实现性要求极高的任务,因为如果串口收发事件得不到及时响应,势必会导致收发字节丢失或Modbus帧定界错误,而对系统掉电事件若不能及时响应则会造成EEprom的写入失败。另外,任务3)也比较特殊,这是因为做FFT运算会比较耗时,若使用通常的8位CPU进行6路信号的FFT运算,哪怕对每路信号仅做128点的FFT,运算一次也需要好几秒。那么,若依然使用前后台系统,则会碰见如下的困境(下面以“时间片轮询法”来书写该程序):
void main()
{
Peri_Init(); //外设初始化
Interrupt_Config(); //中断频率:1000HZ
while(1)
{
if(Task_Delay[1] == 0)
{
Task(1);
Task_Delay[1] = 500;
}
if(Task_Delay[2] == 0)
{
Task(2);
Task_Delay[2] = 20;
}
if(Task_Delay[3] == 0)
{
Task(3); //6路信号的FFT运算
Task_Delay[3] = 1000;
}
if(Task_Delay[4] == 0)
{
Task(4);
Task_Delay[4] = 10;
}
}
}
/*时间片轮询法的中断服务函数, 1ms*/
void Interrupt_Handler()
{
unsigned char i;
for(i=1; i<=NumOfTask; i++)
{
if(Task_Delay[i])
{
Task_Delay[i]--;
}
}
}
程序的框架很容易理解,采用时间片轮询法后多个任务就以一定的频率执行,看起来就像多个任务一起无干扰地执行一样。但上述程序片段明显会出现干扰,因为任务3)执行一次需要几秒的时间,而整个while循环执行一次则至少大于任务3)所需要的时间,所以满足不了各个任务响应的实时性。
为了解决这个问题,可将任务3)拆分为多个子任务,每个子任务耗时10ms左右,并定义好各个子任务完成后的状态,while循环中每次根据状态只执行一个子任务。具体做法如下:
if(Task_Delay[3] == 0)
{
switch(subtask) //子任务状态
{
case status_1:
task(1);
break;
case status_2:
task(2);
break;
...
case status_n:
task(n);
break;
}
Task_Delay[3] == 10;
}
这样看来问题貌似已经得到解决,但实际上依然隐含有许多问题。首先,拆分得到的子任务或许有数十甚至上百个,这显然是不可接受的。除此之外,while循环还有个特点就是,随着任务的增加,循环体的执行时间也是线性增加的,因此也很难保证系统响应其他任务的实时性。
二、嵌入式实时操作系统是什么?
上面所介绍的前后台系统就好比一个人从头到尾的去做一件事,偶尔还会处理一些突发事件(中断服务函数),但当事情变多变复杂的时候可能就没法把所有事情都做好。
引入实时操作系统后,就好比有一管理团队在协调处理这些事情,也即是提高了CPU处理复杂事件的能力。不过,请一个管理团队会占用公司的部分资源,这在操作系统中则对应着CPU的负担增加。实时操作系统可通过一系列软件管理让一个CPU拥有多个线程,就好像有多个CPU同时执行一般。
三、不可不知的细小知识点
上图是实时操作系统的两种分类,当然还有其他的分类角度,读者可自行查阅。
1. 硬实时操作系统:要求系统在规定时间内必须完成任务。
2. 软实时操作系统:要求系统越快完成越好。
3. 可剥夺型操作系统:只要存在有更高优先级的线程就绪,低优先级的线程就会被打断,高优先级线程就占有CPU。
4. 不可剥夺型操作系统:只要当前线程放弃使用CPU之后,其他线程才能占用CPU。
读完上面这段描述,或许你会注意到可剥夺型操作系统的独特之处。在可剥夺型操作系统中,当前任务随时都可能被打断去执行另外的任务,这时任务可能正在执行往一个变量中写入数值,而这个过程是需要多个指令的。如果写入过程被中途打断,而且这个变量有可能在中断或其他任务中被读取,那么将读取到一个错误的值。所以写入的这个过程就不能被中断或者其他任务抢占,这种类似主要的程序段被称之为临界段,即不能被中断或者其他优先级的任务打断的程序段。
一般来说,有两种方式可以保护临界段:一种是关中断,一种是锁调度器,但后者仅能防止其他任务访问临界段代码,而不能防止中断访问。
四、基于时间触发模式的编程思想
前面部分提到的前后台系统本质上是事件触发式的编程方式(中断的使用),下面将论述一种基于时间触发式的编程思想,它也是RTOS编程的雏形。该模式的核心是建立一个基于时间触发的合作式的任务调度器,系统通过任务调度器完成各任务的调度执行,下面是典型的程序结构:
void main()
{
SCH_Init(); //设置调度器
/*任务名称,任务调度延迟,任务调度周期*/
SCH_Add_Task(taskname, delay, period);//将任务加入调度器的任务队列
SCH_Start();//刷新任务队列
while(1)
{
SCH_Dispatch_Task(); //执行任务调度器
}
void interrupt_Handler()
{
//刷新任务队列
}
}
上述程序片段中,系统为每个任务都设定了优先级、任务循环周期和任务延迟时间。中断服务函数则按照设定的节拍对任务队列进行刷新,在while循环中只执行任务调度器,根据任务队列的状态安排任务的执行。不过这种方式也仅仅是解决了部分问题,还远算不上真正的嵌入式RTOS,但本文仅意在完成一场过渡,具体更为深层次的内容读者感兴趣的话可自行挖掘。
参阅资料
UCOS III 源码分析笔记
安福莱FreeRTOS文档
https://wenku.baidu.com/view/aaa246de7f1922791688e8c4.html