裸机与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=1)
b(); //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();
}