3. 分层架构设计:应用层、服务层、驱动层、硬件抽象层(HAL)的职责划分

做单片机软件,最怕什么?

我最怕的是代码写到最后成了一团乱麻。中断里调延时,驱动里写业务逻辑,应用层直接操作寄存器……这种项目,三个月后连自己都看不懂。

分层架构就是为了解决这个问题。说白了,就是把不同职责的代码放到不同的盒子里,让它们各司其职。今天我们就聊聊这四层——应用层、服务层、驱动层、硬件抽象层——到底该怎么分。

3.1 为什么需要分层?

先讲个我自己的经历。早年做一款智能家居网关,MCU 用的是 STM32F103。刚开始图省事,所有代码都揉在 main.c 里。按键检测、LCD 刷新、Wi-Fi 通信、传感器读取……全在一个 while 循环里。

结果呢?

  • 换了一个 LCD 屏,要改 17 个文件
  • Wi-Fi 模块从 ESP8266 换成 ESP32,几乎重写了整个通信逻辑
  • 新同事接手,看了三天代码,跟我说想辞职

这就是不分层的代价。你想想看,如果当时我把硬件操作和业务逻辑分开,换屏只需要改驱动层,换 Wi-Fi 模块只需要改 HAL 层,应用层根本不用动。

核心原则:每一层只关心自己的事,不越界。上层调用下层,下层不依赖上层。

3.2 四层架构总览

这张图是我做项目时常用的分层结构,你可以把它当作模板来用。

单片机软件四层架构 应用层(Application Layer) 业务逻辑 · 状态机 · 任务调度 · 用户交互 服务层(Service Layer) 协议解析 · 数据缓存 · 事件管理 · 超时处理 驱动层(Driver Layer) 外设初始化 · 中断处理 · DMA传输 · 寄存器操作 硬件抽象层(HAL) MCU寄存器封装 · 引脚映射 · 时钟配置 · 外设抽象接口 高层 底层 越上层 越底层

3.3 各层职责详解

3.3.1 硬件抽象层(HAL)

HAL 是最底层,直接跟 MCU 寄存器打交道。它的任务只有一个:把硬件细节藏起来。

举个例子。你用 STM32 的 GPIO 点灯,直接写寄存器是这样的:

// 直接操作寄存器
GPIOA->BSRR = (1 << 5);   // PA5 置高
GPIOA->BRR  = (1 << 5);   // PA5 置低

但如果你换了 MCU,比如换成 GD32,寄存器地址和位定义可能完全不同。怎么办?

HAL 层就是用来解决这个问题的。我一般会封装成这样:

// HAL 层接口
void HAL_GPIO_WritePin(GPIO_TypeDef* port, uint16_t pin, uint8_t state);
uint8_t HAL_GPIO_ReadPin(GPIO_TypeDef* port, uint16_t pin);

这样,上层代码永远只调用 HAL_GPIO_WritePin,不管底层是 STM32 还是 GD32,甚至换成 NXP 的芯片,只要重新实现 HAL 层,上层代码一行都不用改。

我的习惯:HAL 层只做三件事——寄存器封装、引脚映射、外设初始化。不要在这里写任何业务逻辑,哪怕是一个 if 判断都要慎重。

3.3.2 驱动层(Driver Layer)

驱动层在 HAL 之上,负责管理具体的外设。比如 I2C、SPI、UART、定时器、ADC 这些。

驱动层的核心是:提供完整的外设操作接口,并处理中断和 DMA。

拿 UART 驱动来说,我通常会提供这些接口:

// 驱动层接口
void DRV_UART_Init(uint32_t baudrate);
void DRV_UART_Send(uint8_t* data, uint16_t len);
uint8_t DRV_UART_Receive(uint8_t* data, uint16_t timeout);
void DRV_UART_IRQHandler(void);  // 中断处理函数

注意看,驱动层已经不再暴露寄存器了。上层调用者只需要知道「初始化、发送、接收」这三个操作,完全不用关心 UART 的波特率寄存器怎么配、中断标志位怎么清。

我曾经踩过的坑:在驱动层里直接处理协议。比如在 UART 驱动里解析 Modbus 报文。结果换了一个协议,驱动层要重写。记住,驱动层只负责「怎么收发」,不负责「收发的数据是什么意思」。

3.3.3 服务层(Service Layer)

服务层是承上启下的关键一层。它把驱动层的原始数据加工成应用层能直接用的信息。

举个例子。驱动层从传感器读回来的是原始字节:

// 驱动层返回
uint8_t raw_data[4] = {0x01, 0xAB, 0xCD, 0xEF};

服务层负责解析这些字节,转换成有意义的物理量:

