分层架构基础:经典三层架构

做嵌入式开发这么多年,我见过太多「一锅粥」式的代码。说白了,就是所有功能揉在一个文件里,GPIO 初始化、任务调度、业务逻辑全混在一起。这种代码,换一个芯片基本等于重写。

那怎么解决?分层。今天我们就聊聊嵌入式领域最经典的三层架构:HAL、OSAL、App。

为什么需要分层?

先问一个问题:你写的代码,换一块 MCU 还能跑吗?

如果不能,说明你的硬件依赖和业务逻辑绑得太死了。我在项目中遇到过好几次这种情况——客户说「换个更便宜的芯片」,结果底层驱动全要改,上层业务逻辑也跟着遭殃。那段时间加班加得我头皮发麻。

分层架构的核心目的就一个:隔离变化。硬件变了,只改 HAL;操作系统换了,只改 OSAL;业务需求变了,只改 App。互不干扰。

三层架构全景图

先看一张整体结构图,我习惯用这张图给团队新人做培训:

嵌入式三层架构全景图 嵌入式软件三层架构 App 层(应用层) 业务逻辑 · 状态机 · 协议解析 · 用户交互 依赖 OSAL 接口,不直接调用 HAL OSAL 层(操作系统抽象层) 任务管理 · 信号量 · 消息队列 · 时间管理 屏蔽 RTOS 差异(FreeRTOS / RT-Thread / uCOS) HAL 层(硬件抽象层) GPIO · UART · SPI · I2C · ADC · Timer 直接操作寄存器或调用 MCU SDK 依赖方向:App → OSAL → HAL(上层依赖下层接口,下层不依赖上层)

这张图我用了好多年。每次画完,我都会补一句:箭头方向就是依赖方向。App 依赖 OSAL,OSAL 依赖 HAL。反过来不行——HAL 里绝对不能出现 App 的调用。

HAL:硬件抽象层

HAL 是离硬件最近的一层。它的任务就是把芯片厂商的寄存器操作包装成统一的接口。

举个例子。同样是点亮一个 LED,STM32 用 HAL_GPIO_WritePin,GD32 用 gpio_bit_set,NXP 用 GPIO_PinWrite。如果没有 HAL,你的业务代码里就会到处是这些厂商特有的函数。

我个人的习惯是,HAL 接口只暴露最基本的功能:

/* hal_gpio.h - 硬件抽象层 GPIO 接口 */
#ifndef HAL_GPIO_H
#define HAL_GPIO_H

#include <stdint.h>

/* GPIO 方向枚举 */
typedef enum {
    HAL_GPIO_DIR_IN,
    HAL_GPIO_DIR_OUT
} hal_gpio_dir_t;

/* GPIO 电平枚举 */
typedef enum {
    HAL_GPIO_LOW  = 0,
    HAL_GPIO_HIGH = 1
} hal_gpio_level_t;

/* 初始化 GPIO */
void hal_gpio_init(uint8_t pin, hal_gpio_dir_t dir);

/* 设置 GPIO 输出电平 */
void hal_gpio_write(uint8_t pin, hal_gpio_level_t level);

/* 读取 GPIO 输入电平 */
hal_gpio_level_t hal_gpio_read(uint8_t pin);

#endif /* HAL_GPIO_H */
我的经验:HAL 接口不要做得太「智能」。比如自动配置上拉下拉、自动切换速度——这些应该留给上层去组合。HAL 只做原子操作,越简单越好。

OSAL:操作系统抽象层

OSAL 这一层,说白了就是让上层代码不依赖具体的 RTOS。

你想想看,FreeRTOS 的任务创建函数是 xTaskCreate,RT-Thread 是 rt_thread_create,uCOS 是 OSTaskCreate。如果 App 层直接调用这些,换 RTOS 就得改 App——这跟没分层有什么区别?

OSAL 要做的就是定义一套通用的接口:

/* osal_task.h - 操作系统抽象层任务管理 */
#ifndef OSAL_TASK_H
#define OSAL_TASK_H

#include <stdint.h>

