第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(&current_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就像换衣服一样简单。

嗯,如果你现在正在做一个新项目,不妨试试三层架构。一开始可能会觉得多写了一些接口代码,但三个月后你会感谢自己的。

记住三个原则:

  1. HAL层只操作硬件,不包含业务逻辑
  2. Service层封装功能,不暴露硬件细节
  3. App层只关心业务,不直接操作硬件

做到这三点,你的代码就能经得起时间考验。