裸机与RTOS开发模式day1

裸机与RTOS开发模式——day1(提高)

​ 感谢韦东山老师的直播教学,在今天的这次教学中学到了很多实用的知识。对逻辑开发和RTOS开发有了本质的理解。

​ 主要讲了裸机与RTOS开发模式,先将了裸机的开发模式,主要分为轮训方式、事件驱动方式、改进的事件驱动方式、常用事件驱动方式——定时器。通过讲述裸机开发模式,总结了裸机程序存在的缺陷,进一步引入了RTOS开发的模式。

1. 裸机开发模式

首先根据一个例子来引入裸机

假设要做两件事:

  • 给小孩喂饭
  • 回复同事信息
    在这里插入图片描述

怎么写程序?

1.1 轮询方式

// 经典单片机程序
void main()
{
	while (1)
    {
        喂一口饭();
        回一个信息();
    }
}

缺点:函数之间互相有影响。如果喂饭的时间比较长,回复信息的就要推迟。反之也一样。

在这里插入图片描述

假设喂一口饭是t1-t5,回复信息时间是ta-te,小孩和同事的感觉是你理我一下,然后就不理我了。缺点很明显:在执行喂饭函数是无法执行回复消息的函数,同理,在回复消息的时候无法喂饭;这样子就相当于两边存在无法及时回复和处理的问题。那么如何解决这个问题呢,一些工程师使用中断来优化,中断这种优化方式又叫做事件驱动的方式。

1.2 事件驱动方式

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

