1. 课程导入

嵌入式软件架构,说白了就是一套组织代码的规矩。没有规矩,项目做到一半就会变成一团乱麻。我见过太多团队,前期图快,把所有逻辑揉在一个文件里,结果后期改一个GPIO电平都要翻半天代码——这种痛,经历过一次就再也不想碰了。

这一章,我们聊聊架构的底层逻辑。不扯虚的,全是实战中能落地的原则。

1.1 为什么嵌入式软件需要架构?

很多人觉得嵌入式系统资源有限,搞架构是浪费Flash。其实恰恰相反。资源越紧张,架构越重要。为什么?

  • 可维护性:一个项目少则半年,多则三五年。没有架构,后面接手的人根本看不懂。
  • 可移植性:换MCU、换外设是家常便饭。好的架构让你只改驱动层,业务代码几乎不动。
  • 可测试性:分层之后,每一层都能单独测试。我习惯在开发阶段就写单元测试,架构清晰了,测试才好写。
  • 团队协作:驱动工程师写驱动,应用工程师写业务,互不干扰。接口定好,并行开发。

核心观点:架构不是用来炫技的,是用来降低长期维护成本的。嵌入式软件的成本,80%花在维护上。

1.2 分层架构:从硬件到业务的清晰边界

分层架构是嵌入式软件最经典的架构模式。我参与过的项目,从智能家居到工业控制器,几乎都采用这种模式。它把软件从上到下分成三层:

层级 职责 典型内容 依赖关系
业务层 实现产品功能逻辑 状态机、算法、协议解析 只调用驱动接口层
驱动接口层 抽象硬件操作,提供统一API 设备句柄、读写函数、配置结构体 调用硬件驱动层
硬件驱动层 直接操作寄存器、外设 GPIO、SPI、I2C、UART底层代码 依赖MCU手册

每一层只和相邻层通信。业务层永远不直接操作寄存器。驱动接口层不关心业务逻辑。这就是分层最核心的约束。

我的习惯:在驱动接口层定义一个结构体,把所有操作函数打包成一张表。业务层拿到这个结构体指针,就能调用所有驱动功能。这样换驱动时,业务层代码一行都不用改。

1.3 模块化设计:高内聚、低耦合

模块化是分层架构的落地手段。每个模块只做一件事,并且做好。我见过最糟糕的代码,一个.c文件里既有I2C驱动,又有温度换算,还有日志打印——这种代码根本没法复用。

模块化设计有几个原则:

  • 单一职责:一个模块只负责一个功能。比如,i2c_driver.c只做I2C通信,不做协议解析。
  • 接口最小化:暴露给外部的函数越少越好。能声明为static的函数,绝不放在头文件里。
  • 依赖方向:业务层依赖驱动接口层,驱动接口层依赖硬件驱动层。绝不允许反向依赖。
/* 驱动接口层示例:温度传感器驱动接口 */
typedef struct {
    int32_t (*init)(void);
    int32_t (*read_temperature)(float *temp);
    int32_t (*deinit)(void);
} temp_sensor_t;

/* 业务层使用方式 */
void temperature_monitor_task(void *param) {
    temp_sensor_t *sensor = (temp_sensor_t *)param;
    float temp;
    
    if (sensor->init() != 0) {
        // 错误处理
        return;
    }
    
    while (1) {
        if (sensor->read_temperature(&temp) == 0) {
            // 处理温度数据
        }
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

避坑指南:我曾经在一个项目中,把驱动初始化放在业务层的构造函数里。结果换了一个型号的传感器,业务层代码改了三分之一。后来我强制要求:驱动初始化必须在驱动层完成,业务层只负责调用。

1.4 驱动层与业务层解耦:接口即契约

解耦的核心是定义清晰的接口。接口就是契约,双方都遵守这个契约,各干各的活。

解耦的具体做法:

  1. 抽象硬件差异:用结构体封装操作函数,业务层只认这个结构体。
  2. 使用回调机制:驱动层完成操作后,通过回调通知业务层。业务层不需要轮询。
  3. 引入中间层:如果驱动层和业务层差异太大,加一个适配层。比如,把不同厂商的WiFi模块统一成同一个API。

举个例子。同样是SPI Flash,不同厂商的指令集可能不同。但业务层只关心“读”“写”“擦除”。驱动接口层把这些操作抽象成统一函数,底层驱动去适配具体芯片。

/* 驱动接口层:统一SPI Flash接口 */
typedef struct {
    int32_t (*read)(uint32_t addr, uint8_t *buf, uint32_t len);
    int32_t (*write)(uint32_t addr, const uint8_t *buf, uint32_t len);
    int32_t (*erase)(uint32_t addr, uint32_t len);
} spi_flash_t;

/* 硬件驱动层:适配W25Q64 */
static int32_t w25q64_read(uint32_t addr, uint8_t *buf, uint32_t len) {
    // 发送W25Q64专用的读指令
    // ...
}

static int32_t w25q64_write(uint32_t addr, const uint8_t *buf, uint32_t len) {
    // 发送W25Q64专用的写指令
    // ...
}

/* 注册到接口层 */
spi_flash_t flash = {
    .read = w25q64_read,
    .write = w25q64_write,
    .erase = w25q64_erase
};

业务层拿到flash这个结构体,根本不需要知道底层是W25Q64还是GD25Q64。换芯片?改驱动层,业务层不动。

1.5 架构落地:一张图看懂整体结构

说了这么多,不如一张图来得直观。下面这张图展示了典型的分层架构和模块间的关系。

嵌入式软件分层架构图 业务层(Application Layer) 状态机 · 算法 · 协议解析 · 用户逻辑 只调用驱动接口层,不直接操作硬件 驱动接口层(Driver Interface Layer) 统一API · 设备句柄 · 回调注册 · 适配层 抽象硬件差异,提供稳定接口 硬件驱动层(Hardware Driver Layer) 寄存器操作 · 中断处理 · DMA配置 · 外设时序 直接与MCU和外设硬件交互 关键约束:依赖方向从上到下,禁止反向依赖。每层只与相邻层通信。 换MCU只改硬件驱动层,换外设只改驱动接口层,业务层几乎不动。

1.6 实战中的常见误区

理论讲完了,说说我在项目中踩过的坑。

  • 过度抽象:有人为了解耦,搞了五六层抽象。结果一个简单的GPIO翻转,要经过四层函数调用。没必要。三层就够了,多了反而增加维护成本。
  • 接口膨胀:驱动接口层暴露了太多函数。业务层本来只需要读温度,结果能看到写配置、校准、自检等一堆函数。接口要最小化,业务层用不到的,就别暴露。
  • 忽视错误处理:接口函数一定要有返回值。我见过太多驱动函数返回void,出错了业务层根本不知道。每个接口都要定义清晰的错误码。
  • 耦合在头文件里:业务层直接#include了硬件驱动层的头文件。这等于开了后门,分层形同虚设。业务层只能包含驱动接口层的头文件。

我的建议:在项目初期,花一天时间把接口定义好。哪怕后面要改,也比没有接口强。接口就是骨架,骨架正了,后面填肉才不歪。

1.7 本章小结

嵌入式软件架构不是一蹴而就的,它是在实践中不断演进的。但分层、模块化、解耦这三个原则,是任何项目都绕不开的基础。记住一句话:好的架构让代码像搭积木,坏的架构让代码像一团乱麻。