3、分层架构总览:经典三层架构(应用层、中间件层、硬件抽象层)
好,咱们进入正题。这一章我带你看看嵌入式软件分层架构的“骨架”——经典三层架构。说白了,就是把代码从上到下切成三块:应用层、中间件层、硬件抽象层。
为什么要这么切?我刚开始做嵌入式那会儿,代码全是“大泥球”。一个 .c 文件里,既有按键扫描,又有 LCD 驱动,还混着 PID 算法。改个 GPIO 引脚,得把整个文件翻个底朝天。后来我意识到,这种写法,项目规模一上去,必死无疑。
分层架构的核心思想就一句话:每层只关心自己的事,层与层之间通过接口通信。你想想看,这样是不是清爽多了?
3.1 为什么需要分层?
先别急着看代码,咱们聊聊“为什么”。
嵌入式软件有个特点:硬件变化快,业务逻辑相对稳定。你今天用 STM32F4,明天可能换成 GD32,后天又可能用国产 RISC-V 芯片。如果代码和硬件绑死,每次换芯片都等于重写。
分层架构就是为了解决这个痛点。它把代码拆成三块:
- 应用层:只管“做什么”。比如:按下按钮就亮灯。
- 中间件层:提供“通用服务”。比如:TCP/IP 协议栈、文件系统。
- 硬件抽象层:封装“怎么做”。比如:GPIO 写高低电平、SPI 收发数据。
我在一个工业控制项目里遇到过这种情况:客户中途要求把主控从 STM32F103 换成 GD32F103。因为硬件抽象层封装得好,我只改了 HAL 层里几个寄存器地址,应用层代码一行没动。嗯,这就是分层的价值。
核心原则:上层依赖下层,但下层绝不能依赖上层。应用层可以调用中间件层,中间件层可以调用 HAL 层,但反过来不行。这叫“单向依赖”。
3.2 三层架构全景图
光说理论太干,咱们直接上图。下面这张图是我个人习惯用的三层架构结构,你可以把它当作模板。
这张图你看懂了吗?从上到下,依赖关系是单向的。应用层不知道底层用的是 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 层间接口设计要点
分层架构好不好用,关键看接口设计。我总结了几条经验:
- 接口要稳定:一旦确定,不要轻易修改。否则上层代码全要跟着改。
- 接口要通用:不要为某个特定硬件设计接口。比如 SPI 接口,应该支持不同字长、不同时钟极性。
- 接口要简洁:参数不要太多。我见过一个函数有 8 个参数,调用者每次都要查手册。
- 使用结构体封装参数:当参数较多时,用结构体传参,方便扩展。
接口设计黄金法则:让接口的使用者(上层)尽可能简单,让接口的实现者(下层)尽可能灵活。
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 层函数。这破坏了分层原则。如果非要这么做,说明中间件层的接口设计不合理。
- 接口膨胀:每层都提供几十个接口函数。其实很多接口可以合并。保持接口精简,维护起来才轻松。
- 忽视性能:分层确实会带来一点性能损失(函数调用开销)。但对于绝大多数嵌入式应用,这点损失可以忽略。如果真遇到性能瓶颈,可以在局部做优化,但不要破坏整体架构。
嗯,这一章就到这里。三层架构是嵌入式软件设计的基石。你把它搞懂了,后面的章节学起来会轻松很多。