3、分层架构模式:经典的三层架构(HAL、OSAL、APP)及其变体

分层架构,说白了就是把代码像切蛋糕一样,按职责切成几层。每一层只管自己的事,上下层之间通过定义好的接口通信。这样做的好处很明显——哪一层出了问题,就只修哪一层,不会牵一发动全身。

我在嵌入式这行干了十几年,见过太多「一锅粥」式的代码。一个 main.c 文件几千行,中断服务程序里直接操作寄存器,业务逻辑和硬件驱动搅在一起。这种代码,三个月后连原作者自己都看不懂。嗯,分层架构就是用来治这个病的。

3.1 为什么需要分层?

先看一个反面教材。我早年接手过一个智能家居项目,前任工程师把所有代码都写在了一个文件里。灯控、传感器读取、Wi-Fi 通信、定时任务,全混在一起。后来要换一颗主控芯片,从 STM32F103 换到 GD32F303,结果发现硬件相关的代码散落在各个角落,改了一个月还没改完。

分层架构的核心价值就三点:

  • 隔离变化:硬件换了,只改 HAL 层;操作系统换了,只改 OSAL 层;业务逻辑变了,只改 APP 层。
  • 提高复用:写好的 HAL 驱动,换个项目只要改一下底层寄存器映射,上层代码几乎不用动。
  • 方便测试:每一层都可以单独测试。APP 层的逻辑可以用桩函数模拟底层,不用等硬件就绪。

核心原则:上层依赖下层,但下层绝不能依赖上层。依赖关系是单向的,从 APP → OSAL → HAL,不能反过来。

3.2 经典三层架构详解

最经典的分层模式,就是 HAL(硬件抽象层)、OSAL(操作系统抽象层)、APP(应用层)这三层。我习惯把它叫做「三明治架构」——上下两片面包夹着中间的火腿。

3.2.1 HAL(硬件抽象层)

HAL 是最底层,直接跟寄存器打交道。它的职责是:把芯片厂商提供的寄存器操作,封装成一组标准化的 API。

举个例子,你要操作一个 GPIO 引脚输出高电平。芯片手册上可能写的是:

// 直接寄存器操作
GPIOA->BSRR = (1 << 5);   // 设置 PA5 为高电平

但 HAL 层应该把它封装成:

// HAL 层提供的 API
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);

这样上层代码就不用关心寄存器地址、位操作这些细节了。换芯片时,只要重新实现这个函数,上层调用完全不用改。

我的习惯:HAL 层的头文件里,只暴露函数声明和必要的枚举/结构体,绝不暴露寄存器地址和宏定义。这样上层开发者根本看不到底层细节,想乱来都没机会。

3.2.2 OSAL(操作系统抽象层)

OSAL 这一层,很多人会忽略。但我觉得它恰恰是嵌入式软件架构里最容易被低估的一层。

OSAL 的职责是:把操作系统相关的功能(任务创建、信号量、消息队列、定时器等)封装成统一的接口。这样你的应用代码就可以在 FreeRTOS、RT-Thread、uC/OS 甚至裸机之间无缝切换。

我曾经在一个项目中,客户要求从 FreeRTOS 迁移到 RT-Thread。因为前期做了 OSAL 封装,迁移只花了三天。而隔壁团队没做 OSAL,硬生生改了两周,还引入了一堆 bug。

典型的 OSAL 接口长这样:

// OSAL 层定义的任务创建接口
typedef void* OSAL_TaskHandle;
OSAL_TaskHandle OSAL_TaskCreate(
    void (*task_func)(void*),
    const char* name,
    uint32_t stack_size,
    void* param,
    uint32_t priority
);

// 信号量接口
typedef void* OSAL_SemaphoreHandle;
OSAL_SemaphoreHandle OSAL_SemCreate(uint32_t initial_count);
void OSAL_SemGive(OSAL_SemaphoreHandle sem);
void OSAL_SemTake(OSAL_SemaphoreHandle sem, uint32_t timeout_ms);

避坑指南:OSAL 层不要试图封装所有 OS 特性。只封装你项目中真正用到的功能。我曾经见过一个 OSAL 层,封装了 50 多个接口,结果项目里只用了 10 个,剩下的全是死代码。保持精简,够用就好。

3.2.3 APP(应用层)

