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

分层架构,说白了就是把代码按职责切几层。我刚开始做嵌入式那会儿,代码全写在一个文件里,一个函数管LED闪烁,另一个函数管I2C读写,中间还夹着延时逻辑。后来项目一复杂,改个定时器参数能引发三个模块的bug。嗯,从那以后我就老老实实分层了。

今天咱们聊的经典三层架构,就是嵌入式领域最常用的那套:硬件抽象层(HAL)、操作系统抽象层(OSAL)、应用层(App)。这三层怎么分?为什么分完就好测了?咱们一个个看。

1. 三层架构长什么样

先画个图,把结构看清楚。

嵌入式经典三层架构 应用层(App) 业务逻辑 · 状态机 · 协议处理 不直接操作硬件,不依赖具体OS 操作系统抽象层(OSAL) 任务管理 · 信号量 · 队列 · 时间服务 屏蔽RTOS差异,提供统一API 硬件抽象层(HAL) GPIO · UART · SPI · I2C · ADC · Timer 封装寄存器操作,提供硬件无关接口 依赖方向:App → OSAL → HAL,上层只能调用下层,禁止反向依赖

这个图我建议你存下来。每次写代码前看一眼,问问自己:这行代码该放哪一层?

2. 硬件抽象层(HAL)——把硬件藏起来

HAL层干的事很简单:把芯片寄存器操作封装成函数接口。比如你要点亮一个LED,HAL层给你一个HAL_GPIO_WritePin(),你传个引脚号和电平值就行,不用管那个引脚对应的ODR寄存器地址是多少。

核心原则:HAL层只做一件事——把硬件差异挡在下面。上层代码永远不直接操作寄存器。

举个例子,STM32和GD32的GPIO寄存器布局不一样,但如果你在HAL层统一了接口,上层代码一个字都不用改。我在项目中遇到过两次芯片换型,一次从STM32F103换到GD32F103,另一次从NXP LPC换到STM32L4。第一次没做HAL,改代码改到怀疑人生;第二次提前搭好了HAL,三天搞定移植。

HAL层的典型接口长这样:

// hal_gpio.h
typedef enum {
    HAL_GPIO_MODE_INPUT,
    HAL_GPIO_MODE_OUTPUT,
    HAL_GPIO_MODE_ALT
} HAL_GPIO_Mode_t;

void HAL_GPIO_Init(uint8_t pin, HAL_GPIO_Mode_t mode);
void HAL_GPIO_WritePin(uint8_t pin, uint8_t level);
uint8_t HAL_GPIO_ReadPin(uint8_t pin);

你看,接口里没有寄存器地址,没有位操作,没有volatile关键字。上层代码看到的就是干净的、可测试的函数。

3. 操作系统抽象层(OSAL)——把RTOS藏起来

OSAL层解决的是另一个痛点:不同RTOS的API千差万别。FreeRTOS的任务创建函数是xTaskCreate(),RT-Thread是rt_thread_create(),uC/OS是OSTaskCreate()。如果你在应用层直接调用这些函数,换个RTOS就得重写所有任务代码。

OSAL层提供一套统一接口:

// osal_task.h
typedef void* OSAL_TaskHandle_t;

OSAL_TaskHandle_t OSAL_TaskCreate(
    void (*func)(void*),
    const char* name,
    uint32_t stack_size,
    void* param,
    uint32_t priority
);

void OSAL_TaskDelay(uint32_t ms);
void OSAL_TaskDelete(OSAL_TaskHandle_t task);

我个人习惯把OSAL层做得尽量薄。它不负责业务逻辑,只做一件事:把RTOS的API翻译成统一的接口。翻译工作用宏或者内联函数完成,零开销最好。

一个小技巧:OSAL层的头文件里不要包含任何RTOS的头文件。应用层只包含OSAL的头文件,这样换RTOS时只需要换OSAL层的实现文件,应用层连重新编译都不需要。

4. 应用层(App)——只关心业务

应用层是代码量最大的一层,也是最容易出bug的一层。但好消息是,有了HAL和OSAL垫底,应用层的代码可以写得非常干净。

应用层里应该只有:

  • 业务逻辑:比如温度超过阈值就报警
  • 状态机:比如设备初始化→待机→运行→故障
  • 协议处理:比如解析Modbus报文、组包发出去
  • 任务调度:比如每100ms采集一次传感器数据

应用层绝对不应该出现:

  • 直接操作GPIO寄存器
  • 调用FreeRTOS的xQueueSend()(应该用OSAL封装后的接口)
  • 写死某个引脚的编号(应该用宏定义或配置表)

