跳到主要内容

嵌入式软件架构演进

嵌入式开发主要包括单片机(MCU)开发和以 ARM 为代表的嵌入式 Linux 开发。本文主要讲解单片机开发中嵌入式软件架构的演进,包括从最初的简单轮询式架构,到前后台系统,再到如今复杂多样的实时操作系统(RTOS)。对比各种嵌入式软件架构的特点和差异,并讨论学习 RTOS 的必要性。

裸机系统

在嵌入式开发领域,“裸机开发”在没有操作系统(OS)或者其他高级软件支持的情况下,直接在裸机硬件上进行开发和编程的过程。这种方式需要开发人员直接与处理器、外设和硬件寄存器等底层硬件进行交互,以实现系统的功能和需求。通常涉及编写底层驱动程序、配置硬件寄存器、处理中断和定时器等底层任务。

裸机系统(Bare-metal embedded system)一般又分为轮询系统(polling system)和前后台系统(foreground/background system)。

轮询系统

轮询系统即是在裸机编程的时候,先初始化好相关的硬件,然后让主程序在一个死循环里面不断循环,顺序地做各种事情。

轮询系统的伪代码可表示如下:

int main(void)
{
/* 硬件相关初始化 */
HardWareInit();

/* 无限循环 */
while (1) {
/* 处理事情 1 */
DoSomething1();

/* 处理事情 2 */
DoSomething2();

/* 处理事情 3 */
DoSomething3();
}
}

轮询系统是一种最简单的软件结构,通常只适用于那些只需要顺序执行代码且不需要外部事件来驱动的就能完成的事情,当有外部事件驱动时(例如按键),实时性就会降低。在代码清单中,如果只是实现 LED 翻转,串口输出,液晶显示、驱动蜂鸣器等操作,那么使用轮询系统将会非常完美。

但是,如果加入了按键操作等需要检测外部信号的事件,用来模拟紧急报警,那么整个系统的实时响应能力就不会那么好了。假设 DoSomething3 是按键扫描,当外部按键被按下,相当于一个警报,这个时候,需要立马响应,并做紧急处理,而这个时候程序刚好执行到 DoSomething1,要命的是 DoSomething1 需要执行的时间比较久,久到按键释放之后都没有执行完毕,那么当执行到 DoSomething3 的时候就会丢失掉一次事件。

因此,轮询系统只适合顺序执行的功能代码,当有外部事件驱动时,实时性就会降低。

前后台系统

前后台系统是在轮询系统的基础上采用了中断处理,外部事件的响应在中断里面完成,事件的处理还是回到轮询系统中完成,中断响应称为前台, main 函数里面的无限循环称为后台,按顺序处理业务功能,以及中断标记的可执行的事件。

前后台系统的伪代码可表示如下:

int flag1 = 0;
int flag2 = 0;
int flag3 = 0;

int main(void)
{
/* 硬件相关初始化 */
HardWareInit();

/* 无限循环 */
while (1) {
if (flag1) {
/* 处理事情 1 */
DoSomething1();
}

if (flag2) {
/* 处理事情 2 */
DoSomething2();
}

if (flag3) {
/* 处理事情 3 */
DoSomething3();
}
}
}

void ISR1(void)
{
/* 置位标志位 */
flag1 = 1;
/* 如果事件处理时间很短,则在中断里面处理;
如果事件处理时间比较长,在回到后台处理 */
DoSomething1();
}

void ISR2(void)
{
/* 置位标志位 */
flag2 = 2;

/* 如果事件处理时间很短,则在中断里面处理;
如果事件处理时间比较长,在回到后台处理 */
DoSomething2();
}

void ISR3(void)
{
/* 置位标志位 */
flag3 = 1;
/* 如果事件处理时间很短,则在中断里面处理;
如果事件处理时间比较长,在回到后台处理 */
DoSomething3();
}

在顺序执行后台程序的时候,如果有中断来临,那么中断会打断后台程序的正常执行流,转而去执行中断服务程序,在中断服务程序里面标记事件,如果事件要处理的事情很简短,则可在中断服务程序里面处理,如果事件要处理的事情比较多,则返回到后台程序里面处理。

虽然事件的响应和处理是分开了,但事件的处理还是在后台里面顺序执行的,但相比轮询系统,前后台系统确保了事件不会丢失,再加上中断具有可嵌套的功能,这可以大大的提高程序的实时响应能力。在大多数的中小型项目中,前后台系统运用的好,堪称有操作系统的效果。

