3. 分层架构基础:表示层、业务层、数据层、硬件抽象层的职责划分
分层架构,说白了就是把代码按职责切分成几个独立的「房间」。每个房间只管自己的事,别越界。我刚开始做嵌入式时,也喜欢把所有逻辑塞进一个 main.c,结果后来改一个传感器驱动,把整个业务逻辑都带崩了。嗯,从那以后,我再也不敢写「大泥球」代码了。
今天我们就来聊聊嵌入式分层架构中最经典的四个层次:表示层、业务层、数据层和硬件抽象层。你想想看,这四层就像一栋楼的四个功能区,各司其职,互不干扰。
3.1 为什么需要分层?
先问一个问题:如果你的代码里,按键检测、温度计算、LCD 刷新、I2C 读写全混在一起,当你要换一块 LCD 屏幕时,会发生什么?
答案是:你很可能把温度计算的逻辑也改坏了。这就是耦合的代价。
我个人习惯把分层的好处总结为三点:
- 可移植性:换硬件时,只改底层,不动业务。
- 可测试性:业务逻辑可以脱离硬件单独测试。
- 可维护性:新人接手时,看一眼目录结构就知道代码在哪。
核心原则:每一层只依赖它的下一层,绝不跨层调用。表示层不能直接操作硬件寄存器,业务层不能直接读写 I2C 总线。
3.2 四层职责详解
3.2.1 硬件抽象层(HAL)
HAL 是离硬件最近的一层。它把 GPIO、UART、SPI、I2C、定时器等外设的操作封装成统一的接口。我在项目中遇到过,不同厂商的 MCU 对 GPIO 的配置方式完全不同——STM32 用 HAL_GPIO_WritePin,NXP 用 GPIO_PinWrite,瑞萨又是一种写法。如果没有 HAL,业务层就得为每款芯片写一套代码。
HAL 的典型接口长这样:
/* hal_gpio.h */
void HAL_GPIO_Init(GPIO_Pin_t pin, GPIO_Mode_t mode);
void HAL_GPIO_Set(GPIO_Pin_t pin, uint8_t level);
uint8_t HAL_GPIO_Get(GPIO_Pin_t pin);
注意,HAL 只做「怎么操作硬件」,不做「操作硬件是为了什么」。比如「按下按键后点亮 LED」这个逻辑,不属于 HAL。
3.2.2 数据层
数据层负责数据的存储、读取和缓存。它不关心数据是什么含义,只关心数据怎么存、怎么取。比如 EEPROM 读写、Flash 存储、环形缓冲区、参数管理,都属于数据层。
我曾经在一个项目中,把参数存储逻辑直接写在业务层里,结果后来要换存储介质(从 EEPROM 换到 Flash),业务层代码改得面目全非。后来我把所有存储操作抽象成数据层接口,换存储介质时只改数据层,业务层一行代码都没动。
数据层接口示例:
/* data_storage.h */
int32_t Data_Write(uint16_t addr, uint8_t *buf, uint16_t len);
int32_t Data_Read(uint16_t addr, uint8_t *buf, uint16_t len);
int32_t Data_Erase(uint16_t addr, uint16_t len);
3.2.3 业务层
业务层是整个系统的核心。它处理具体的业务逻辑,比如温度控制算法、PID 调节、状态机转换、协议解析等。业务层不关心数据是从哪个传感器来的,也不关心数据最终显示在哪个屏幕上。
我建议业务层尽量做到「纯逻辑」——不直接调用 HAL,不直接操作硬件。它只调用数据层和表示层的接口。这样,业务逻辑可以独立测试,甚至可以放到 PC 上跑单元测试。
举个例子,一个恒温控制器的业务层:
/* biz_temperature.c */
void Biz_Temperature_Control(void) {
float current = Data_ReadTemperature(); // 从数据层读温度
float target = Data_ReadTargetTemp(); // 从数据层读目标温度
float output = PID_Calculate(current, target); // PID 计算
Data_WriteHeaterPower(output); // 写入加热功率
}
你看,业务层完全不知道温度是从 NTC 还是 DS18B20 读来的,也不知道加热器是继电器还是 PWM 控制。这就是解耦。
3.2.4 表示层
表示层负责与用户交互。它管理 LCD 显示、按键输入、LED 指示、蜂鸣器、触摸屏等。表示层只做两件事:把业务层的数据「画」出来,把用户的输入「传」给业务层。
我个人习惯把表示层再拆成两个子层:
- UI 控件层:菜单、列表、进度条、弹窗等通用组件。
- 页面逻辑层:哪个页面显示什么数据,按键按下后跳转到哪个页面。
表示层绝对不能包含业务计算。比如「温度超过 50°C 时显示红色警告」,这个判断应该由业务层来做,表示层只负责根据业务层给出的状态来显示颜色。
3.3 四层依赖关系图
下面这张图展示了四层之间的依赖关系。箭头方向表示「依赖方向」——上层依赖下层,下层绝不依赖上层。
3.4 各层职责对比表
为了方便你快速对照,我把四层的职责、典型接口和禁止行为整理成了一张表:
| 层次 | 核心职责 | 典型接口 | 禁止行为 |
|---|---|---|---|
| 表示层 | 用户交互、信息展示 | UI_ShowMenu(), UI_GetKey() | 不能做业务计算、不能直接操作硬件 |
| 业务层 | 业务逻辑、状态管理 | Biz_ProcessEvent(), Biz_GetStatus() | 不能直接调用 HAL、不能直接读写外设 |
| 数据层 | 数据存储、缓存管理 | Data_Read(), Data_Write() | 不能包含业务判断、不能直接操作硬件 |
| HAL | 硬件抽象、外设封装 | HAL_GPIO_Set(), HAL_UART_Send() | 不能包含业务逻辑、不能调用上层接口 |
3.5 避坑指南
我曾经犯过的错:
- 在业务层里直接调用 HAL_GPIO_Set 来控制 LED,结果后来换 MCU,业务层代码全要重写。
- 在表示层里写温度转换公式,导致换了一个温度传感器后,UI 代码也要跟着改。
- 数据层里混入了「如果温度超过 50°C 就报警」的逻辑——这明明是业务层的事。
我的建议:
- 写代码前,先画一张分层依赖图,明确每一层的边界。
- 每一层都定义好头文件接口,其他层只能通过接口调用,不能直接访问内部变量。
- 如果发现某段代码「既像业务又像数据」,那就拆成两个函数,分别放到不同层。
3.6 一个简单的分层示例
假设我们要做一个智能温控器:按键设定目标温度,LCD 显示当前温度,加热器根据 PID 输出控制。
分层后的代码结构如下:
/* 表示层 */
void UI_ProcessKey(void) {
Key_t key = UI_GetKey();
if (key == KEY_UP) {
Biz_IncreaseTargetTemp(); // 调用业务层
}
}
void UI_Display(void) {
float temp = Biz_GetCurrentTemp(); // 从业务层获取数据
float target = Biz_GetTargetTemp();
LCD_ShowTemp(temp, target);
}
/* 业务层 */
void Biz_IncreaseTargetTemp(void) {
float target = Data_ReadTargetTemp();
target += 0.5f;
Data_WriteTargetTemp(target);
}
void Biz_ControlLoop(void) {
float current = Data_ReadCurrentTemp();
float target = Data_ReadTargetTemp();
float output = PID_Calculate(current, target);
Data_WriteHeaterPower(output);
}
/* 数据层 */
float Data_ReadCurrentTemp(void) {
uint8_t raw[2];
HAL_I2C_Read(0x48, raw, 2); // 调用 HAL
return (raw[0] << 8 | raw[1]) * 0.0625f;
}
void Data_WriteHeaterPower(float power) {
uint8_t duty = (uint8_t)(power / 100.0f * 255);
HAL_PWM_SetDuty(TIM2, CH1, duty); // 调用 HAL
}
/* HAL */
void HAL_I2C_Read(uint8_t addr, uint8_t *buf, uint16_t len) {
// 操作 I2C 寄存器
}
void HAL_PWM_SetDuty(TIM_TypeDef *tim, uint8_t ch, uint8_t duty) {
// 操作定时器寄存器
}
你看,每一层只做自己的事。换 MCU 时,只需要重写 HAL 层;换传感器时,只需要改数据层;改控制算法时,只动业务层。这就是分层架构的魅力。