嵌入式软件架构重构与代码优化实战 - 第1节

一、嵌入式软件架构到底是什么?

做了十几年嵌入式开发,我经常被问到这个问题。说白了,嵌入式软件架构就是代码的组织方式——你决定哪些功能写在一起,哪些分开,谁调用谁,数据怎么流动。

拿盖房子打个比方。架构就是房子的框架结构。没有框架,你也能把砖头水泥堆起来,但风一吹就倒。嵌入式软件也一样。没有架构,代码也能跑,但改一个功能可能引发三个bug。

我个人习惯把架构拆成三个层面来看:

  • 模块划分:功能怎么切分,比如传感器采集、通信协议、控制算法各归各的
  • 交互机制:模块之间怎么说话,是直接调用、回调函数、还是消息队列
  • 资源管理:CPU时间、内存、中断这些稀缺资源怎么分配

核心观点:架构不是写出来的,是长出来的。好的架构能让你在项目后期还能睡得着觉。

二、为什么需要架构重构?

你想想看,一个嵌入式项目从原型到量产,代码量可能翻10倍。我见过太多项目,一开始图快,所有逻辑都塞在main函数的while循环里。等产品要加新功能时,发现动一行代码要测试三天。

什么时候该考虑重构?我总结了几条红线:

症状 说明 我踩过的坑
改一处崩三处 一个全局变量被10个函数读写 我曾经改了个LED闪烁的延时,结果把电机控制搞崩了
中断服务函数太长 ISR里做浮点运算、调printf 某次中断里调了malloc,系统随机死机,查了两周
模块耦合严重 传感器驱动里直接操作LCD显示 换个显示屏,整个驱动层重写
测试覆盖率低 手动测试占80%以上时间 每次发版前通宵测,还是漏了边界条件

避坑指南:我曾经接手过一个项目,代码里光全局变量就定义了200多个。重构花了三个月,但重构后bug率下降了70%。记住:重构不是重写,是逐步改善。

三、三种常见嵌入式架构模式

3.1 前后台系统(Super Loop)

这是最原始也最直接的架构。主循环里轮询各个任务,中断负责处理紧急事件。

// 典型的前后台架构
void main(void)
{
    system_init();
    
    while(1)
    {
        task_scan_key();      // 轮询按键
        task_update_display(); // 刷新显示
        task_control_motor();  // 控制电机
        // ... 更多任务
    }
}

void TIMER_IRQHandler(void)
{
    // 中断服务:处理定时事件
    g_tick_count++;
}

这种架构的优点很明显:简单、直观、资源开销小。我最早做8位单片机项目时,全是这种写法。

但问题也很突出:

  • 任务之间互相阻塞——一个任务卡住,整个系统就停了
  • 实时性无法保证——按键扫描可能被电机控制拖慢
  • 扩展性差——加一个新功能,主循环就多一个函数调用

适用场景:功能固定、任务数少于10个、对实时性要求不高的产品。比如简单的温控器、电子秤。

3.2 事件驱动架构

前后台架构最大的痛点是CPU空转。事件驱动架构解决了这个问题——有事件才干活,没事件就休眠。

核心思想是:系统维护一个事件队列,各个模块产生事件并投递到队列,主循环或调度器从队列取出事件并分发处理。

// 事件驱动架构示意
typedef struct {
    uint32_t event_id;
    void*    data;
} event_t;

event_t g_event_queue[32];
uint8_t  g_queue_head, g_queue_tail;

void event_post(uint32_t evt_id, void* data)
{
    // 将事件放入队列
    g_event_queue[g_queue_head].event_id = evt_id;
    g_event_queue[g_queue_head].data = data;
    g_queue_head = (g_queue_head + 1) % 32;
}

