分层架构概览:经典三层架构(HAL、OSAL、APP)的职责与边界

做单片机开发久了,你会发现一个规律:代码不怕多,就怕乱。我刚入行那会儿,写个流水灯恨不得把所有逻辑都塞进 main 函数。后来项目大了,一个按键响应要翻三页代码……嗯,那滋味真不好受。

分层架构,说白了就是给代码划地盘。每一层只干自己的事,层与层之间通过接口说话。今天咱们就聊聊最经典的三层架构:HAL、OSAL 和 APP。

核心思想:上层依赖下层,下层不依赖上层。每一层只对紧挨着的下层提要求,不跨层调用。

为什么是三层?不是两层或四层?

你可能会问:两层不够吗?应用直接调硬件,多直接。我刚开始也这么想,直到有一次换了 MCU 型号……好家伙,所有驱动代码重写,应用层也跟着改,那叫一个酸爽。

三层架构的妙处在于:

  • 硬件抽象层(HAL):把芯片外设的差异藏起来
  • 操作系统抽象层(OSAL):把 RTOS 的差异藏起来
  • 应用层(APP):只关心业务逻辑,不关心跑在什么芯片上

这样分层后,换芯片、换 RTOS,应用层基本不用动。我有个项目从 STM32F1 迁移到 GD32,只改了 HAL 层,APP 层一行代码没动。

HAL:硬件抽象层

HAL 层是离硬件最近的一层。它的职责就一个:把芯片外设的操作封装成统一的接口。

举个例子,同样是 GPIO 输出高电平:

// STM32 HAL 风格
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);

// 我们自己封装的 HAL 接口
void hal_gpio_set(uint8_t port, uint8_t pin) {
    // 内部调用芯片厂商的驱动
    HAL_GPIO_WritePin((GPIO_TypeDef*)(port_map[port]), (1 << pin), GPIO_PIN_SET);
}

我个人习惯在 HAL 层做三件事:

  1. 外设初始化:时钟、GPIO、定时器、UART 等
  2. 基本读写操作:读传感器、写显示、发数据
  3. 中断处理:把中断服务函数里最底层的活干了

我的经验:HAL 层的接口要尽量"原子化"。比如读温度传感器,就只返回原始温度值,不做滤波、不做单位转换。那些是上层的事。

OSAL:操作系统抽象层

OSAL 层是很多人容易忽略的一层。它的存在是为了隔离 RTOS 的差异。

你想想看,FreeRTOS、uC/OS、RT-Thread 的任务创建、信号量、消息队列,API 都不一样。如果应用层直接调 FreeRTOS 的 API,哪天想换 RT-Thread,应用层得改多少?

// FreeRTOS 创建任务
xTaskCreate(task_func, "task", 256, NULL, 1, NULL);

// 我们封装的 OSAL 接口
osal_task_t* osal_task_create(const char* name, 
                               void (*func)(void*),
                               uint32_t stack_size,
                               uint8_t priority) {
    // 内部根据实际 RTOS 调用
    #ifdef OS_FREERTOS
        xTaskCreate(func, name, stack_size, NULL, priority, NULL);
    #elif OS_RTTHREAD
        rt_thread_create(name, func, NULL, stack_size, priority, 10);
    #endif
}

OSAL 层通常封装这些功能:

功能 说明 典型接口
任务管理 创建、删除、挂起、恢复任务 osal_task_create, osal_task_suspend
同步机制 信号量、互斥锁、事件组 osal_sem_create, osal_mutex_lock
通信机制 消息队列、邮箱 osal_mq_create, osal_mq_send
时间管理 延时、获取系统 tick osal_delay_ms, osal_get_tick

我曾经踩过的坑:OSAL 层不要封装得太厚。只封装那些应用层真正用到的功能,别想着把 RTOS 所有 API 都包一层。否则 OSAL 层本身就成了一个巨大的维护负担。

APP:应用层

APP 层是业务逻辑的所在地。它只关心"做什么",不关心"怎么做"。

比如一个智能温控器:

  • APP 层:温度到了 30°C 就开风扇,低于 25°C 就关风扇
  • HAL 层:怎么读温度传感器、怎么控制风扇 GPIO
  • OSAL 层:温度采集任务和风扇控制任务怎么调度

APP 层的代码应该长这样:

void app_temp_control_task(void* param) {
    while(1) {
        int16_t temp = hal_temp_sensor_read();
        
        if (temp >= 300) {  // 30.0°C
            hal_fan_set(ON);
        } else if (temp <= 250) {  // 25.0°C
            hal_fan_set(OFF);
        }
        
        osal_delay_ms(1000);  // 每秒检测一次
    }
}

你看,APP 层不知道温度传感器是 DS18B20 还是 NTC,不知道风扇是 GPIO 控制还是 PWM 控制,也不知道这个任务跑在 FreeRTOS 还是裸机。它只关心业务逻辑。

三层之间的边界

边界不清是分层架构最大的敌人。我见过有人把 printf 写在 HAL 层,也有人把 GPIO 操作写在 APP 层。这些都是越界行为。

记住三条铁律:

  1. APP 只能调 OSAL 和 HAL,不能直接操作寄存器
  2. HAL 不能调 APP,也不能调 OSAL(中断回调除外)
  3. OSAL 不能调 HAL 和 APP,它只封装 RTOS 功能

边界检查清单:写代码前问自己三个问题——这行代码属于哪一层?它调用了不该调的函数吗?换芯片/换 RTOS 时,这层需要改吗?

三层架构的 SVG 结构图

下面这张图展示了三层架构的调用关系和边界。我建议你把它打印出来贴在工位上。

经典三层架构:HAL · OSAL · APP APP 应用层 业务逻辑 · 任务调度 · 状态机 · 用户交互 OSAL 操作系统抽象层 任务管理 · 信号量 · 消息队列 · 时间管理 HAL 硬件抽象层 GPIO · UART · SPI · I2C · ADC · 定时器 ⚠ 上层只能调用下层接口,禁止跨层调用

实际项目中的分层策略

三层架构不是银弹。小项目(比如一个 LED 闪烁)硬套三层架构,反而显得臃肿。我一般这样判断:

  • 裸机小项目(< 5K 代码):只用 HAL + APP 两层,OSAL 层省略
  • 带 RTOS 的中等项目(5K-50K 代码):三层全上,OSAL 层薄薄一层就行
  • 大型项目(> 50K 代码):三层基础上,每层内部再细分模块

我的建议:第一次尝试分层架构时,从"HAL + APP"两层开始。等你对分层有感觉了,再引入 OSAL 层。一口吃不成胖子,分层也是。

避坑指南

最后分享几个我亲身踩过的坑:

  • HAL 层太厚:把算法、滤波、协议解析都塞进 HAL 层。结果换芯片时,这些跟硬件无关的代码也得重写。
  • OSAL 层形同虚设:应用层直接调 FreeRTOS 的 API,OSAL 层只是个空壳。后来换 RTOS,改到想哭。
  • APP 层太薄:业务逻辑散落在各个中断和回调里,想找个功能入口都难。