打印
[经验分享]

深入理解裸机与RTOS开发模式

[复制链接]
304|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
八层楼|  楼主 | 2024-12-13 08:24 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
裸机开发模式
所谓裸机开发,指的就是没有操作系统,就是单片机开发。程序的运行,完全取决于代码的逻辑设计,硬件设备的固定设定。不需要操作系统的参与和调度。
这里将韦老师上课举得例子拿来进行分析
讲的是一位宝妈,需要一编进行喂孩子吃饭,一边需要回复同事的消息。



那么我们首先想到的方式就是进行轮询

轮询方式
void main{}{
while(1){
        eat();//喂孩子吃饭函数
        message();//回复同事消息
}
}


这是一个非常经典的单片机程序,是不是就是你的跑马灯程序。那么我们来分析这个程序:
在执行喂孩子这个函数的时候,回复同事消息这个函数是无法执行的,在执行回复同事消息这个函数的时候,程序是无法执行喂孩子这个函数,那么对于同事而言,宝妈总是在一段时间消失,无法回复消息。对于孩子而言,妈妈总是在一段时间无法来喂我吃饭。
双方(同事和孩子)似乎都没有得到满足。显然这个程序是不太好的,那么我们如何来进行优化呢?
相信已经有朋友类比到了我们最初的单片机实验,我们可以使用中断呀!没错中断就是下面我们的优化方式,也叫做事件驱动方式

事件驱动方式
事件是一个宽泛的概念,什么是事件?可以是:按下了按键、串口接收到了数据、模块产生了中断、某个全局变量被设置了。

什么叫事件驱动?当某个事件发生时,才调用对应函数,这就叫事件驱动。

我们将上面的例子进行改进:

当孩子哭的时候宝妈就给他喂饭
当同事发送了消息,电脑提示了才去回复同事
void crying_isr(){//检测孩子是否在哭的中断函数
        eating();//哭了就执行喂孩子吃饭的函数
}
void message_isr(){//检测同事是否发消息函数.
        message();//执行回消息函数
}
void main(){
        while(1){
        }
}


这种编程方式就使得这两个中断函数执行的都很快,不用像轮询一样再去等待上一个函数的执行完毕。

但是如果两个中断同时发生,就会相互影响:

两个中断,同一时间只能处理一个
如果当前中断处理时间比较长,就会影响到另一个中断的处理。
下面继续优化
改进的事件驱动方式
对于上面的程序,我们出现的问题是,当两个中断同时产生的时候,同一时间只能处理一个,如果一个中断处理时间比较长,就会影响另一个中断的处理。

下面我们针对这些问题来进行改进我们的程序。

对于中断的处理,原则上是“尽快”。否则就会影响其他中断,导致其他中断的处理延迟,甚至丢失。

下面我们通过设置标志位来改进程序。

void crying_isr(){//检测孩子是否哭了
is_crying=1;//如果哭了就将标志位置1
}
void message_isr(){
is_message=1;//将有消息标志位置1。
}
void main(){
while(1){
        if(is_crying==1)
                eating();
        if(is_message==1)
                message();
}
}


设置了标志位以后,我们的中断处理函数就会很快执行,那么就不会影响到其他中断的处理,不会导致中断的延迟,丢失。

相信大家已经想到了,中断持续触发后的后续处理就退回轮询了。那岂不是我们这也没啥改进?别急,下面我们继续改进!

常用时间驱动方式:定时器
这里我先用韦老师的例子来给大家介绍这种方法,然后再来分析上面的例子的改进方法。

例子:宝妈喂饭这个例子只有两个任务,如果有多个任务,一些有经验的工程师会使用定时器来驱动

设置一个定时器,比如每1ms产生一次中断
对于函数A,可以设置它的执行周期,比如每1ms执行一次
对于函数B,可以设置它的执行周期,比如每2ms执行一次
对于函数C,可以设置它的执行周期,比如每3ms执行一次
注意:1ms、2ms、3ms只是假设,你可根据实际情况调整。
那么我们编写代码可以如下

typedef struct soft_timer{
        int remain;//表示剩余多少时间,就需要调用下面的函数
        int period;//表示周期
        void (*function)(void);//处理函数
}soft_timer,*p_soft_timer;

static soft_timer timers[]={
        {1,1,A},
        {2,2,B},
        {3,3,C}
};//符合题目要求

void main(){
        while(1){
        }
}
void timer_isr(){
        int i;//是每个timers数组成员的remain都减1.
        for(i=0;i<3;i++){
        timers.remain--;
}
//当remain减到0,就表示要调用对应结构体中的函数了
        for(i=0;i<3;i++){
        if(timers.remain==0){
        timers.function();//调用函数
        timers.remain=timers.period;//重置remain.
}
}
}




经过这样设置以后,我们很好的解决了每个人数的处理时间。但是对于当某一个程序执行时间很长,就会出现下面的后果:

