3、重构目标设定:从混乱到有序——定义清晰的分层架构(HAL、OSAL、BSP、APP)

好,咱们进入第三章。前面两章我们聊了为什么要重构,以及怎么去分析现有代码的“坏味道”。这一章,我们要动真格的了——设定重构目标。

说白了,就是回答一个问题:我们要把代码改成什么样?

我个人习惯,在动手改任何一行代码之前,先画好架构图。没有目标的重构,就是瞎改。你改完可能比原来还乱。

这一章,我们就来定义一套经典的分层架构:HAL、OSAL、BSP、APP。这套分层,我在十几个项目里用过,从智能家居网关到工业控制器,效果都不错。

3.1 为什么需要分层?

先想一个问题:你写嵌入式代码,最怕什么?

我最怕的是——换芯片。

项目做了一半,客户说“成本压力太大,换一颗便宜点的MCU”。你打开代码一看,GPIO操作、定时器配置、中断处理……全跟业务逻辑混在一起。改一个地方,崩一片。那种感觉,就像拆炸弹剪错了线。

分层的目的,就是把变化隔离起来。

硬件会变,操作系统会变,但你的业务逻辑不应该跟着变。这就是分层架构的核心价值。

分层架构的核心原则:

  • 单向依赖:上层只能调用下层,下层不能反向依赖上层
  • 接口隔离:层与层之间通过抽象接口通信,不直接访问内部实现
  • 可替换性:每一层都可以独立替换,不影响其他层

3.2 四层架构总览

我们这次要定义的四层架构,从上到下分别是:

层级 全称 职责 典型内容
APP Application Layer 业务逻辑、状态机、协议处理 任务调度、算法、用户交互逻辑
OSAL Operating System Abstraction Layer 操作系统抽象,屏蔽RTOS差异 任务创建、信号量、消息队列、互斥锁
HAL Hardware Abstraction Layer 硬件外设抽象,屏蔽芯片差异 GPIO、UART、SPI、I2C、Timer、ADC
BSP Board Support Package 板级支持,初始化硬件和系统 时钟配置、引脚复用、中断向量、启动代码

嗯,这里要注意:BSP和HAL容易搞混。

我简单解释一下:BSP是“板子相关的”,它知道你的板子上用了哪个晶振、哪个引脚接了哪个外设。HAL是“芯片相关的”,它只关心这个芯片的UART怎么配置、怎么收发数据。BSP调用HAL,HAL不关心BSP。

你想想看,这样分层之后,换芯片时你只需要改BSP和HAL,APP层几乎不用动。是不是舒服多了?

3.3 逐层拆解:从底层到顶层

3.3.1 BSP层:板级支持包

BSP是离硬件最近的一层。它的职责很明确:让板子跑起来。

我在项目中遇到过一个问题:某次量产时,发现一批板子的以太网不通。查了半天,发现是PHY芯片的复位时序不对。问题出在哪?BSP层的初始化顺序写死了,没有留出足够的延时。

从那以后,我定了一个规矩:BSP层只做初始化,不做业务。

典型的BSP代码结构:

// bsp.c
void BSP_Init(void)
{
    // 1. 时钟初始化
    SystemClock_Config();
    
    // 2. 引脚复用配置
    MX_GPIO_Init();
    
    // 3. 外设初始化
    MX_USART1_Init();
    MX_SPI1_Init();
    MX_I2C1_Init();
    
    // 4. 中断优先级配置
    NVIC_SetPriorityGrouping(4);
    
    // 5. 板级特定初始化(如PHY复位)
    Board_Specific_Init();
}

我的习惯:BSP层只提供 BSP_Init() 一个入口函数。所有板级初始化都在里面完成。APP层只需要在main函数开头调用一次。

3.3.2 HAL层:硬件抽象层

HAL层是嵌入式架构的“腰”。腰不好,整个架构就塌了。

HAL层的核心思想:定义一套统一的接口,屏蔽不同芯片的差异。

举个例子,GPIO操作:

// hal_gpio.h — 统一的HAL接口
typedef enum {
    HAL_GPIO_MODE_INPUT,
    HAL_GPIO_MODE_OUTPUT,
    HAL_GPIO_MODE_ALT_FUNC
} HAL_GPIO_Mode_t;

