嵌入式RTOS学习篇:01 从单片机到嵌入式实时操作系统

引言:目前嵌入式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

猜你喜欢

转载自blog.csdn.net/a574780196/article/details/80484832