3. 分层架构基础:硬件抽象层(HAL)的概念与作用、中间层(Middleware)的定位、应用层(Application)的职责
好,咱们今天聊聊分层架构。很多朋友刚开始写单片机程序,都是在一个 main 函数里把所有事情干完——读传感器、算数据、刷屏幕、处理按键。嗯,我当年也是这么过来的。代码写到两三千行的时候,就开始头疼了:改一个地方,好几个地方跟着崩。
为什么会这样?说白了,就是代码耦合太严重了。硬件换了、外设换了,整个程序就得重写。这时候,分层架构就派上用场了。
硬件抽象层(HAL):把硬件“藏”起来
硬件抽象层,英文叫 Hardware Abstraction Layer,简称 HAL。它的核心任务就一个:让上层代码不直接碰硬件寄存器。
你想想看,如果你在代码里到处写 GPIOA->ODR |= (1<<5),哪天换了 STM32F1 到 STM32F4,或者换成 GD32、AT32,你得把所有 GPIO 操作全改一遍。这谁受得了?
HAL 层就是帮你把这些底层操作封装成统一的接口。比如:
// HAL 层提供的接口
void hal_gpio_write(uint8_t pin, uint8_t level);
uint8_t hal_gpio_read(uint8_t pin);
void hal_uart_send_byte(uint8_t data);
uint8_t hal_uart_receive_byte(void);
我在项目中遇到过一件事:一个产品原本用 STM32F103,后来因为芯片涨价,被迫换成 GD32F103。幸好当时 HAL 层封装得比较干净,只改了底层几个文件,上层代码一行没动。你想想看,要是没这层,那得改到什么时候?
HAL 层的核心价值:
- 隔离硬件变化,换芯片时只改 HAL 层
- 提供统一接口,上层代码不用关心具体寄存器
- 方便单元测试,可以 mock 掉硬件行为
我个人习惯,HAL 层一般包含这些内容:
- GPIO 读写封装
- UART、SPI、I2C 等通信接口封装
- 定时器基本操作封装
- ADC、DAC 等模拟接口封装
- 中断回调函数的统一管理
注意:HAL 层不是越厚越好。我曾经见过有人把 HAL 层封装了七八层,结果一个简单的 GPIO 翻转要调用五六个函数。嗯,这就过头了。HAL 层应该保持薄而清晰,只做硬件操作的封装,不做业务逻辑。
中间层(Middleware):搭起桥梁
中间层,也叫 Middleware。它的定位很明确:在 HAL 层和应用层之间,提供通用的、可复用的功能模块。
举个例子。你写了一个 OLED 屏幕的驱动,它依赖 HAL 层的 I2C 接口。这个驱动本身不关心业务逻辑,只负责把数据发到屏幕上。这就是典型的中间层组件。
中间层通常包括:
- 各种外设驱动库(LCD、OLED、传感器等)
- 通信协议栈(Modbus、CANopen、MQTT 等)
- 文件系统(FATFS、LittleFS 等)
- RTOS 封装层
- 数据管理模块(环形缓冲区、FIFO 等)
// 中间层示例:OLED 驱动
// 它只调用 HAL 层的 I2C 接口,不关心具体硬件
void oled_init(void) {
hal_i2c_init(OLED_I2C_PORT, 400000);
// 发送初始化命令序列...
}
void oled_show_string(uint8_t x, uint8_t y, const char *str) {
// 计算字模,通过 HAL 层 I2C 发送
for (int i = 0; str[i] != '\0'; i++) {
oled_show_char(x + i*8, y, str[i]);
}
}
我个人觉得,中间层是分层架构里最容易被人忽略的一层。很多人直接把驱动代码和应用代码混在一起写,结果换个屏幕就得改应用逻辑。我曾经接手过一个项目,OLED 显示代码和业务逻辑全写在一个文件里,三千多行,看得我头皮发麻。
我的建议:中间层的代码应该做到“拿来即用”。你从项目 A 里拷过来的 OLED 驱动,放到项目 B 里,只需要改一下 HAL 层的接口配置就能跑。这才是中间层该有的样子。
应用层(Application):只管业务逻辑
应用层,就是你的业务代码所在的地方。它的职责很纯粹:实现产品的具体功能,不关心硬件怎么操作。
比如一个温控器产品,应用层要做的事情是:
- 读取温度值(通过中间层的传感器驱动)
- 判断是否超过阈值
- 控制继电器开关(通过中间层的 GPIO 控制接口)
- 更新屏幕显示(通过中间层的 OLED 驱动)
你看,应用层根本不需要知道温度传感器是 DS18B20 还是 NTC 热敏电阻,也不需要知道继电器是接在 PA0 还是 PB1 上。它只关心业务逻辑本身。
// 应用层代码示例
void app_temperature_control(void) {
float temp = sensor_read_temperature(); // 调用中间层接口
if (temp > TEMP_THRESHOLD_HIGH) {
relay_control(RELAY_HEATER, RELAY_OFF); // 调用中间层接口
oled_show_string(0, 0, "Heater OFF");
} else if (temp < TEMP_THRESHOLD_LOW) {
relay_control(RELAY_HEATER, RELAY_ON);
oled_show_string(0, 0, "Heater ON");
}
}
应用层的黄金法则:代码里不要出现任何寄存器操作、不要直接调用 HAL 层的函数。所有硬件操作都必须通过中间层转发。这样,当硬件变化时,应用层代码完全不需要修改。
三层之间的关系
这三层的关系,说白了就是一条单向依赖链:
应用层 → 中间层 → HAL 层 → 硬件
每一层只依赖它的下一层,不能反向依赖。HAL 层不知道中间层的存在,中间层不知道应用层的存在。这样,每一层都可以独立修改、独立测试。
我刚开始做分层设计时,总忍不住在应用层里直接调用 HAL 函数,觉得省事。后来吃了几次亏——换芯片时改代码改到崩溃——才老老实实按规矩来。嗯,有些坑,真的得自己踩过才知道疼。
一个小技巧:在代码里用文件命名来体现分层。比如 HAL 层的文件叫 hal_gpio.c、hal_uart.c;中间层的文件叫 mid_oled.c、mid_ds18b20.c;应用层的文件叫 app_temperature.c、app_display.c。这样一看文件名,就知道代码属于哪一层,维护起来方便很多。
好了,这一章的内容就到这里。记住:分层不是为了让代码变复杂,而是为了让代码在项目变大、硬件变化时,依然能保持清晰和可控。下一章,咱们聊聊具体怎么搭建一个分层架构的工程模板。