APP 层是业务逻辑所在的地方。它只调用 OSAL 和 HAL 提供的接口,不直接操作硬件,也不直接调用 RTOS 的 API。

APP 层的代码应该像这样:

// APP 层:温度采集任务
void TempSensor_Task(void* param)
{
    uint16_t adc_value;
    float temperature;
    
    while(1)
    {
        // 通过 HAL 层读取 ADC 值
        adc_value = HAL_ADC_Read(ADC_CHANNEL_TEMP);
        
        // 计算温度
        temperature = (adc_value * 3.3f / 4096.0f - 0.5f) / 0.01f;
        
        // 通过 OSAL 层发送到消息队列
        OSAL_QueueSend(g_temp_queue, &temperature, 100);
        
        // 通过 OSAL 层延时
        OSAL_TaskDelay(1000);
    }
}

你看,APP 层的代码里,没有一句寄存器操作,也没有一句 RTOS 特有的 API 调用。它只认 HAL_ 和 OSAL_ 开头的函数。这就是分层的魅力。

3.3 三层架构的变体

实际项目中,三层架构往往需要根据场景做一些调整。我总结了几种常见的变体:

变体名称 适用场景 核心特点
四层架构 大型复杂系统 在 HAL 和 OSAL 之间增加「驱动层」,专门管理外设驱动
双层架构 资源极度受限的 MCU 合并 OSAL 和 HAL,只保留硬件抽象和应用层
五层架构 需要网络通信的系统 增加「协议栈层」和「中间件层」
分层+模块化 产品线丰富的团队 每层内部再按功能拆成独立模块

举个例子,我做过一个物联网网关项目,用了五层架构:HAL → 驱动层 → OSAL → 协议栈层 → APP。驱动层管理 Wi-Fi 模块、4G 模块、传感器等外设;协议栈层封装 MQTT、HTTP、CoAP 等通信协议。这样每层职责清晰,团队分工也明确。

3.4 分层架构的接口设计要点

分层架构好不好用,关键看接口设计。我踩过不少坑,总结了几条经验:

  1. 接口要稳定:一旦发布,尽量不改。改接口意味着所有上层代码都要跟着改。
  2. 参数要简单:能用基本类型就别用结构体,能用结构体就别用回调。参数越复杂,耦合越深。
  3. 错误处理要统一:所有接口返回统一的错误码,比如 OSAL_OK、OSAL_ERROR_TIMEOUT、HAL_ERROR_BUSY 等。
  4. 不要跨层调用:APP 层不能直接调用 HAL 层的函数,必须经过 OSAL 层。这是铁律。

我的经验:接口设计遵循「最小知识原则」——一个函数只做一件事,参数只传必要的信息。别想着一个函数解决所有问题,那只会让接口变得臃肿难用。

3.5 分层架构的 SVG 结构图

下面这张图展示了经典三层架构及其变体的关系。你可以看到,每一层都只与相邻层通信,依赖方向是单向的。

嵌入式软件分层架构图 嵌入式软件分层架构(经典三层及其变体) 经典三层架构 APP 应用层 OSAL 操作系统抽象层 HAL 硬件抽象层 五层架构(物联网网关) APP 应用层 协议栈层(MQTT/HTTP) OSAL 操作系统抽象层 驱动层(外设管理) HAL 硬件抽象层 核心原则 依赖方向单向:APP → OSAL → HAL(或 APP → 协议栈 → OSAL → 驱动 → HAL) 下层绝不能依赖上层,每层只暴露接口,隐藏实现细节

3.6 什么时候该用分层?什么时候不该用?

分层架构不是银弹。我见过有人在一个只有 8KB Flash 的 MCU 上硬套三层架构,结果代码体积膨胀了 30%,根本塞不下。

我的建议是:

  • 资源充足(Flash > 64KB,RAM > 8KB):大胆用分层架构,收益远大于成本。
  • 资源中等(Flash 16-64KB):用精简版分层,比如只做 HAL 和 APP 两层,OSAL 可以内联到 APP 中。
  • 资源极度受限(Flash < 16KB):放弃分层,用模块化替代。把功能拆成独立文件,但允许跨模块直接调用。

我的经验:分层架构的「度」很重要。层数不是越多越好,够用就行。我一般遵循「三层起步,按需增加」的原则。项目初期先搭好三层骨架,后续发现某层太臃肿了,再考虑拆分。