4. 嵌入式软件架构设计:前后台系统、RTOS与裸机选型

做嵌入式这么多年,我见过太多人在软件架构选型上栽跟头。说白了,选错了架构,后面调试、维护、加功能,每一步都是坑。今天咱们就把这几种架构掰开揉碎,讲清楚什么时候该用什么。

4.1 裸机编程:最直接的轮子

裸机,就是没有操作系统,没有调度器。你的代码直接跑在硬件上,一个 main 函数从头跑到尾。

核心特征:顺序执行、中断嵌套、全局变量满天飞。

我在早期做智能家居面板时,就用过纯裸机。功能简单:读按键、刷新屏幕、控制灯光。代码结构大概这样:

void main(void) {
    system_init();
    while(1) {
        key_scan();        // 扫描按键
        display_update();  // 刷新显示
        led_control();     // 控制灯光
        delay_ms(10);      // 简单延时
    }
}

这种写法,优点是简单、直观、资源占用极低。但缺点也很明显——实时性全靠程序员手算。你想想看,如果按键扫描里加了个耗时的 EEPROM 写入,屏幕刷新就得等,用户按了键没反应,体验极差。

对比项 裸机 前后台系统 RTOS
资源占用 极低(KB级RAM) 低(几KB RAM) 中等(10KB+ RAM)
实时性 依赖主循环周期 中断响应快,任务响应慢 可预测,优先级抢占
任务数量 1~3个 3~8个 8个以上
调试难度 低 中等 较高(需理解调度机制)
典型场景 玩具、简单传感器 家电控制面板 工业控制、无人机

4.2 前后台系统:中断驱动的艺术

前后台系统,也叫「中断驱动系统」。前台是中断服务程序(ISR),处理紧急事件;后台是主循环,处理非紧急任务。

我曾经做过一个温控器项目,用的就是前后台架构。温度传感器通过外部中断通知 MCU 读取数据,主循环里只做 PID 计算和屏幕刷新。效果不错,但有个坑——中断里不能做太多事。

避坑指南:我曾经在 ISR 里直接调用了 printf 打印调试信息,结果系统频繁死机。后来才发现,printf 是阻塞的,中断里一卡,整个系统就崩了。ISR 里只做标记,具体处理放主循环。

前后台系统的典型代码结构:

volatile uint8_t g_flag_adc_ready = 0;

void ADC_IRQHandler(void) {
    g_flag_adc_ready = 1;  // 只设标志,不做处理
}

void main(void) {
    while(1) {
        if(g_flag_adc_ready) {
            g_flag_adc_ready = 0;
            process_adc_data();  // 后台处理
        }
        update_display();
        check_keypad();
    }
}

这种架构,中断响应快,但后台任务之间没有优先级区分。如果同时有多个标志被置位,处理顺序完全取决于代码顺序。说白了,就是「谁先被检查,谁先被服务」。

4.3 RTOS:当复杂度超过临界点

当你的系统里有 5 个以上需要独立调度的任务时,就该上 RTOS 了。FreeRTOS 和 RT-Thread 是目前最主流的两个选择。

4.3.1 FreeRTOS:轻量级王者

FreeRTOS 我用了快十年。它的核心优势是极小、极稳定。一个最小系统只需要 4KB ROM 和 1KB RAM。任务调度、信号量、消息队列、软件定时器,该有的都有。

我记得有个项目,要在 STM32F103 上跑 8 个任务:传感器采集、数据解析、显示刷新、通信管理、日志记录、看门狗喂狗、按键处理、LED 指示。用裸机写,光任务切换逻辑就够写几百行。用 FreeRTOS,每个任务就是一个独立线程:

void task_sensor(void *pvParameters) {
    while(1) {
        read_sensor();
        vTaskDelay(pdMS_TO_TICKS(100));  // 100ms 采集一次
    }
}

void task_display(void *pvParameters) {
    while(1) {
        update_screen();
        vTaskDelay(pdMS_TO_TICKS(50));   // 50ms 刷新一次
    }
}