main()
{
	while(1)
	{
		a();  //函数a里设置变量例如flag =1
         if (flag=1b();  //a就是事件,事件驱动
	}
}

在这里插入图片描述

min板子与wifi模块是串口通信,手机与wifi是无线通信,串口传输信号是一个事件,触发事件产生中断执行相应的。

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

比如上述的例子,我们可以这样写程序:

  • 孩子喊叫时,再给他喂一口饭
  • 同事发来信息电脑响时,再回复一个信息

写成程序就是这样:

void main()
{
	while (1)
    {
        if (get_key)
            process_key();
    }
}

void key_isr() /* 孩子喊叫触发中断a */
{
    key = xxx;
    get_key = 1;
}

void b_isr() /* 同事发来信息触发中断b */
{
    回一个信息();
}

当这两个中断函数执行得都很快,这种编程方式很好。

如果a、b中断同时发生,就会互相影响:

  • 两个中断,同一时间只能处理一个
  • 如果当前中断处理时间比较长,就会影响到另一个中断的处理

1.3 改进的事件驱动方式

对于中断的处理,原则是"尽快"。否则会影响到其他中断,导致其他中断的处理被延迟、甚至丢失。

如果某些中断的处理确实比较慢,怎么办?

void main()
{
	while (1)
    {
        if (crying == 1)
            喂一口饭();
        if (get_msg == 1)
            回一个信息();
    }
}

void a_isr() /* 孩子喊叫触发中断a */
{
    crying = 1;
}

void b_isr() /* 同事发来信息触发中断b */
{
    get_msg = 1;
}

设置标志位 来处理中断,就不会影响其他中断的处理,不会导致中断的延迟,但是:

可以解决中断的相应问题:中断处理很快,不会导致别的中断被延迟、丢失。

但是中断触发的后续处理退化为轮询方式!相互之间有会产生影响。

1.4 常用事件驱动方式:定时器

上述例子中只有两个任务,如果有更多的任务,很多有经验的工程师会使用定时器来驱动:

  • 设置一个定时器,比如每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;

//定义一个数组,比如{1,1,A}函数A,每1ms执行一次,执行完还剩来1ms
static soft_timer timers[] = {
    {1, 1, A},
    {2, 2, B},
    {3, 3, C},
};

void main()
{
	while (1)
    {
    }
}

//初始化一个定时器,定时器里假设没过1ms会产生一次硬件中断,这个函数就会被调用。
void timer_isr() 
{
    int i;
    //遍历数组
	/* timers数组里每个成员的expire都减一 */
    for (i = 0; i < 3; i++)
        timers[i].remain--;
    //再去遍历数组,如果发现:
    /* 如果timers数组里某个成员的expire等于0:(时间到了,要调用函数了)
     *   1. 调用它的函数
     *   2. 恢复expire为period
     */
    for (i = 0; i < 3; i++)
    {
        if (timers[i].remain == 0) //(时间到了,要调用函数了)
        {
            timer[i].function();   //调用函数
            timers[i].remain = timers[i].period; //让定时器剩余时间复位。
        }
    }
}

上述例子中有三个函数:A、B、C。根据它们运行时消耗的时间调整运行周期,也可以达到比较好的效果。

在这里插入图片描述

但是,一旦某个函数执行的时间超长,就会有如下后果:如图所示

  • 影响其他函数
  • 延误整个时间基准

当然可以改进:

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()
{
    int i;
	while (1)
    {
        /* 如果timers数组里某个成员的expire等于0:
         *   1. 调用它的函数
         *   2. 恢复expire为period
         */
        for (i = 0; i < 3; i++)
        {
            if (timers[i].remain == 0)
            {
                timer[i].function();
                timers[i].remain = timers[i].period;
            }
        }
    }
}

void timer_isr() 
{
    int i;
	/* timers数组里每个成员的expire都减一 */
    for (i = 0; i < 3; i++)
        if (timers[i].remain)
	        timers[i].remain--;    
}

通过设置标志位,时间基准不会被耽误,A、B、C的调用再次退化为轮询方式,ABC相互之间有影响。

2. 裸机程序的缺陷

假设要调用两个函数AB,AB执行的时间都很长,裸机就很难处理这种场景。

如果非要基于裸机解决这个问题的话,可以使用状态机。人为把一个复杂的函数拆分成若干块。(图3)

示例代码如下:

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

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

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

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

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

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

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

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

void main()
{
	while (1)
    {
        feed_kid();
        send_msg();
    }
}

需要我们使用状态机拆分程序:

  • 比较麻烦
  • 有些复杂的程序无法拆分为状态机

基于裸机的程序框架无法完美地解决这类问题:复杂的、很耗时的多个函数。在这里插入图片描述

3. RTOS的引入

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

  • 分时:函数A运行一小段时间,函数B再运行一小段时间
  • 复用:复用谁?就是CPU

还是以那位妈妈为例,对于眼明手快的人,她可以一心多用,她这样做:

  • 左手拿勺子,给小孩喂饭
  • 右手敲键盘,回复同事
  • 两不耽误,小孩“以为”妈妈在专心喂饭,同事“以为”她在专心聊天
  • 但是脑子只有一个啊,虽然说“一心多用”,但是谁能同时思考两件事?
  • 只是她反应快,上一秒钟在考虑夹哪个菜给小孩,下一秒钟考虑给同事回复什么信息

程序运行时间图如下:

在这里插入图片描述

示例代码如下:

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

回信息()
{
    while (1)
    {
        回一个信息();
    }
}
//上面的函数根本不用担心会影响到别人。
void main()
{
    create_task(喂饭);//创建任务
    create_task(回信息);
    start_scheduler();  //启动调度器
    while (1)
    {
        sleep();
    }
}

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

4. RTOS编程要注意的新问题

4.1 临界资源的访问

有可能被同时使用的资源。

4.2 任务的休眠唤醒

当我们对某一个任务的执行设置了条件的时候,如果我们不将被设置条件的任务进行休眠,那么这个函数就会不停的进行条件判断,如下:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sICDdEI9-1648701633244)(裸机与RTOS开发模式day1.assets/4.png)]

引用休眠-唤醒,使得CPU利用率更高了。
void main(){
	A(){
	//假设A执行要1亿次,一亿次后flag被赋值为1。
	if(判断)
	flag=1;
	};
    //只有flag==1才能执行B函数。
	if(flag==1)//如果不将B进行休眠,如果A执行1亿次,那么这个if判断条件就会执行这么多次。所以这样就会造成浪费资源,没必要的开销。
	B();
}

4. RTOS编程要注意的新问题

4.1 临界资源的访问

有可能被同时使用的资源。

4.2 任务的休眠唤醒

当我们对某一个任务的执行设置了条件的时候,如果我们不将被设置条件的任务进行休眠,那么这个函数就会不停的进行条件判断,如下:
在这里插入图片描述

引用休眠-唤醒,使得CPU利用率更高了。
void main(){
	A(){
	//假设A执行要1亿次,一亿次后flag被赋值为1。
	if(判断)
	flag=1;
	};
    //只有flag==1才能执行B函数。
	if(flag==1)//如果不将B进行休眠,如果A执行1亿次,那么这个if判断条件就会执行这么多次。所以这样就会造成浪费资源,没必要的开销。
	B();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值