你想想看,如果应用层代码里到处都是GPIOA->ODR |= (1<<5),那这个代码基本没法测——你得接上真实硬件才能验证它对不对。

5. 分层如何天然促进可测试性?

这是今天的核心问题。我直接说结论:分层架构把“测什么”和“怎么测”分开了。

咱们用一张表对比一下:

测试对象 没分层时 分层后
应用层逻辑 必须跑在真实硬件上,依赖外设和RTOS 可以编译成PC程序,用桩函数替换HAL/OSAL,纯软件测试
HAL层驱动 与应用层代码混在一起,改驱动可能影响业务 独立测试,用回环测试或硬件在环测试验证
OSAL层封装 应用层直接调用RTOS API,换RTOS要重测所有任务 只测OSAL层接口,应用层代码不受影响
集成测试 所有代码一起编译,出问题不知道是哪层的锅 逐层集成,先测HAL,再测OSAL,最后测App,定位快

说白了,分层架构让依赖关系变得单向且清晰。上层依赖下层,下层不依赖上层。这意味着你可以把下层替换成测试桩,单独测上层逻辑。

注意:分层不是万能的。如果层与层之间的接口设计得又大又乱,测试照样痛苦。接口要小、要稳定、要语义清晰。我见过一个HAL层,一个函数传8个参数,其中3个是枚举、2个是结构体指针、还有1个回调函数——这种接口测起来比不分层还累。

6. 一个实战案例:温度采集模块

咱们看个具体例子。假设你要做一个温度采集模块,每100ms读一次温度传感器,超过阈值就报警。

没分层时,代码可能长这样:

void TemperatureTask(void* param) {
    while(1) {
        // 直接操作I2C寄存器读传感器
        I2C1->CR1 |= (1<<0);
        while(!(I2C1->SR1 & (1<<0)));
        uint16_t raw = I2C1->DR;
        float temp = raw * 0.0625f;
        
        // 直接调用FreeRTOS API发消息
        if(temp > 50.0f) {
            xQueueSend(alarm_queue, &temp, 0);
        }
        
        vTaskDelay(100);
    }
}

这段代码怎么测?你得接上真实的I2C温度传感器,还得让FreeRTOS跑起来。想模拟一个高温场景?你得拿热风枪吹传感器。想测边界条件?你得把传感器放到冰水里。嗯,想想就头疼。

分层后,代码变成这样:

void TemperatureTask(void* param) {
    while(1) {
        float temp = HAL_I2C_ReadTemperature(SENSOR_ADDR);
        
        if(temp > TEMP_THRESHOLD_HIGH) {
            OSAL_QueueSend(alarm_queue, &temp, 0);
        }
        
        OSAL_TaskDelay(100);
    }
}

现在测试就简单了:

  • 写一个HAL_I2C_ReadTemperature()的桩函数,让它返回你想要的温度值
  • 写一个OSAL_QueueSend()的桩函数,检查它是否被正确调用
  • 然后你就可以在PC上跑这个任务,测试所有温度阈值逻辑

关键点:分层之后,应用层的测试不再需要硬件。你可以在PC上编译、运行、调试,用单元测试框架(比如Unity、Ceedling)跑几百个测试用例,几分钟搞定。这在传统嵌入式开发里想都不敢想。

7. 避坑指南

分层架构虽然好,但用不好也会翻车。我总结几个常见的坑:

  • 层间耦合过紧:HAL层返回结构体指针,应用层直接修改结构体成员。这等于没分层。接口应该只传基本类型或简单结构体。
  • 层数太多:有人喜欢分五层、六层,结果每层都在做转发,性能损耗大,测试也麻烦。三层够用就别加第四层。
  • 接口不稳定:今天HAL_GPIO_WritePin有三个参数,明天改成四个,后天又改成两个。应用层跟着改来改去,测试用例也得重写。接口设计要一次想好。
  • 忽略OSAL层:有人觉得“我就用FreeRTOS,不用抽象”。结果项目后期要换RTOS,或者要在Windows上模拟测试,才发现所有任务代码都绑死在FreeRTOS上,改不动。

我曾经在一个项目里,因为没做OSAL层,导致无法在PC上做集成测试。最后只能把整个固件烧到板子上,用串口打日志来调试。一个bug定位三天,其中两天在等编译和下载。从那以后,我再也不敢省OSAL层了。

8. 小结

经典三层架构不是新东西,但很多人用错了。记住三个要点:

  1. HAL层藏硬件——让应用层不依赖具体芯片
  2. OSAL层藏RTOS——让应用层不依赖具体操作系统
  3. App层只写业务——让测试可以脱离硬件运行