4、软件分层架构设计:分层原则、经典三层架构(HAL、OSAL、APP)、模块化设计

好,咱们今天聊一个真正决定嵌入式软件“命”的话题——分层架构。

说实话,我见过太多项目,一开始图省事,所有代码揉在一个文件里。GPIO操作、算法逻辑、通信协议全混在一起。结果呢?三个月后,改一个LED闪烁频率,都能把Wi-Fi连不上。这种“大泥球”架构,我年轻时也写过,后来被折腾得够呛。

分层架构,说白了就是“各司其职,互不干扰”。每一层只关心自己的事,层与层之间通过定义好的接口说话。你想想看,这像不像一个公司?销售部只管签单,研发部只管做产品,财务部只管算账。要是销售直接去改研发的代码,那不乱套了?

4.1 分层原则:为什么必须分?

嵌入式系统里,硬件千差万别。今天用STM32,明天可能换GD32,后天客户要求用国产RISC-V。如果你的应用代码直接操作寄存器,换一次芯片,重写一次应用。这谁受得了?

我个人习惯,分层遵循三条铁律:

  • 单向依赖:上层可以调用下层,下层绝不能反向依赖上层。这叫“依赖倒置”。
  • 接口稳定:层与层之间的API一旦定下来,就不要轻易改。改接口等于改所有上层代码。
  • 封装变化:把容易变化的东西(硬件寄存器、操作系统API)藏在底层,把稳定的业务逻辑放在上层。

核心原则:每一层只对紧邻的下层有依赖,跨层调用是架构的大忌。

我在项目中遇到过一件事:有个同事为了省事,在应用层直接调用了HAL层的某个内部函数。后来换了MCU,那个内部函数名字变了,结果应用层代码也要跟着改。嗯,这就是典型的“跨层依赖”陷阱。

4.2 经典三层架构:HAL、OSAL、APP

嵌入式领域最经典的分层,就是这三层。我画个图,你一看就明白。

嵌入式经典三层架构图 嵌入式经典三层架构 APP 应用层 业务逻辑、状态机、协议解析、用户交互 OSAL 操作系统抽象层 任务管理、信号量、消息队列、时间管理 HAL 硬件抽象层 GPIO、UART、I2C、SPI、定时器、ADC 依赖方向:APP → OSAL → HAL,上层调用下层接口,下层不感知上层存在

4.2.1 HAL(硬件抽象层)

HAL层是离硬件最近的一层。它的任务就是把MCU的寄存器操作、外设驱动封装成统一的API。比如,不管你是STM32还是GD32,HAL层都提供一个 HAL_GPIO_WritePin() 函数。

我曾经在一个项目里,因为HAL层封装得好,客户临时要求从STM32F4换到国产AT32。我只花了2天就完成了移植——改HAL层,上层代码一行没动。这就是HAL的价值。

我的习惯:HAL层不要包含任何业务逻辑。它只做一件事:把硬件操作翻译成标准接口。判断、循环、状态机,统统不要出现在这里。

4.2.2 OSAL(操作系统抽象层)

OSAL层,说白了就是“操作系统的翻译官”。你的应用可能需要跑在FreeRTOS上,也可能跑在RT-Thread上,甚至裸机。OSAL层把这些系统的任务创建、信号量、消息队列等API统一起来。

举个例子:

// OSAL 统一接口
osal_task_t osal_task_create(const char* name, 
                              void (*entry)(void*),
                              void* param,
                              uint32_t stack_size,
                              uint32_t priority);

// FreeRTOS 实现
osal_task_t osal_task_create(...) {
    TaskHandle_t handle;
    xTaskCreate(entry, name, stack_size, param, priority, &handle);
    return (osal_task_t)handle;
}

// RT-Thread 实现
osal_task_t osal_task_create(...) {
    rt_thread_t thread;
    thread = rt_thread_create(name, entry, param, 
                              stack_size, priority, 10);
    rt_thread_startup(thread);
    return (osal_task_t)thread;
}

你想想看,如果没有OSAL层,你的应用代码里到处都是 xTaskCreate 和 rt_thread_create。换一次RTOS,改几百处代码。嗯,我经历过,那感觉就像在泥潭里走路。