int main(void) {
    xTaskCreate(task_sensor, "Sensor", 128, NULL, 1, NULL);
    xTaskCreate(task_display, "Display", 256, NULL, 2, NULL);
    vTaskStartScheduler();
    while(1);  // 不会执行到这里
}

个人经验:任务栈大小别抠太死。我习惯给每个任务多分配 20% 的栈空间,防止递归调用或局部变量过大导致栈溢出。调试时可以用 uxTaskGetStackHighWaterMark() 查看实际使用量。

4.3.2 RT-Thread:国产之光

RT-Thread 比 FreeRTOS 重一些,但功能更丰富。它自带设备驱动框架、文件系统、网络协议栈,甚至还有 POSIX 接口。如果你做的是物联网网关或带屏幕的智能设备,RT-Thread 能省很多事。

我去年做的一个边缘计算节点,需要同时管理 WiFi、MQTT、本地存储和 Web 配置界面。用 FreeRTOS 的话,这些组件都得自己移植。用 RT-Thread,直接 menuconfig 勾选就行,开箱即用。

对比项 FreeRTOS RT-Thread
最小 ROM 4KB 10KB
最小 RAM 1KB 3KB
调度方式 抢占式 + 协作式 抢占式 + 时间片轮转
组件生态 第三方移植 官方包管理器
学习曲线 平缓 中等
适合场景 资源受限、任务少 功能丰富、需要网络/文件系统

4.4 选型策略:到底该用哪个?

这个问题没有标准答案,但我总结了一套三步判断法:

  1. 数任务:如果功能逻辑可以拆成 3 个以内独立循环,裸机或前后台就够了。超过 5 个,上 RTOS。
  2. 看实时性:如果某个任务必须在 1ms 内响应(比如电机控制),RTOS 的抢占式调度是刚需。
  3. 算资源:MCU 的 RAM 小于 8KB、Flash 小于 64KB,优先考虑裸机或 FreeRTOS。资源充裕,RT-Thread 更省心。

我的建议:新手做项目,如果拿不准,先用裸机写个原型。跑通了,发现任务切换逻辑越来越复杂,再迁移到 RTOS。别一开始就上 RTOS,调度器本身也是 bug 源。

下面这张图,是我自己总结的选型决策流程,供你参考:

嵌入式软件架构选型决策流程 开始评估 任务数量 > 5 个? 是 实时性要求 < 1ms? 是 RAM > 8KB 且 Flash > 64KB? 是 推荐:RT-Thread(功能丰富) 否 推荐:裸机或前后台系统 否 否 推荐:FreeRTOS(轻量高效) 提示:以上为通用建议,实际选型还需考虑团队经验、生态支持和维护成本

4.5 实际项目中的选型案例

我挑两个真实项目,说说当时是怎么选的。

案例一:智能门锁

  • 任务:指纹识别、密码输入、蓝牙通信、电机驱动、低功耗管理
  • MCU:STM32L051(8KB RAM, 64KB Flash)
  • 选型:FreeRTOS
  • 原因:任务数 5 个,低功耗模式下需要精确控制唤醒时机,FreeRTOS 的 tickless 模式正好合适。

案例二:便携式血氧仪

  • 任务:传感器采集、数据处理、OLED 显示、按键响应
  • MCU:GD32F103(20KB RAM, 128KB Flash)
  • 选型:前后台系统
  • 原因:任务只有 4 个,且采集和显示之间有明确的数据依赖关系,用标志位控制流程更简单。

避坑指南:我曾经在一个项目里,因为觉得 RTOS 很酷,硬给一个只需要 3 个任务的温控器上了 FreeRTOS。结果调试时发现,任务切换导致 ADC 采样时序抖动,反而比裸机更难调。后来换回前后台,问题立刻解决。记住:合适的才是最好的。

4.6 小结

嵌入式软件架构没有银弹。裸机简单直接,前后台灵活可控,RTOS 强大但复杂。选型时,先数任务、再看实时性、最后算资源。别为了用而用,也别因为怕学而不用。

嗯,这一章就到这里。记住我那句老话:架构选对了,后面全是顺风局;选错了,每一步都是逆风翻盘。