多线程系统

RTOS(Real Time Operating System,实时操作系统)也称为多线程/任务系统。相比前后台系统,多线程/任务系统的事件触发是在中断中完成的,但事件的处理是在线程/任务中完成的。在多线程/任务系统中,线程/任务跟中断一样,也具有优先级,当一个紧急的事件/信号在中断被触发之后,事件/信号对应的线程/任务的优先级足够高,就会立马得到响应。

因此,相比前后台系统,使用 RTOS 可以使整个系统的实时性更高。并且,当所有的线程/任务处于等待状态时,系统会进入 idle 线程/任务,在 idle 线程/任务里面可以让 CPU 进入 stop、standy 等低功耗模式,降低系统功耗。

多线程系统大概的伪代码可表示如下:

os_event event1;
os_event event2;

int main(void)
{
/* 硬件相关初始化 */
HardWareInit();

/* OS 初始化 */
RTOSInit();

/* OS 启动,开始多线程调度,不再返回 */
RTOSStart();

while(1); /* 程序不会执行到这里 */
}

void ISR1(void)
{
/* 触发event1事件 */
os_set_event(event1);
}

void ISR2(void)
{
/* 触发event2事件 */
os_set_event(event2);
}

void Task1(void) // 任务1
{
while(1) {
os_event_wait(event1);
DoSomething1();
}
}

void Task2(void) // 任务2
{
while(1) {
os_event_wait(event2);
DoSomething2();
}
}

void Task_idle(void) //优先级最低
{
cpu_stop();
}

在多线程/任务系统中,根据程序的功能,我们把这个程序主体分割成一个个独立的,无限循环且不能返回的小程序,称之为线程/任务。每个线程/任务都是独立的,互不干扰的,且具备自身的优先级,由操作系统进行调度管理。加入操作系统后,我们在编程的时候不需要精心地去设计程序的执行流,不用担心每个功能模块之间是否存在干扰。

对于一些具有复杂需求的系统,使用 RTOS 可以简化软件开发。当然,你需要为此付出一些代价 —— 整个系统随之带来的额外开销是操作系统占据的 Flash 和 RAM。不过如今单片机的 Flash 和 RAM 越来越大,完全能够满足使用 RTOS 所带来的开销。

选择哪种架构?

一个优秀的 RTOS 对复杂的嵌入式系统来说非常有必要,但并不是所有的嵌入式软件都需要使用 RTOS,尤其是对成本非常敏感的功能相对简单的项目。比如小型的电子产品(小家电、玩具等)大部分用的都是裸机系统,而且也能够满足需求。另外,在一些对资源和性能要求较高的嵌入式应用中,裸机系统仍然有广泛应用,例如实时控制系统、嵌入式传感器等领域。一般来说,如果的项目里面没有使用 RTOS,一般使用的都是前后台系统。

裸机开发的优势在于:

  1. 性能控制:由于没有操作系统的开销,裸机开发可以更好地控制系统的性能。
  2. 资源利用率:可以充分利用系统资源,避免了操作系统等高级软件可能引入的额外资源消耗。
  3. 实时性:裸机开发可以实现更高的实时性,适用于对时间要求严格的应用场景。

但裸机开发也存在一些挑战:

  1. 复杂性:需要对硬件及其编程方式有深入的理解,开发工作相对复杂。
  2. 可移植性:裸机开发通常依赖于特定硬件平台,因此不太容易移植到其他平台上。
  3. 维护困难:对系统的维护和更新可能较为困难,特别是随着系统复杂度的增加。

为什么要学 RTOS?

实际上,RTOS 的出现就是为了解决裸机开发中存在的劣势。随着嵌入式产品的功能越来越丰富,单纯的裸机系统已经不能完美地解决问题,反而会使编程变得更加复杂,后续的维护成本更高。如果想降低编程的难度,就必须引入 RTOS 实现多任务管理。因此,对于嵌入式软件工程师,学习 RTOS 非常有必要!

RTOS 因为有了任务优先级、任务调度、事件/信号量等管理机制,能更有效区处理紧急任务,能在 CPU 没有事情需要处理时,进入空闲任务方便低功耗管理。另外,RTOS 生态有众多的软件组件,可以极大的加速项目进度,避免重复造车轮。

最后,掌握 RTOS 之后,可以很容易切换到 Linux 生态。而 Linux 是目前应用最广泛的操作系统,因此可以拓宽嵌入式软件工程师的职业发展道路。