4.2.3 APP(应用层)

APP层是业务逻辑的所在地。它只关心“做什么”,不关心“怎么做”。比如一个智能门锁,APP层只管“验证指纹→开锁→上报状态”,至于指纹传感器怎么读、电机怎么转,那是HAL层的事。

APP层的代码,理论上应该可以在任何MCU、任何RTOS上直接编译运行。如果做不到,说明你的分层没做好。

避坑指南:我曾经见过一个项目,APP层里直接调用了HAL_GPIO_WritePin来控制LED。后来LED从GPIO控制改成了I2C扩展芯片控制,APP层代码全要改。正确的做法是:APP层调用OSAL层提供的“设备控制接口”,OSAL层再调用HAL层。这样换硬件,APP层完全不受影响。

4.3 模块化设计:怎么分才合理?

分层是纵向切分,模块化是横向切分。一个层里,可以包含多个模块。比如HAL层里,有GPIO模块、UART模块、I2C模块。

模块化设计,我总结了三句话:

  • 高内聚:一个模块只做一件事,并且把它做好。
  • 低耦合:模块之间通过接口通信,不要直接访问对方的内部变量。
  • 接口最小化:暴露给外部的函数越少越好。能隐藏的,都藏在模块内部。

举个例子,一个UART驱动模块:

// uart_driver.h —— 对外接口
void uart_init(uint32_t baudrate);
void uart_send_byte(uint8_t data);
uint8_t uart_receive_byte(void);
bool uart_is_data_available(void);

// uart_driver.c —— 内部实现
static uint8_t rx_buffer[256];
static uint8_t tx_buffer[256];
static void uart_isr_handler(void);  // 中断处理函数,对外不可见

你看,外部只需要知道4个函数。中断处理、缓冲区管理,全部藏在.c文件里。这就是“接口最小化”。

模块化检查清单:

  • 模块的.h文件是否只包含必要的接口?
  • 模块内部是否有全局变量暴露给外部?
  • 模块之间是否存在循环依赖?
  • 修改一个模块的实现,是否需要修改其他模块?

4.4 分层架构的常见误区

我这些年评审过不少嵌入式代码,发现几个反复出现的问题:

误区 表现 后果
跨层调用 APP层直接调用HAL层函数 换硬件必须改APP代码
层间耦合过紧 OSAL层依赖特定的HAL实现 换MCU时OSAL层也要重写
接口膨胀 一个模块暴露几十个函数 模块之间依赖关系混乱
伪分层 虽然分了文件,但函数之间互相调用 实际上还是大泥球

我的建议:刚开始做分层时,不要追求完美。先定好接口,哪怕实现粗糙一点。接口稳定了,后面重构实现就轻松多了。我见过太多人,接口没想清楚就开始写代码,最后改得面目全非。

4.5 一个实际的分层案例

最后,我分享一个智能温控器的分层设计。这个项目我做过,结构很清晰:

project/
├── app/                    # 应用层
│   ├── temp_control.c      # 温度控制逻辑
│   ├── user_interface.c    # 按键、显示交互
│   └── communication.c     # 与上位机通信协议
│
├── osal/                   # 操作系统抽象层
│   ├── osal_task.c         # 任务管理
│   ├── osal_queue.c        # 消息队列
│   └── osal_timer.c        # 软件定时器
│
├── hal/                    # 硬件抽象层
│   ├── hal_gpio.c          # GPIO驱动
│   ├── hal_uart.c          # 串口驱动
│   ├── hal_i2c.c           # I2C驱动(接温度传感器)
│   └── hal_adc.c           # ADC驱动(接电位器)
│
└── board/                  # 板级支持包
    ├── board_init.c        # 板级初始化
    └── board_config.h      # 引脚映射、时钟配置

你看,每一层各司其职。APP层只调用OSAL层的接口,OSAL层调用HAL层,HAL层操作寄存器。board目录放的是板级配置,换板子只需要改这里。

嗯,这就是我心目中“干净”的嵌入式软件架构。它可能不是最炫酷的,但一定是最耐用的。你想想看,当项目迭代到第5年,当硬件换了3代,当团队成员换了一批又一批,能让你睡个好觉的,就是这种清晰的分层架构。