第3章:分层架构设计原则——模块化思想与接口规范

分层架构这个词,很多工程师都听过。但真正在STM32裸机项目里把它落地,又是另一回事。我见过不少团队,一开始说好了要分层,写着写着就变成了“伪分层”——业务逻辑里直接操作寄存器,HAL层里又塞了业务判断。嗯,这其实不是分层,是“乱炖”。

这一章,我们就来聊聊怎么把分层架构做扎实。核心就三件事:模块化思想、硬件抽象层(HAL)与业务逻辑层分离、接口设计规范。

3.1 模块化思想:从“大泥球”到“乐高积木”

什么叫模块化?说白了,就是把一个复杂系统拆成若干个独立的小单元。每个单元只做一件事,做好一件事。单元之间通过明确的接口通信,内部实现对外部不可见。

我刚开始做嵌入式时,也喜欢把所有代码写在一个main.c里。那时候觉得“反正单片机资源有限,拆那么多文件干嘛”。结果呢?项目到后期,改一个LED闪烁频率,都要翻遍整个文件,生怕改错别的地方。这就是典型的“大泥球”架构。

模块化设计有几个基本原则:

  • 高内聚:一个模块内部的元素之间关联紧密,共同完成一个单一职责。
  • 低耦合:模块之间的依赖关系尽可能弱,修改一个模块不影响其他模块。
  • 接口稳定:模块对外暴露的接口一旦确定,尽量不要频繁变动。

核心观点:模块化的本质是“封装变化”。把容易变化的硬件细节封装在底层,把相对稳定的业务逻辑放在上层。这样,换硬件时只改底层,业务代码几乎不动。

举个例子。一个温控系统,你可以拆成:温度采集模块、加热控制模块、PID计算模块、用户界面模块。每个模块独立编译、独立测试。PID模块不需要知道温度是用NTC还是DS18B20测的,它只关心输入的温度值和目标值。

3.2 硬件抽象层(HAL)与业务逻辑层分离

这是分层架构里最关键的一刀。很多新手会问:“HAL层和业务逻辑层到底怎么分?分界线在哪里?”

我的经验是:凡是直接操作寄存器、外设寄存器、中断向量表的代码,都应该归到HAL层。凡是描述“做什么”而不是“怎么做”的代码,归到业务逻辑层。

来看一个对比表格:

层次 职责 典型代码 依赖关系
业务逻辑层 描述系统行为、状态机、算法 if (temp > threshold) { fan_on(); } 只调用HAL接口
硬件抽象层(HAL) 封装硬件操作、寄存器读写 void fan_on() { GPIOB->BSRR = GPIO_PIN_1; } 直接操作硬件
硬件层 MCU、传感器、执行器 STM32G474、NTC热敏电阻 物理连接

我曾经在一个项目中,把UART的发送函数直接写在业务逻辑里。后来客户要求换用RS485,我不得不把所有调用UART_Send的地方改成RS485_Send。改了几十个文件,还漏了两个,导致现场调试时数据发不出去。从那以后,我强制自己:业务逻辑里永远不出现“UART”、“SPI”、“GPIO”这些硬件名词。

正确的做法是:业务逻辑调用一个叫 send_data() 的接口,至于底层是用UART还是RS485,业务逻辑不关心。这就是“依赖倒置”——上层定义接口,下层实现接口。

3.3 接口设计规范:让分层真正“解耦”

接口是分层架构的“粘合剂”。接口设计得好,分层就成功了一半。接口设计得差,分层就是形式主义。

我总结了几条接口设计规范,供你参考:

  1. 接口参数尽量简单:能用基本类型就别用结构体,能用结构体就别用指针套指针。参数越多,耦合越紧。
  2. 接口命名要自解释:temp_get_value() 比 read_temp() 好,motor_set_speed(uint8_t speed) 比 motor_ctrl(1) 好。
  3. 接口返回值要统一:我习惯用 int8_t 返回状态,0表示成功,负数表示错误码。这样上层可以统一处理错误。
  4. 接口不要暴露内部状态:比如 uart_init() 内部可能有一个缓冲区,但接口不要返回缓冲区指针给上层。