void HAL_GPIO_Init(uint8_t port, uint8_t pin, HAL_GPIO_Mode_t mode);
void HAL_GPIO_Set(uint8_t port, uint8_t pin, uint8_t level);
uint8_t HAL_GPIO_Get(uint8_t port, uint8_t pin);

底层实现可能是STM32的HAL库,也可能是NXP的SDK,甚至是寄存器直接操作。但上层代码看到的,永远是这套接口。

我曾经在一个项目里,把MCU从STM32F4换成了GD32F4。因为HAL层封装得好,APP层一行代码都没改。只换了HAL层的实现文件和BSP的初始化配置。两天搞定。

避坑指南:HAL层不要暴露芯片特有的功能。比如STM32的DMA双缓冲模式,如果你在HAL接口里暴露了,换芯片时就麻烦了。HAL接口要“通用”,不要“全功能”。

3.3.3 OSAL层:操作系统抽象层

OSAL层,说白了就是把RTOS的API包一层。

为什么要包一层?因为RTOS的种类太多了。FreeRTOS、RT-Thread、uC/OS、ThreadX……它们的API长得都不一样。

你想想看,如果APP层直接调用 xTaskCreate(),哪天你想换成RT-Thread,是不是得把所有任务创建的地方都改一遍?

OSAL层就是解决这个问题的:

// osal.h — 统一的OS抽象接口
typedef void* OSAL_TaskHandle_t;

int OSAL_TaskCreate(
    OSAL_TaskHandle_t* handle,
    void (*func)(void*),
    void* param,
    uint32_t stack_size,
    uint32_t priority
);

int OSAL_SemCreate(OSAL_SemHandle_t* handle);
int OSAL_SemTake(OSAL_SemHandle_t handle, uint32_t timeout_ms);
int OSAL_SemGive(OSAL_SemHandle_t handle);

底层实现:

// osal_freertos.c — FreeRTOS实现
#include "FreeRTOS.h"
#include "task.h"

int OSAL_TaskCreate(
    OSAL_TaskHandle_t* handle,
    void (*func)(void*),
    void* param,
    uint32_t stack_size,
    uint32_t priority
) {
    BaseType_t ret = xTaskCreate(
        (TaskFunction_t)func,
        "task",
        stack_size / sizeof(StackType_t),
        param,
        priority,
        (TaskHandle_t*)handle
    );
    return (ret == pdPASS) ? 0 : -1;
}

关键点:OSAL层只抽象“操作系统服务”,不抽象“硬件”。任务、信号量、消息队列、互斥锁、时间管理——这些是OSAL的范畴。GPIO、UART——那是HAL的事。

3.3.4 APP层:应用层

APP层是业务逻辑的所在地。它只关心“做什么”,不关心“怎么做”。

APP层调用OSAL来创建任务、同步数据;调用HAL来读写外设。但它不知道底层是FreeRTOS还是ThreadX,也不知道芯片是STM32还是GD32。

一个典型的APP层代码:

// app_sensor.c — 传感器采集任务
#include "osal.h"
#include "hal_i2c.h"
#include "hal_gpio.h"

static void SensorTask(void* param)
{
    uint8_t data[4];
    
    while(1) {
        // 读取传感器数据(通过HAL)
        HAL_GPIO_Set(PORT_A, PIN_0, 1);  // 使能传感器
        HAL_I2C_Read(DEVICE_ADDR, REG_DATA, data, 4);
        HAL_GPIO_Set(PORT_A, PIN_0, 0);  // 关闭传感器
        
        // 处理数据(纯业务逻辑)
        ProcessSensorData(data);
        
        // 等待下一次采集(通过OSAL)
        OSAL_TaskDelay(100);
    }
}

void APP_Init(void)
{
    OSAL_TaskHandle_t handle;
    OSAL_TaskCreate(&handle, SensorTask, NULL, 2048, 5);
}

你看,APP层里没有 HAL_GPIO_WriteBit() 这种芯片相关的调用,也没有 vTaskDelay() 这种RTOS特有的API。全部通过抽象接口完成。

3.4 分层架构的依赖关系图

下面这张图,是我每次做架构设计时都会画的。它清晰地展示了各层之间的依赖关系:

