第2章
裸机系统与多线程系统
在真正开始动手编写RTOS之前,我们先来讲解一下单片机编程中的裸机系统和多线程系统。
2.1 裸机系统
裸机系统通常分成轮询系统和前后台系统,有关这两者的具体实现方式请看下面的讲解。
2.1.1 轮询系统
轮询系统即在裸机编程过程中,先初始化相关的硬件,然后让主程序在一个死循环里面不断循环,顺序地做各种事情,大概的伪代码参见代码清单2-1。轮询系统是一种非常简单的软件结构,通常只适用于那些只需要顺序执行代码且不需要外部事件来驱动就能完成的操作。在代码清单2-1中,如果只是实现LED翻转、串口输出、液晶显示等操作,那么使用轮询系统将会非常完美。但是,如果加入了按键操作等需要检测外部信号的事件,或者用来模拟紧急报警,那么整个系统的实时响应能力就不会那么好了。假设DoSomething3是按键扫描操作,当外部按键被按下,相当于产生一个警报,这个时候,需要立刻响应,并做紧急处理,而这时程序刚好执行到DoSomething1,并且DoSomething1执行的时间会比较久,久到按键释放之后都没有执行完毕,那么当执行到DoSomething3时就会丢失一次事件。由此可见轮询系统只适合用于顺序执行的功能代码,当有外部事件驱动时,实时性就会降低。
代码清单2-1 轮询系统伪代码
1 int main(void) 2 { 3 /* 硬件相关初始化 */ 4 HardWareInit(); 5 6 /* 无限循环 */ 7 for (; ; ) { 8 /* 处理事件1 */ 9 DoSomething1(); 10 11 /* 处理事件2 */ 12 DoSomething2(); 13 14 /* 处理事件3 */ 15 DoSomething3(); 16 } 17 }
2.1.2 前后台系统
相较于轮询系统,前后台系统是在轮询系统的基础上加入了中断。外部事件的响应在中断里面完成,对事件的处理还是回到轮询系统中完成。在这里我们称中断为“前台”, main()函数里面的无限循环称为“后台”,大概的伪代码参见代码清单2-2。
代码清单2-2 前后台系统伪代码
1 int flag1 = 0; 2 int flag2 = 0; 3 int flag3 = 0; 4 5 int main(void) 6 { 7 /* 硬件相关初始化 */ 8 HardWareInit(); 9 10 /* 无限循环 */ 11 for (; ; ) { 12 if (flag1) { 13 /* 处理事件1 */ 14 DoSomething1(); 15 } 16 17 if (flag2) { 18 /* 处理事件2 */ 19 DoSomething2(); 20 } 21 22 if (flag3) { 23 /* 处理事件3 */ 24 DoSomething3(); 25 } 26 } 27 } 28 29 void ISR1(void) 30 { 31 /* 置位标志位 */ 32 flag1 = 1; 33 /* 如果事件处理时间很短,则在中断里面处理; 34 如果事件处理时间比较长,则回到后台处理 */ 35 DoSomething1(); 36 } 37 38 void ISR2(void) 39 { 40 /* 置位标志位 */ 41 flag2 = 1; 42 43 /* 如果事件处理时间很短,则在中断里面处理; 44 如果事件处理时间比较长,则回到后台处理 */ 45 DoSomething2(); 46 } 47 48 void ISR3(void) 49 { 50 /* 置位标志位 */ 51 flag3 = 1; 52 53 /* 如果事件处理时间很短,则在中断里面处理; 54 如果事件处理时间比较长,则回到后台处理 */ 55 DoSomething3(); 56 }
在顺序执行后台程序时,如果有中断产生,那么中断会打断后台程序的正常执行流,转而去执行中断服务程序,在中断服务程序里面标记事件。如果要处理的事件很简短,则可在中断服务程序里面处理,如果要处理的事件比较繁杂,则返回后台程序中处理。虽然事件的响应和处理被分开了,但是事件的处理还是在后台中顺序执行的,相比轮询系统,前后台系统确保了事件不会丢失,再加上中断具有可嵌套的功能,这可以大大提高程序的实时响应能力。在大多数中小型项目中,前后台系统运用得好,堪比操作系统的效果。
2.2 多线程系统
相比前后台系统,多线程系统的事件响应也是在中断中完成的,但事件的处理是在线程中完成的。在多线程系统中,线程与中断一样,也具有优先级,优先级高的线程会被优先执行。当一个紧急事件在中断中被标记之后,如果事件对应的线程的优先级足够高,就会立刻得到响应。相比前后台系统,多线程系统的实时性又被提高了。多线程系统大概的伪代码参见代码清单2-3。
代码清单2-3 多线程系统伪代码
1 int flag1 = 0; 2 int flag2 = 0; 3 int flag3 = 0; 4 5 int main(void) 6 { 7 /* 硬件相关初始化 */ 8 HardWareInit(); 9 10 /* OS初始化 */ 11 RTOSInit(); 12 13 /* OS启动,开始多线程调度,不再返回 */ 14 RTOSStart(); 15 } 16 17 void ISR1(void) 18 { 19 /* 置位标志位 */ 20 flag1 = 1; 21 } 22 23 void ISR2(void) 24 { 25 /* 置位标志位 */ 26 flag2 = 2; 27 } 28 29 void ISR3(void) 30 { 31 /* 置位标志位 */ 32 flag3 = 1; 33 } 34 35 void DoSomething1(void) 36 { 37 /* 无限循环,不能返回 */ 38 for (; ; ) { 39 /* 线程实体 */ 40 if (flag1) { 41 42 } 43 } 44 } 45 46 void DoSomething2(void) 47 { 48 /* 无限循环,不能返回 */ 49 for (; ; ) { 50 /* 线程实体 */ 51 if (flag2) { 52 53 } 54 } 55 } 56 57 void DoSomething3(void) 58 { 59 /* 无限循环,不能返回 */ 60 for (; ; ) { 61 /* 线程实体 */ 62 if (flag3) { 63 64 } 65 } 66 }
相比前后台系统中后台顺序执行的程序主体,在多线程系统中,根据程序的功能,我们把这个程序主体分割成一个个独立的、无限循环且不能返回的小程序,这个小程序我们称之为“线程”。每个线程都是独立的、互不干扰的,且具备自身的优先级,它由操作系统调度管理。加入操作系统后,我们在编程时不需要再精心设计程序的执行流,不用担心每个功能模块之间是否存在干扰。加入了操作系统,我们的编程反而变得简单了。整个系统带来的额外开销就是操作系统占据的少量FLASH和RAM。现如今,单片机的FLASH和RAM容量越来越大,完全足以支撑RTOS的开销。
无论是轮询系统、前后台系统还是多线程系统,不能单纯地评定孰优孰劣,它们是不同时代的产物,在各自的领域有相当大的应用价值,只有合适的才是最好的。有关这3个软件模型的区别如表2-1所示。
表2-1 轮询系统、前后台系统和多线程系统软件模型的区别