1. 课程导论与分层架构全景:为什么需要分层?

各位同学好,我是你们这趟实战之旅的向导。咱们这门课叫《STM32分层架构思想在HAL库项目中的模块解耦落地驱动开发实战》,名字挺长,但核心就一件事:怎么把代码写得漂亮、写得稳、写得容易改。

第一节课,咱们先不急着敲键盘。先聊聊一个根本问题——为什么需要分层?

1.1 从裸机开发说起:那段“一把梭”的日子

我记得刚入行那会儿,做单片机开发,基本都是裸机。什么叫裸机?就是一个 main 函数里,初始化外设、轮询按键、处理逻辑、刷新显示,全塞在一起。代码结构大概长这样:

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART2_UART_Init();
    MX_I2C1_Init();

    while (1)
    {
        if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
        {
            // 按键处理
            HAL_Delay(20);
            // 读取传感器
            HAL_I2C_Master_Receive(...);
            // 计算数据
            // 串口发送
            HAL_UART_Transmit(...);
            // 刷新屏幕
        }
    }
}

你看,逻辑上好像也没错。但项目一复杂,问题就来了。

我曾经接手过一个项目,前任工程师把整个温控系统写在一个文件里,足足 3000 行。我想改个传感器驱动,结果发现它和按键消抖、PID 计算、OLED 刷新全耦合在一起。改一行,崩一片。那段时间我每天都在“改 bug 引入新 bug”的循环里打转。

说白了,裸机开发最大的痛点就是耦合度高、复用性差、调试困难。你想想看,一个项目里,硬件换了、外设变了、需求改了,代码就得重写大半。这哪行?

1.2 分层架构的演进:从“一锅炖”到“分盘装”

那怎么解决?其实思路很简单——把不同职责的代码分开。

就像做饭,你不会把洗菜、切菜、炒菜全在一个锅里完成吧?你会分步骤、分工具。代码也一样。

分层架构的演进,大致经历了三个阶段:

阶段 特点 典型问题
裸机直写 所有代码在 main.c,函数间直接调用 耦合高、难复用、调试痛苦
模块化拆分 按功能拆成 .c/.h 文件,如 led.c、uart.c 模块间仍存在直接依赖,换硬件要改多处
分层架构 按抽象层级划分,下层为上层提供服务 接口清晰、可替换、可测试

我个人习惯把分层架构理解成“依赖倒置”——上层依赖下层的接口,而不是具体实现。这样,底层换了,上层不用动。

1.3 三层架构的职责划分:HAL层、中间层、应用层

咱们这门课采用经典的三层架构。我画了张图,你看一眼就明白了:

STM32 三层架构全景图 应用层(Application Layer) 业务逻辑 · 任务调度 · 状态机 · 用户交互 例:温控策略、数据记录、报警逻辑 中间层(Middleware Layer) 协议解析 · 数据封装 · 模块管理 · 抽象接口 例:Modbus协议栈、FATFS文件系统、FreeRTOS封装 HAL层(Hardware Abstraction Layer) 外设驱动 · 寄存器操作 · 中断处理 · 时钟管理 例:HAL_UART_Transmit、HAL_I2C_Master_Receive 箭头方向:上层调用下层接口,下层不依赖上层

这张图就是咱们整个课程的骨架。咱们来逐一拆解:

1.3.1 HAL层:硬件的“翻译官”

HAL 层,全称 Hardware Abstraction Layer,硬件抽象层。说白了,它就是 ST 官方帮我们封装好的外设驱动库。

你不需要关心 USART 的波特率寄存器怎么配、I2C 的时序怎么调。你只需要调用:

HAL_UART_Transmit(&huart1, data, len, timeout);
HAL_I2C_Master_Receive(&hi2c1, dev_addr, buf, len, timeout);

HAL 层的职责很纯粹:屏蔽硬件差异,提供统一 API。换芯片?只要 STM32 系列,HAL 接口基本不变。

一个小建议:尽量不要在 HAL 层之外直接操作寄存器。我见过有人为了“性能”在应用层直接写 GPIO->ODR,结果换芯片时改到崩溃。HAL 层就是你的防火墙。

1.3.2 中间层:承上启下的“桥梁”

中间层,也叫 Middleware Layer。它是整个架构里最灵活、也最考验设计能力的一层。

它的职责包括:

  • 协议封装:比如把 HAL 的 UART 收发包装成 Modbus 协议栈
  • 数据管理:比如环形缓冲区、FIFO、参数存储
  • 模块抽象:比如定义一个“传感器接口”,底下可以接 DS18B20、DHT11 或模拟输入
  • 系统服务:比如 FreeRTOS 的任务管理、信号量、消息队列

举个例子,你写了一个按键驱动模块:

// 中间层:按键模块
typedef struct {
    uint8_t pin;
    GPIO_TypeDef *port;
    uint32_t last_press_time;
    uint8_t state;
} Key_t;

uint8_t Key_GetState(Key_t *key);  // 返回按下/释放/长按

应用层调用 Key_GetState() 时,根本不知道底层是 GPIO 中断还是轮询。这就是解耦。

1.3.3 应用层:业务逻辑的“舞台”

应用层是离用户最近的一层。它只关心“做什么”,不关心“怎么做”。

比如一个智能温控器:

  • 应用层:读取温度 → 判断是否超限 → 控制继电器 → 更新屏幕
  • 中间层:提供 TempSensor_Read()、Relay_Control()、LCD_Display()
  • HAL层:提供 HAL_I2C_Master_Receive()、HAL_GPIO_WritePin()

你看,应用层只调用中间层的接口,中间层再调用 HAL 层。每一层各司其职。

1.4 三层之间的交互关系:单向依赖,接口隔离

这里有个关键原则:依赖方向必须从上到下,不能反向。

  • 应用层 → 调用 → 中间层
  • 中间层 → 调用 → HAL层
  • HAL层 → 不依赖任何上层

如果中间层需要通知应用层(比如数据接收完成),怎么办?用回调函数或事件标志,而不是直接调用应用层的函数。

我曾经在一个项目中,看到中间层直接调用了应用层的显示函数。结果后来要换屏幕,应用层改了接口,中间层也得跟着改,连锁反应。记住:下层永远不知道上层的存在。

为了更直观,我画了另一张交互图:

三层交互关系:单向依赖 + 接口隔离 应用层 中间层 HAL层 业务逻辑 协议/抽象/管理 硬件驱动 关键规则:上层只能调用下层的接口,下层不能反向依赖上层 跨层通信通过回调、事件、消息队列实现

1.5 为什么这套架构能落地?

你可能会问:道理我都懂,但实际项目中真的能这么规整吗?

我的回答是:能,而且必须这么做。

咱们这门课后面的 29 章,每一章都会带着你在 STM32 HAL 库项目里,一步步把这三层架构落地。从 GPIO 驱动、UART 协议栈、I2C 传感器接入,到 FreeRTOS 任务管理、参数存储、状态机设计,全部按这个架构来。

到课程结束时,你会拥有一个可复用、可移植、可测试的嵌入式项目框架。换芯片?换传感器?加功能?改需求?都不怕。

一句话总结这节课:分层架构不是理论空谈,它是解决嵌入式项目“耦合地狱”的实战利器。从裸机到分层,不是炫技,是生存需要。