第4章 分层架构模式:经典三层架构(HAL+Service+App)在嵌入式中的应用
做嵌入式开发久了,你会发现一个规律:
代码写得好不好,跟硬件关系不大,跟人关系很大。
我见过太多项目,一开始跑得飞快,三个月后改一个功能要两天。为什么?因为代码全揉在一起了。GPIO操作、业务逻辑、协议解析,全在一个文件里。改一个LED闪烁频率,都能把WiFi搞断连。
嗯,这就是典型的「没有分层」的后果。
4.1 为什么需要分层?
先问一个问题:你写嵌入式代码时,有没有遇到过这种情况?
- 换了一个MCU型号,整个项目重写
- 改了一个外设驱动,三个模块跟着崩溃
- 测试一个功能,要搭一整套硬件环境
如果有,说明你的代码耦合太紧了。
分层架构的核心思想,说白了就是「各司其职」。每一层只关心自己的事,不越界。这样改一层,不影响其他层。
三层架构的核心理念:
- HAL层(硬件抽象层):只跟寄存器、外设打交道
- Service层(服务层):封装功能模块,提供接口
- App层(应用层):只关心业务逻辑,不碰硬件
我个人习惯把这三层比作「厨师、配菜员、服务员」。厨师只管炒菜(HAL),配菜员只管切菜备料(Service),服务员只管端菜给客人(App)。各干各的,互不干扰。
4.2 HAL层:跟硬件死磕的地方
HAL层,全称Hardware Abstraction Layer。说白了,就是把你跟硬件寄存器隔离开的那层代码。
我在项目中遇到过最典型的反面教材:有人在应用层直接操作GPIO寄存器。结果换了一个引脚,要改十几个地方。你说崩溃不崩溃?
HAL层应该包含什么?
- MCU外设驱动:GPIO、UART、SPI、I2C、Timer等
- 传感器驱动:温度、湿度、加速度等
- 通信接口:CAN、Ethernet、USB等
HAL层的设计原则:
- 只提供「原子操作」,不封装业务逻辑
- 接口要通用,不绑定具体硬件
- 返回值要明确,错误码要统一
来看一个例子。这是典型的HAL层GPIO接口:
/* hal_gpio.h */
#ifndef _HAL_GPIO_H_
#define _HAL_GPIO_H_
#include "hal_types.h"
/* GPIO方向定义 */
typedef enum {
HAL_GPIO_DIR_INPUT,
HAL_GPIO_DIR_OUTPUT
} hal_gpio_dir_t;
/* GPIO电平定义 */
typedef enum {
HAL_GPIO_LEVEL_LOW,
HAL_GPIO_LEVEL_HIGH
} hal_gpio_level_t;
/* 初始化GPIO引脚 */
hal_err_t hal_gpio_init(uint8_t pin, hal_gpio_dir_t dir);
/* 设置GPIO输出电平 */
hal_err_t hal_gpio_set_level(uint8_t pin, hal_gpio_level_t level);
/* 读取GPIO输入电平 */
hal_err_t hal_gpio_get_level(uint8_t pin, hal_gpio_level_t *level);
#endif /* _HAL_GPIO_H_ */
注意看,这个接口里没有任何业务相关的名字。没有「LED_ON」、「BUTTON_PRESSED」这种东西。它只关心引脚号和电平状态。
我的经验:HAL层的接口命名,尽量用「hal_」前缀。这样在代码里一眼就能看出哪些是硬件相关操作。我曾经在一个项目里看到有人把HAL函数和业务函数混在一起命名,结果重构时花了三天才理清楚。
4.3 Service层:承上启下的关键
Service层,也叫中间层。它负责把HAL层的原子操作组合成有意义的服务。
你想想看,HAL层只提供了「设置GPIO电平」这种操作。但你的业务需要的是「点亮LED」、「读取按键状态」。这些就是Service层要做的事。
Service层应该包含什么?
- 设备驱动封装:LED、按键、蜂鸣器、显示屏等
- 协议处理:Modbus、MQTT、HTTP等
- 数据处理:滤波、校验、格式转换等
- 状态管理:设备状态机、任务调度等
举个例子,基于上面的HAL层,我们可以封装一个LED服务:
/* svc_led.h */
#ifndef _SVC_LED_H_
#define _SVC_LED_H_
#include "hal_gpio.h"
/* LED状态定义 */
typedef enum {
SVC_LED_OFF,
SVC_LED_ON,
SVC_LED_BLINK_SLOW,
SVC_LED_BLINK_FAST
} svc_led_state_t;
/* 初始化LED(内部调用hal_gpio_init) */
svc_err_t svc_led_init(uint8_t pin);
/* 设置LED状态 */
svc_err_t svc_led_set_state(svc_led_state_t state);
/* 获取LED当前状态 */
svc_err_t svc_led_get_state(svc_led_state_t *state);
#endif /* _SVC_LED_H_ */
看到区别了吗?Service层的接口是面向「功能」的,不是面向「硬件」的。App层调用时,只需要说「我要让LED闪烁」,不用管具体是哪个GPIO引脚。
注意:Service层不要直接操作硬件寄存器。我曾经见过有人为了「性能优化」,在Service层直接读写GPIO寄存器。结果换MCU时,Service层代码全废了。记住:Service层只能调用HAL层的接口。
4.4 App层:只管业务逻辑
App层是最高层,也是最「干净」的一层。它只关心业务逻辑,不关心硬件细节。
说白了,App层就是「老板层」。老板只说要做什么,不关心具体怎么做。
App层应该包含什么?
- 业务逻辑:设备控制策略、告警逻辑、数据上报策略等
- 用户交互:按键处理、界面显示逻辑等
- 系统管理:初始化流程、错误处理、日志记录等
来看一个App层调用Service层的例子:
/* app_main.c */
#include "svc_led.h"
#include "svc_button.h"
#include "svc_timer.h"
/* 按键处理回调 */
static void on_button_pressed(void)
{
svc_led_state_t current_state;
/* 获取当前LED状态 */
svc_led_get_state(¤t_state);
/* 切换LED状态 */
if (current_state == SVC_LED_OFF) {
svc_led_set_state(SVC_LED_ON);
} else {
svc_led_set_state(SVC_LED_OFF);
}
}
/* 系统初始化 */
void app_init(void)
{
/* 初始化各服务模块 */
svc_led_init(LED_PIN);
svc_button_init(BUTTON_PIN);
svc_timer_init();
/* 注册按键回调 */
svc_button_register_callback(on_button_pressed);
}
/* 主循环 */
void app_main_loop(void)
{
while (1) {
svc_button_process();
svc_timer_process();
/* 其他业务逻辑 */
}
}
你看,App层完全不知道GPIO是什么,不知道寄存器怎么操作。它只调用Service层的接口,完成业务逻辑。
分层的好处:
- 可移植性:换MCU时,只改HAL层,Service和App层不动
- 可测试性:可以Mock掉HAL层,单独测试Service和App层
- 可维护性:改业务逻辑时,不用担心搞坏硬件驱动
- 可复用性:HAL层和Service层可以在不同项目中复用
4.5 避坑指南:我踩过的那些坑
讲完了理论,说说实战中容易踩的坑。这些都是我亲身经历过的。
坑一:层与层之间直接传递硬件参数
我曾经在一个项目里看到,Service层直接向App层暴露了GPIO引脚号。结果App层代码里到处都是「#define LED_PIN 13」这种宏定义。后来换板子,引脚变了,改得想哭。
正确做法:Service层把硬件细节全部封装掉,App层只跟逻辑状态打交道。
坑二:HAL层做了太多事
有人觉得HAL层「反正都是操作硬件」,就把协议解析、数据缓存也塞进去了。结果HAL层变得又大又乱,换个外设驱动都要小心翼翼。
正确做法:HAL层只做「原子操作」,复杂逻辑交给Service层。
坑三:App层直接调用HAL层
这个最要命。我见过一个项目,App层直接调用了HAL_UART_SendByte()。后来UART驱动升级,接口变了,App层代码全崩。
正确做法:App层只能调用Service层,不能越级调用HAL层。
我的建议:在代码规范里明确规定调用关系。App→Service→HAL,只能单向调用,不能反向。可以用代码审查来保证这一点。我团队里就有一条硬性规定:App层代码里出现「hal_」开头的函数调用,直接打回重写。
4.6 实际项目中的分层策略
理论讲完了,说说实际项目中怎么落地。
小项目(单MCU,功能简单):
- 可以简化分层,但至少要有HAL和App两层
- Service层可以合并到App层中
- 但HAL层必须独立,方便后续移植
中等项目(多外设,中等复杂度):
- 严格三层架构,每层独立文件夹
- HAL层按外设分类:hal_gpio.c、hal_uart.c、hal_spi.c
- Service层按功能分类:svc_led.c、svc_sensor.c、svc_comm.c
- App层按业务模块分类:app_main.c、app_alarm.c、app_report.c
大型项目(多MCU,复杂系统):
- 在三层基础上,可以增加中间件层(Middleware)
- 中间件层处理:RTOS封装、文件系统、网络协议栈等
- 每层内部可以再细分,但层间接口要保持稳定
我个人习惯在项目初期就搭好分层框架。哪怕功能还没实现,先把目录结构建好,接口定义好。这样后续开发时,每个人都知道自己的代码该放哪里。
4.7 总结
三层架构不是什么高深的技术,但它能解决很多实际问题。
说白了,就是让代码各归其位。硬件的事归硬件,业务的事归业务。别混在一起。
我做了十几年嵌入式,见过太多项目因为不分层而烂尾。也见过很多项目因为分层清晰,换MCU就像换衣服一样简单。
嗯,如果你现在正在做一个新项目,不妨试试三层架构。一开始可能会觉得多写了一些接口代码,但三个月后你会感谢自己的。
记住三个原则:
- HAL层只操作硬件,不包含业务逻辑
- Service层封装功能,不暴露硬件细节
- App层只关心业务,不直接操作硬件
做到这三点,你的代码就能经得起时间考验。