/* 任务句柄类型(对具体 RTOS 句柄的封装) */
typedef void* osal_task_handle_t;

/* 任务入口函数类型 */
typedef void (*osal_task_entry_t)(void* param);

/* 创建任务 */
osal_task_handle_t osal_task_create(
    const char*       name,
    osal_task_entry_t entry,
    void*             param,
    uint32_t          stack_size,
    uint32_t          priority
);

/* 删除任务 */
void osal_task_delete(osal_task_handle_t task);

/* 任务延时(毫秒) */
void osal_task_delay(uint32_t ms);

#endif /* OSAL_TASK_H */
注意:OSAL 的接口设计要「取最大公约数」。不要试图把某个 RTOS 的独有特性暴露出来——比如 FreeRTOS 的 task notification。一旦暴露,换 RTOS 时那个接口就废了。

App:应用层

App 层是写业务逻辑的地方。它只调用 OSAL 和 HAL 的接口,不直接接触任何硬件寄存器或 RTOS API。

这样做的好处是什么?我举个例子。之前做一个数据采集项目,客户要求从 FreeRTOS 换成 RT-Thread。因为 App 层只用了 OSAL 的接口,我只需要重新实现 OSAL 层的几个函数,App 层一行代码没改。整个移植花了不到两天。

App 层的典型代码长这样:

/* app_sensor.c - 应用层传感器采集任务 */
#include "osal_task.h"
#include "hal_gpio.h"
#include "hal_uart.h"

/* 传感器数据采集任务 */
static void sensor_task_entry(void* param)
{
    (void)param;
    uint8_t buffer[64];
    
    while (1) {
        /* 触发传感器 */
        hal_gpio_write(TRIGGER_PIN, HAL_GPIO_HIGH);
        osal_task_delay(10);
        hal_gpio_write(TRIGGER_PIN, HAL_GPIO_LOW);
        
        /* 读取数据 */
        hal_uart_read(SENSOR_UART, buffer, sizeof(buffer));
        
        /* 处理数据... */
        
        /* 等待下一次采集 */
        osal_task_delay(1000);
    }
}

/* 应用初始化 */
void app_init(void)
{
    hal_gpio_init(TRIGGER_PIN, HAL_GPIO_OUT);
    hal_uart_init(SENSOR_UART, 115200);
    
    osal_task_create("sensor", sensor_task_entry, NULL, 1024, 5);
}

层间依赖关系与接口设计原则

三层架构的依赖关系,我用一个表格总结:

层 依赖谁 被谁依赖 关键原则
App OSAL、HAL 无 不直接调用 RTOS API 或寄存器
OSAL HAL(可选)、RTOS App 接口取最大公约数,不暴露 RTOS 特性
HAL MCU SDK / 寄存器 OSAL、App 只做原子操作,不做业务逻辑

接口设计上,我总结了三条铁律:

  1. 单向依赖:上层依赖下层,下层绝不反向依赖上层。这是分层架构的底线。
  2. 接口稳定:HAL 和 OSAL 的接口一旦确定,尽量不要改。改接口意味着所有上层代码都要跟着改。
  3. 最小暴露:只暴露必要的功能。比如 HAL 的 GPIO 接口,只给 init、write、read 就够了,不需要暴露寄存器地址。

避坑指南:我曾经在一个项目里,为了「方便」,在 HAL 层直接调用了 App 层的回调函数。结果后来 App 层重构,HAL 层也得跟着改。那叫一个痛苦。记住:下层永远不知道上层是谁。如果需要回调,用注册机制,不要硬编码。

一个完整的调用链路

最后,看一个实际例子。假设你要通过 UART 发送一条数据:

  1. App 层调用 osal_uart_write(data, len)
  2. OSAL 层获取 UART 互斥锁,然后调用 hal_uart_write(port, data, len)
  3. HAL 层操作 UART 数据寄存器,等待发送完成中断

整个过程,App 层不知道用的是哪个 MCU,也不知道有没有用 DMA。它只管发数据。这就是分层架构的魅力。