四层架构依赖关系图 APP 应用层 业务逻辑 · 状态机 · 协议处理 调用 调用 OSAL 操作系统抽象层 任务 · 信号量 · 消息队列 HAL 硬件抽象层 GPIO · UART · SPI · I2C · Timer 依赖 依赖 BSP 板级支持包 时钟配置 · 引脚复用 · 中断向量 · 启动代码 操作 硬件(MCU + 外设) 单向依赖:APP → OSAL/HAL → BSP → 硬件。上层不直接依赖下层实现,只依赖抽象接口。

3.5 分层架构的接口设计原则

光有分层还不够,层与层之间的接口设计才是关键。我总结了三条原则:

  1. 接口要小:每个接口只做一件事。比如 HAL_UART_Send() 只负责发送,不要在里面加校验逻辑。
  2. 接口要稳:接口一旦定义,尽量不要改。改接口意味着所有上层代码都要改。
  3. 接口要纯:不要有隐藏的全局状态。比如 HAL_GPIO_Set() 不应该依赖某个全局变量来判断当前引脚模式。

我的经验:接口设计时,先写头文件,再写实现。头文件就是“契约”,实现是“履约”。契约定了,就不要轻易毁约。

3.6 一个完整的例子:温湿度采集

最后,我们用一个完整的例子,看看四层架构是怎么协同工作的。

场景:每隔1秒采集一次SHT30温湿度传感器数据,通过UART打印出来。

BSP层:

// bsp.c
void BSP_Init(void)
{
    SystemClock_Config();  // 配置系统时钟
    MX_I2C1_Init();        // 初始化I2C1(接SHT30)
    MX_USART1_Init();      // 初始化USART1(调试打印)
}

HAL层:

// hal_i2c.c
int HAL_I2C_Read(uint8_t dev_addr, uint8_t reg, uint8_t* data, uint16_t len)
{
    // 调用芯片SDK或寄存器操作
    return HAL_I2C_Master_Receive(&hi2c1, dev_addr, data, len, 100);
}

// hal_uart.c
void HAL_UART_Send(uint8_t* data, uint16_t len)
{
    HAL_UART_Transmit(&huart1, data, len, 100);
}

OSAL层:

// osal.c
int OSAL_TaskCreate(OSAL_TaskHandle_t* handle, void (*func)(void*), 
                    void* param, uint32_t stack, uint32_t prio)
{
    return xTaskCreate(func, "task", stack/4, param, prio, handle);
}

void OSAL_TaskDelay(uint32_t ms)
{
    vTaskDelay(pdMS_TO_TICKS(ms));
}

APP层:

// app_sensor.c
static void SensorTask(void* param)
{
    uint8_t buf[6];
    float temp, humi;
    
    while(1) {
        // 读取SHT30数据
        HAL_I2C_Read(0x44, 0x00, buf, 6);
        
        // 计算温湿度
        temp = -45.0 + 175.0 * (buf[0] << 8 | buf[1]) / 65535.0;
        humi = 100.0 * (buf[3] << 8 | buf[4]) / 65535.0;
        
        // 打印结果
        char msg[64];
        sprintf(msg, "Temp: %.1f, Humi: %.1f\r\n", temp, humi);
        HAL_UART_Send((uint8_t*)msg, strlen(msg));
        
        // 等待1秒
        OSAL_TaskDelay(1000);
    }
}

void APP_Init(void)
{
    OSAL_TaskCreate(NULL, SensorTask, NULL, 1024, 3);
}

你看,APP层的代码里,没有一行跟芯片或RTOS相关。如果换芯片,只需要改BSP和HAL;如果换RTOS,只需要改OSAL。APP层纹丝不动。

这就是分层架构的价值:把变化隔离在局部,让稳定部分不受影响。你改你的,我不动。

3.7 小结

这一章,我们定义了四层架构:BSP、HAL、OSAL、APP。每一层都有清晰的职责和边界。

记住几个关键点:

  • BSP只管初始化,不做业务
  • HAL提供统一硬件接口,屏蔽芯片差异
  • OSAL抽象操作系统服务,屏蔽RTOS差异
  • APP只写业务逻辑,不碰硬件和OS细节
  • 依赖方向永远是单向的:从上到下