分层架构概览:经典三层架构(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 层做三件事:
- 外设初始化:时钟、GPIO、定时器、UART 等
- 基本读写操作:读传感器、写显示、发数据
- 中断处理:把中断服务函数里最底层的活干了
我的经验: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 层。这些都是越界行为。
记住三条铁律:
- APP 只能调 OSAL 和 HAL,不能直接操作寄存器
- HAL 不能调 APP,也不能调 OSAL(中断回调除外)
- OSAL 不能调 HAL 和 APP,它只封装 RTOS 功能
边界检查清单:写代码前问自己三个问题——这行代码属于哪一层?它调用了不该调的函数吗?换芯片/换 RTOS 时,这层需要改吗?
三层架构的 SVG 结构图
下面这张图展示了三层架构的调用关系和边界。我建议你把它打印出来贴在工位上。
实际项目中的分层策略
三层架构不是银弹。小项目(比如一个 LED 闪烁)硬套三层架构,反而显得臃肿。我一般这样判断:
- 裸机小项目(< 5K 代码):只用 HAL + APP 两层,OSAL 层省略
- 带 RTOS 的中等项目(5K-50K 代码):三层全上,OSAL 层薄薄一层就行
- 大型项目(> 50K 代码):三层基础上,每层内部再细分模块
我的建议:第一次尝试分层架构时,从"HAL + APP"两层开始。等你对分层有感觉了,再引入 OSAL 层。一口吃不成胖子,分层也是。
避坑指南
最后分享几个我亲身踩过的坑:
- HAL 层太厚:把算法、滤波、协议解析都塞进 HAL 层。结果换芯片时,这些跟硬件无关的代码也得重写。
- OSAL 层形同虚设:应用层直接调 FreeRTOS 的 API,OSAL 层只是个空壳。后来换 RTOS,改到想哭。
- APP 层太薄:业务逻辑散落在各个中断和回调里,想找个功能入口都难。