void event_dispatch(void)
{
    while(g_queue_head != g_queue_tail)
    {
        event_t evt = g_event_queue[g_queue_tail];
        g_queue_tail = (g_queue_tail + 1) % 32;
        
        switch(evt.event_id)
        {
            case EVT_KEY_PRESS:    handle_key(evt.data); break;
            case EVT_SENSOR_READY: handle_sensor(evt.data); break;
            // ...
        }
    }
}

这种架构的好处是:

  • CPU利用率高——没事做就进低功耗模式
  • 模块解耦——按键模块不需要知道显示模块的存在
  • 可预测性强——事件处理顺序由队列控制

我记得有个电池供电的物联网项目,从前后台改成事件驱动后,待机电流从5mA降到了50μA。原因很简单——CPU不再空转了。

3.3 RTOS(实时操作系统)架构

当任务数量超过15个,或者有严格的实时性要求时,就该上RTOS了。

RTOS的核心是任务调度。每个任务有自己的栈空间和优先级,调度器决定谁在什么时候运行。

// FreeRTOS 任务创建示例
void task_sensor(void *pvParameters)
{
    while(1)
    {
        read_sensor_data();
        xQueueSend(sensor_queue, &data, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(100)); // 100ms采集一次
    }
}

void task_control(void *pvParameters)
{
    sensor_data_t data;
    while(1)
    {
        if(xQueueReceive(sensor_queue, &data, portMAX_DELAY))
        {
            calculate_control_output(&data);
            set_motor_speed(output);
        }
    }
}

void main(void)
{
    xTaskCreate(task_sensor, "Sensor", 256, NULL, 2, NULL);
    xTaskCreate(task_control, "Control", 512, NULL, 3, NULL);
    vTaskStartScheduler();
}

RTOS带来的好处:

  • 任务隔离:一个任务崩溃不会拖垮整个系统
  • 实时保障:高优先级任务能抢占CPU
  • 丰富的IPC:队列、信号量、互斥锁、事件组

注意:RTOS不是银弹。我见过有人用RTOS做只有3个任务的系统,结果光任务切换开销就占了20%的CPU。杀鸡用牛刀,反而得不偿失。

四、三种架构对比

维度 前后台 事件驱动 RTOS
代码量 最小 中等 较大(含内核)
实时性 差 中等 好
功耗控制 差 好 中等
调试难度 低 中等 高
扩展性 差 好 好
典型RAM开销 <1KB 1-4KB 4-64KB

五、架构选择决策流程

说了这么多,到底怎么选?我画了张图,帮你快速决策。

嵌入式架构选择决策流程 开始:评估项目需求 任务数量 < 10 个? 是 否 实时性要求高? 是 否 前后台架构 简单、低开销 事件驱动架构 低功耗、解耦好 RAM/Flash充足? 是 否 RTOS架构 强实时、多任务 事件驱动(精简版) 资源受限下的折中 决策要点 • 任务数量:前后台适合 <10 个任务,RTOS 适合 >15 个任务 • 实时性:硬实时(微秒级)必须用 RTOS,软实时(毫秒级)事件驱动可胜任 • 资源:RTOS 内核通常需要 4KB+ RAM,8KB+ Flash • 团队经验:如果团队没做过 RTOS,先从事件驱动开始

六、我的建议

说了这么多,最后分享几点个人经验:

  • 别过度设计:我见过有人用RTOS做电子表,结果电池三天就没电了。架构够用就好。
  • 留好扩展接口:哪怕现在只用前后台,也把模块接口设计成事件驱动风格。将来升级RTOS时,改起来快得多。
  • 重视中断延迟:不管用什么架构,ISR里只做最必要的事。我曾经在ISR里做浮点运算,导致系统响应延迟从5μs飙升到200μs。
  • 先画图再写代码:架构图不是给领导看的,是给自己理思路的。画完图再动手,能少走一半弯路。

一句话总结:架构选择没有标准答案,只有最适合当前项目的方案。理解每种架构的优缺点,结合项目实际做决策,这才是架构师的核心能力。