第一章 嵌入式常用裸机编程框架
第二章 面向对象编程
学习韦东山老师的七天物联网实战,以其课程笔记为骨,记录一下学习的过程,可能会加入一些自己的感想。
最后欢迎点赞,评论交流!
常用的嵌入式程序框架设计方案如下所述:
// 常用模式 void main() { while (1) { Task(A); Task(B); } }
函数之间互相有影响,若函数A耗时特别长,尤其包含一些无意义的delay函数进行耗时,那么B任务还得必须等待A任务执行完,这样的话B任务的执行情况就很不好。
我们在看芯片的datasheet的时候,尤其是新手,总会混淆事件和中断的概念,尤其是在配置寄存器的时候,有时某些操作会产生事件或中断,中断我们理解是cpu放下当前任务去执行其他任务,那事件是什么却有时候不是很明白?
其实事件是一个宽泛的概念,什么叫事件?可以是:按下了按键、串口接收到了数据、模块产生了中断、某个全局变量被设置了。事件就是发生了某事,至于怎么去处理它得看你的软硬件如何设置。
那什么叫事件驱动?就是当某个事件发生时,才调用对应函数,这就叫事件驱动。
void main() { while (1) { if (Flag_a) process_key(); } } void a_isr() /* 事件a产生中断 */ { key = xxx; Flag_a = 1; } void b_isr() /* 事件b产生中断 */ { Task(b); }
其实这种方式本质上还是采用轮询的方式,只不过使用了一些中断的技巧,但是b_isr()中进行了任务处理的一些函数,一般我们不这么使用中断,因为如果Task(b)耗时比较严重的话,cpu一直处于b任务的话,可能a中断的处理就会收到影响,比如a多次产生中断,但cpu一直在task(b)中,导致最后cpu应该就只处理一次a函数,所以一般我们常用以下的方式来改进事件驱动型框架。
void main() { while (1) { if (Flag_a == 1) Task(a); if (Flag_b == 1) Task(b); } } void a_isr() /* 中断a */ { Flag_a = 1; } void b_isr() /* 中断b */ { Flag_b = 1; }
改进型函数只在中断中修改标识符,处理速度就会快很多,不会导致别的中断被延迟、丢失。
但本质上还是轮询的框架。
“时间片”是嵌入式常听的一个概念,即原来的整个while(1)大循环切割成很多小的时间片,每过一个时间片就切换一次执行的任务,这样的话每个任务都在同时执行,不用等待。
例如ABC三个任务,A周期1ms,B周期2ms,C周期3ms,时间片为1ms,即第一个1ms,A执行,B剩余1ms,C剩余2ms;第二个1ms,A执行,B执行,C剩余1ms;第三个1ms,A执行,B剩余1ms,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() { while (1) { } } 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。根据它们运行时消耗的时间调整运行周期,也可以达到比较好的效果。
但是,一旦某个函数执行的时间超长加入超过1ms(一个时间片的长度),这样这个时间片内即使所有时间都用来处理这个任务时间也不够,因此等到下次定时器中断时就不能正常处理原来的ABC任务,就会有如下后果:
此处的改进并没有进行特别的改进,只不过把任务执行的函数放到主循环中,本质仍是轮询算法,若任务函数执行时间过长 ,仍然会有以下的弊端:
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--; }
若非要用“裸机”的思想处理时间片的所说的难点,需要引入“状态机”的概念,其内核思想是:将耗时的任务进行拆分,确保拆分后的任务可以在每个时间片中完成所需功能。
void task_A(void) { static int state = 0; switch (state) { case 0: /* 开始 */ { /* TA1 */ state++; return; } case 1: /* 拆分后任务片 */ { /* TA2 */ state++; return; } case 2: { /* TA3 */ state++; return; } } } void task_B(void) { static int state = 0; switch (state) { case 0: /* 开始 */ { /* TB1 */ state++; return; } case 1: { /* TB2 */ state++; return; } case 2: { /* TB3 */ state++; return; } } } void main() { while (1) { task_A(); task_B(); } }
但状态机拆分程序有以下难点:
基于裸机的程序框架无法完美地解决这类问题:复杂的、很耗时的多个函数。
假设要调用两个任务A、B,AB执行的时间都很长,可以使用以下两种办法来解决:1、使用裸机程序时可以把AB函数改造为"状态机",2、可以使用RTOS。
这两种方法的核心都是"分时复用":
分时:函数A运行一小段时间,函数B再运行一小段时间
复用:复用CPU
二者的核心思想类似,只不过因为RTOS会创建属于任务本身的堆栈,可以保存切换任务时的程序执行现场,因此不用我们再对任务进行人为的拆分。
// RTOS程序 Task_A() { while (1) { FuncA(); } } Task_B() { while (1) { FuncB(); } } void main() { create_task(Task_A); create_task(Task_B); start_scheduler(); while (1) { sleep(); } }