3、分层架构总览:经典三层架构(应用层、中间件层、硬件抽象层)

好,咱们进入正题。这一章我带你看看嵌入式软件分层架构的“骨架”——经典三层架构。说白了,就是把代码从上到下切成三块:应用层、中间件层、硬件抽象层。

为什么要这么切?我刚开始做嵌入式那会儿,代码全是“大泥球”。一个 .c 文件里,既有按键扫描,又有 LCD 驱动,还混着 PID 算法。改个 GPIO 引脚,得把整个文件翻个底朝天。后来我意识到,这种写法,项目规模一上去,必死无疑。

分层架构的核心思想就一句话:每层只关心自己的事,层与层之间通过接口通信。你想想看,这样是不是清爽多了?

3.1 为什么需要分层?

先别急着看代码,咱们聊聊“为什么”。

嵌入式软件有个特点:硬件变化快,业务逻辑相对稳定。你今天用 STM32F4,明天可能换成 GD32,后天又可能用国产 RISC-V 芯片。如果代码和硬件绑死,每次换芯片都等于重写。

分层架构就是为了解决这个痛点。它把代码拆成三块:

  • 应用层:只管“做什么”。比如:按下按钮就亮灯。
  • 中间件层:提供“通用服务”。比如:TCP/IP 协议栈、文件系统。
  • 硬件抽象层:封装“怎么做”。比如:GPIO 写高低电平、SPI 收发数据。

我在一个工业控制项目里遇到过这种情况:客户中途要求把主控从 STM32F103 换成 GD32F103。因为硬件抽象层封装得好,我只改了 HAL 层里几个寄存器地址,应用层代码一行没动。嗯,这就是分层的价值。

核心原则:上层依赖下层,但下层绝不能依赖上层。应用层可以调用中间件层,中间件层可以调用 HAL 层,但反过来不行。这叫“单向依赖”。

3.2 三层架构全景图

光说理论太干,咱们直接上图。下面这张图是我个人习惯用的三层架构结构,你可以把它当作模板。

经典三层嵌入式软件架构 应用层(Application Layer) 任务调度 · 业务逻辑 · 用户交互 例:按键处理、LED 控制、数据采集算法 中间件层(Middleware Layer) 协议栈 · 文件系统 · 算法库 · 状态机 例:FreeRTOS、LwIP、FatFS、环形缓冲区 硬件抽象层(HAL Layer) 寄存器封装 · 外设驱动 · 板级支持包 例:GPIO_WritePin()、SPI_Transmit()、UART_Read() 硬件(MCU / 外设 / 传感器) 依赖 方向 上层 依赖 下层

这张图你看懂了吗?从上到下,依赖关系是单向的。应用层不知道底层用的是 STM32 还是 GD32,它只知道调用中间件层提供的 API。中间件层也不知道 GPIO 寄存器地址,它只知道调用 HAL 层的接口。

我的建议:刚开始做分层时,别追求“完美分层”。先按这个三层结构把代码拆开,哪怕每层只有几个函数,也比大泥球强。后续再慢慢优化。

3.3 各层职责详解

3.3.1 应用层(Application Layer)

应用层是离用户最近的一层。它负责实现产品的“业务逻辑”。说白了,就是产品要干什么。

举个例子:一个智能温控器。

  • 应用层负责:读取温度设定值、比较当前温度、决定是否开启加热。
  • 它不关心:温度传感器是 DS18B20 还是 NTC 热敏电阻。
  • 它也不关心:加热器是继电器还是 MOSFET。

应用层的代码应该尽量“干净”。我见过很多项目,应用层里直接操作寄存器,那叫一个酸爽。后来换芯片,应用层代码全废了。

// 应用层代码示例:温控逻辑
// 注意:这里没有出现任何硬件相关代码
void TemperatureController_Task(void)
{
    float targetTemp = GetTargetTemperature();   // 从中间件层获取目标温度
    float currentTemp = GetCurrentTemperature(); // 从中间件层获取当前温度
    
    if (currentTemp < targetTemp - HYSTERESIS)
    {
        Heater_On();  // 调用中间件层接口
    }
    else if (currentTemp > targetTemp + HYSTERESIS)
    {
        Heater_Off(); // 调用中间件层接口
    }
}

3.3.2 中间件层(Middleware Layer)

中间件层是“承上启下”的一层。它提供通用的、与具体业务无关的服务。

常见的中间件组件包括:

组件 典型实现 说明
实时操作系统 FreeRTOS, RT-Thread 任务调度、信号量、消息队列
TCP/IP 协议栈 LwIP, uIP 网络通信基础
文件系统 FatFS, LittleFS 数据存储管理
通用算法库 环形缓冲区、PID 控制器 可复用的算法模块
设备抽象 传感器抽象、显示抽象 屏蔽同类设备的差异

中间件层有一个特点:它不直接操作硬件,但它依赖 HAL 层提供的接口。比如 LwIP 协议栈,它需要 HAL 层提供“发送一个以太网帧”和“接收一个以太网帧”的函数。