个人习惯:我通常会在每个HAL模块的头文件里,只暴露3~5个函数。如果超过10个,说明这个模块职责过重,需要拆分。比如一个“传感器模块”,如果同时提供初始化、读取、校准、滤波、报警,那它其实应该拆成5个模块。

来看一个实际的接口设计示例。假设我们要封装一个RTC模块:

// rtc_hal.h —— 硬件抽象层接口
#ifndef RTC_HAL_H
#define RTC_HAL_H

#include <stdint.h>

typedef struct {
    uint16_t year;
    uint8_t  month;
    uint8_t  day;
    uint8_t  hour;
    uint8_t  minute;
    uint8_t  second;
} rtc_time_t;

// 初始化RTC硬件
int8_t rtc_hal_init(void);

// 设置当前时间
int8_t rtc_hal_set_time(const rtc_time_t *time);

// 读取当前时间
int8_t rtc_hal_get_time(rtc_time_t *time);

// 进入低功耗模式前的准备
int8_t rtc_hal_sleep_prepare(void);

// 从低功耗模式唤醒后的恢复
int8_t rtc_hal_wakeup_restore(void);

#endif /* RTC_HAL_H */

注意看,这个接口里没有出现任何寄存器名称,没有出现任何硬件细节。上层业务逻辑只需要调用 rtc_hal_get_time() 就能拿到时间,至于底层是STM32内置RTC还是外挂DS3231,业务逻辑完全不知道。

避坑指南:我曾经在接口里直接传了寄存器地址作为参数,比如 write_reg(0x40007000, 0x01)。结果换芯片型号时,寄存器地址全变了,接口全部要重写。记住:接口里永远不要出现硬件地址、寄存器偏移、中断号等硬件细节。

3.4 分层架构的SVG结构图

下面这张图展示了分层架构的核心结构。你可以看到,业务逻辑层在最上层,通过稳定的接口调用HAL层。HAL层封装了硬件操作细节。硬件层在最底层,是物理设备。

分层架构核心结构图 业务逻辑层 状态机 · 算法 · 用户逻辑 · 任务调度 稳定接口层(API) rtc_hal_get_time() · motor_set_speed() · send_data() 硬件抽象层(HAL) 寄存器操作 · 中断处理 · 外设配置 · 时序控制 硬件层 STM32 MCU · 传感器 · 执行器 · 通信接口 依赖方向:上层依赖下层接口 修改影响:下层变化不影响上层

这张图想表达的核心思想是:依赖方向是单向的。业务逻辑层依赖HAL层的接口,但HAL层不依赖业务逻辑层。硬件层的变化(比如换了一个传感器型号),只需要修改HAL层的实现代码,业务逻辑层完全不需要动。

3.5 实战中的常见误区

说了这么多原则,再聊聊我见过的几个常见误区。你可以在自己的项目里对照检查一下。

  • 误区一:HAL层太薄——有些人把HAL层理解为“只是把寄存器操作封装成函数”,结果HAL层只有 GPIO_Set()、UART_Send() 这种极简函数。业务逻辑里依然要组合多个HAL函数来完成一个业务动作。这其实没有真正解耦。
  • 误区二:接口膨胀——一个HAL模块暴露了20多个函数,业务逻辑可以直接调用其中任何一个。这导致业务逻辑和HAL的耦合度依然很高。正确的做法是:业务逻辑只通过少数几个“门面接口”与HAL交互。
  • 误区三:跨层调用——业务逻辑层直接调用另一个业务逻辑模块的HAL函数。比如温控模块直接调用显示模块的 LCD_ShowString()。这破坏了分层结构。正确的做法是:通过事件或消息机制传递数据。

我的建议:在项目初期,花一天时间把分层结构画清楚。每个模块的接口定义好,写在头文件里。然后先写业务逻辑层的伪代码,再写HAL层的实现。这样能保证业务逻辑不被硬件细节“污染”。

3.6 小结

分层架构不是银弹,但它确实是嵌入式裸机项目中控制复杂度、提高可维护性的有效手段。模块化思想让你把大问题拆成小问题;HAL与业务逻辑分离让你换硬件时不慌;接口设计规范让分层真正落地。