// 服务层处理
float temperature = SVC_Sensor_GetTemperature();  // 返回 25.6°C
float humidity    = SVC_Sensor_GetHumidity();     // 返回 68.3%

服务层还负责:

  • 协议解析:Modbus、CANOpen、自定义协议
  • 数据缓存:环形缓冲区、FIFO、状态缓存
  • 事件管理:按键事件、超时事件、错误事件
  • 超时处理:通信超时、任务超时、看门狗喂狗

关键点:服务层是唯一可以包含「状态机」的地方。应用层的状态机调用服务层,服务层内部也可以有自己的状态机(比如协议解析状态机)。但驱动层和 HAL 层绝对不要出现状态机。

3.3.4 应用层(Application Layer)

应用层在最上面,是产品的灵魂。它只关心「做什么」,不关心「怎么做」。

比如一个智能温控器,应用层的逻辑可能是:

// 应用层伪代码
void APP_TemperatureControl(void) {
    float temp = SVC_Sensor_GetTemperature();
    float target = SVC_Config_GetTargetTemp();
    
    if (temp < target - 1.0) {
        SVC_Heater_TurnOn();
    } else if (temp > target + 1.0) {
        SVC_Heater_TurnOff();
    }
}

你看,应用层不知道传感器是 DS18B20 还是 NTC,不知道加热器是继电器还是可控硅,不知道通信用的是 I2C 还是 SPI。它只管业务逻辑。

应用层通常包含:

  • 主状态机:系统状态切换(初始化、运行、休眠、故障)
  • 任务调度:周期性任务、延时任务、事件触发任务
  • 用户交互:按键处理、菜单逻辑、显示刷新
  • 业务算法:PID 控制、滤波算法、数据统计

3.4 层间调用关系

层与层之间的调用必须遵守一个铁律:只能上层调用下层,不能反向调用。

调用方向 是否允许 说明
应用层 → 服务层 ✅ 允许 最常见的调用方式
应用层 → 驱动层 ⚠️ 尽量避免 紧急情况可以,但会破坏分层
应用层 → HAL 层 ❌ 禁止 绝对不要,这是架构红线
服务层 → 驱动层 ✅ 允许 标准调用路径
驱动层 → HAL 层 ✅ 允许 驱动层必须调用 HAL
下层 → 上层 ❌ 禁止 通过回调或事件机制间接实现

特别注意:「下层调用上层」是分层架构的大忌。如果驱动层需要通知应用层数据到了,正确的做法是注册回调函数或者发送事件,而不是直接调用应用层的函数。我在一个项目中见过驱动层直接调用应用层的显示刷新函数,结果换了一个显示屏,驱动层和应用层都要改——这就是典型的耦合灾难。

3.5 实际项目中的文件组织

说了这么多理论,我们看看实际工程中怎么组织文件。这是我常用的目录结构:

project/
├── app/                    # 应用层
│   ├── app_main.c         # 主状态机
│   ├── app_menu.c         # 菜单逻辑
│   └── app_control.c      # 控制算法
├── service/                # 服务层
│   ├── svc_sensor.c       # 传感器数据解析
│   ├── svc_comm.c         # 通信协议
│   └── svc_storage.c      # 数据存储
├── driver/                 # 驱动层
│   ├── drv_uart.c         # UART 驱动
│   ├── drv_i2c.c          # I2C 驱动
│   └── drv_timer.c        # 定时器驱动
├── hal/                    # 硬件抽象层
│   ├── hal_gpio.c         # GPIO 封装
│   ├── hal_spi.c          # SPI 封装
│   └── hal_flash.c        # Flash 操作
└── main.c                  # 入口,只做初始化

每个文件都有明确归属。新同事接手,看目录就知道代码在哪。换芯片,只需要改 hal 目录。换外设,只需要改 driver 目录。改业务逻辑,只需要改 app 目录。

我的建议:从项目第一天就按这个结构建文件夹。哪怕刚开始只有几个文件,也要把架子搭好。等代码量上来再重构,成本就高了。

3.6 分层架构的收益

最后总结一下,分层架构到底能带来什么好处:

  1. 可移植性:换 MCU 只改 HAL 层,换外设只改驱动层
  2. 可维护性:每层代码独立,修改不影响其他层
  3. 可测试性:可以单独测试每一层,不用搭完整硬件
  4. 团队协作:不同工程师负责不同层,接口定好就能并行开发
  5. 代码复用:驱动层和服务层可以在多个项目中复用

嗯,分层架构不是银弹,但它能解决 80% 的嵌入式软件混乱问题。从下一个项目开始,试试这个四层结构。你会回来感谢我的。