我曾经踩过的坑:在中间件层里直接写硬件相关的代码。比如在环形缓冲区里用到了某个 MCU 特有的 DMA 寄存器。后来换平台,这个环形缓冲区就废了。记住:中间件层应该只依赖 HAL 层的接口,不依赖具体硬件。

3.3.3 硬件抽象层(HAL Layer)

HAL 层是“最底层”的软件层。它直接和硬件打交道,封装所有硬件相关的操作。

HAL 层的核心职责:

  • 寄存器封装:把寄存器操作封装成有意义的函数名。
  • 外设驱动:提供 GPIO、UART、SPI、I2C 等外设的初始化与读写接口。
  • 板级支持:定义引脚映射、时钟配置等板级差异。

一个好的 HAL 层,应该让上层代码完全不知道底层硬件细节。比如:

// HAL 层代码示例:GPIO 抽象
// 上层调用时,不需要知道 GPIOA、GPIOB 这些寄存器地址

void HAL_GPIO_WritePin(GPIO_Pin_t pin, uint8_t state)
{
    // 内部实现:根据 pin 参数操作对应的寄存器
    // 不同 MCU 的实现不同,但接口保持一致
    if (state)
    {
        *((volatile uint32_t *)(pin.port_base + GPIO_BSRR)) = pin.pin_mask;
    }
    else
    {
        *((volatile uint32_t *)(pin.port_base + GPIO_BRR)) = pin.pin_mask;
    }
}

3.4 层间接口设计要点

分层架构好不好用,关键看接口设计。我总结了几条经验:

  1. 接口要稳定:一旦确定,不要轻易修改。否则上层代码全要跟着改。
  2. 接口要通用:不要为某个特定硬件设计接口。比如 SPI 接口,应该支持不同字长、不同时钟极性。
  3. 接口要简洁:参数不要太多。我见过一个函数有 8 个参数,调用者每次都要查手册。
  4. 使用结构体封装参数:当参数较多时,用结构体传参,方便扩展。

接口设计黄金法则:让接口的使用者(上层)尽可能简单,让接口的实现者(下层)尽可能灵活。

3.5 一个完整的例子:LED 闪烁

咱们用三层架构实现一个最简单的功能:LED 以 1Hz 频率闪烁。

HAL 层:

// hal_led.h
void HAL_LED_Init(void);
void HAL_LED_On(void);
void HAL_LED_Off(void);

// hal_led.c(假设 LED 接在 PA0)
void HAL_LED_Init(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    GPIOA->MODER &= ~(0x3 << (0 * 2));
    GPIOA->MODER |= (0x1 << (0 * 2));  // 推挽输出
}

void HAL_LED_On(void)
{
    GPIOA->BSRR = (1 << 0);
}

void HAL_LED_Off(void)
{
    GPIOA->BRR = (1 << 0);
}

中间件层:

// mw_timer.h
void MW_Timer_Init(uint32_t period_ms);
uint8_t MW_Timer_IsExpired(void);

// mw_timer.c(使用 HAL 层的定时器驱动)
static uint32_t tick_count = 0;

void MW_Timer_Init(uint32_t period_ms)
{
    HAL_TIM_Init(period_ms);  // 调用 HAL 层接口
}

uint8_t MW_Timer_IsExpired(void)
{
    if (HAL_TIM_GetTick() >= tick_count)
    {
        tick_count = HAL_TIM_GetTick() + period_ms;
        return 1;
    }
    return 0;
}

应用层:

// app_main.c
#include "hal_led.h"
#include "mw_timer.h"

void App_Init(void)
{
    HAL_LED_Init();
    MW_Timer_Init(500);  // 500ms 翻转一次,实现 1Hz 闪烁
}

void App_Loop(void)
{
    if (MW_Timer_IsExpired())
    {
        static uint8_t led_state = 0;
        led_state = !led_state;
        if (led_state)
            HAL_LED_On();
        else
            HAL_LED_Off();
    }
}

你看,应用层完全不知道 LED 接在哪个引脚,也不知道定时器用的是 SysTick 还是 TIM2。这就是分层的好处。

个人经验:刚开始做分层时,可以先从“接口先行”开始。先定义好每层的接口头文件,再实现具体代码。这样能保证层与层之间解耦。

3.6 分层架构的常见误区

最后,我聊聊几个常见的坑:

  • 过度分层:有人把三层又拆成五层、七层。对于小型 MCU 项目,三层足够了。层数太多,反而增加调用开销和代码复杂度。
  • 层间耦合:应用层直接调用 HAL 层函数。这破坏了分层原则。如果非要这么做,说明中间件层的接口设计不合理。
  • 接口膨胀:每层都提供几十个接口函数。其实很多接口可以合并。保持接口精简,维护起来才轻松。
  • 忽视性能:分层确实会带来一点性能损失(函数调用开销)。但对于绝大多数嵌入式应用,这点损失可以忽略。如果真遇到性能瓶颈,可以在局部做优化,但不要破坏整体架构。

嗯,这一章就到这里。三层架构是嵌入式软件设计的基石。你把它搞懂了,后面的章节学起来会轻松很多。