影响其他函数的调用
延误整个时间基准
那么我们怎么改进呢?针对这个问题,这里我以上面第二个例子进行分析,对于宝妈问题同理

typedef struct soft_timer{
        int remain;
        int period;
        void (*function)(void);
}soft_timer,*p_soft_timer;

static soft_timer timers[]={
        {1,1,A},
        {2,2,B},
        {3,3,C}
};

void main(){
        while(1){
        for(int j=0;j<3;j++){
                if(flag){
                        timers.function();//调用函数
                }
        }
        }
}
void timer_isr(){
        for(i=0;i<3;i++){
        timers.remain--;
}
        for(i=0;i<3;i++){
        if(timers.remain==0){
        flag=1;//设置标志位
        timers.remain=timers.period;
}
}
}



通过上面设置标志位,来解决因为某个函数执行时间过长导致影响整个过程的时间基准。

使用状态机进行改进
问题,如果当任务处理函数执行时间都很长的时候,我们的裸机该怎么办呢?
这里我们可以使用状态机的思想来解决这个问题(其实思路就是操作系统的时间片)

void crying_isr(void)
{
        static int state = 0;

        switch (state)
        {
                case 0: /* 开始 */
                {
                        /* 盛饭 */
                        state++;
                        return;
                }

                case 1: /* 盛菜 */
                {
                        /* 盛菜 */
                        state++;
                        return;
                }

                case 2:
                {
                        /* 拿勺子 */
                        state++;
                        return;
                }
               
        }
}

void mesage_isr(void)
{
        static int state = 0;

        switch (state)
        {
                case 0: /* 开始 */
                {
                        /* 打开电脑 */
                        state++;
                        return;
                }

                case 1:
                {
                        /* 观看信息 */
                        state++;
                        return;
                }

                case 2:
                {
                        /* 打字 */
                        state++;
                        return;
                }
               
        }
}

void main()
{
        while (1)
    {
        crying_isr();
        message_isr();
       //其实就是将这个执行时间很长的函数,拆分为短时间来处理。
    }
}



显然这里使用状态机拆分程序:

比较麻烦
有些复杂的程序无法拆分为状态机。
总结
总的来说,裸机程序难以解决的问题就是,控制每个任务的运行时间。难以消除任务与任务之间的相互影响。

RTOS的引入
假设要调用两个函数AB,AB执行的时间都很长,使用裸机程序时可以把AB函数改造为"状态机",还可以使用RTOS。这两种方法的核心都是"分时复用":

分时:函数A运行一小段时间,函数B再运行一小段时间
复用:复用谁?就是CPU
这里还是以宝妈的例子进行分析:
将宝妈比作CPU,喂孩子比作函数A,回消息比作函数B

宝妈一会儿喂孩子饭,一会儿回消息。当这个时间足够短的时候,从宏观上来看就是两个事件同时发生;从微观上来看,这依旧是两件事情。



// RTOS程序   
喂饭()
{
    while (1)
    {
        喂一口饭();
    }
}

回信息()
{
    while (1)
    {
        回一个信息();
    }
}

void main()
{
    create_task(喂饭);//创建一个任务
    create_task(回信息);//创建一个任务
    start_scheduler();//执行任务列表
    while (1)
    {
        sleep();
    }
}



关键在于RTOS让多个任务轮流运行,不再需要我们手工在任务函数去使用状态机拆分程序。

注意: RTOS其实现的原理就是链表的操作,通过优先级的高低,形成遍历链表顺序的先后。达到优先级高,先处理。通过判断链表是否为空,判断是否需要执行函数。同时同一链表,通过分时,在时间片内时间执行一个任务后,将该任务放置链表末尾,进而执行下一个任务。
关于休眠和唤醒,其实就是将要休眠(或者没有达到满足条件的任务)放置到休眠链表中,当条件满足时再唤醒该任务。
后面详细介绍该部分。

RTOS编程要注意的问题
临界资源的访问
这里其实就是和我们平时在Linux上编程一样,要考虑临界资源的访问问题,解决办法依旧还是设置互斥锁。

任务的休眠唤醒
当我们对某一个任务的执行设置了条件的时候,如果我们不将被设置条件的任务进行休眠,那么这个函数就会不停的进行条件判断,如下

void main(){
        A(){
        //当A快要执行完,执行此内容(假设,也可能是某个条件)
        if(xxx){
        flag=1;
        }
        };
        if(flag){//如果不将B进行休眠,如果A执行100000次,那么这个if判断条件就会执行这么多次。所以这样就会造成浪费资源,没必要的开销。
        B();
        }
}


所以设置任务的休眠,将B进行休眠,就让A一直执行,当flag为1时,再唤醒B,这样就能避免这个浪费。
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/m0_56145255/article/details/122901150

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

91

主题

4146

帖子

2

粉丝