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 分层架构的依赖关系图
下面这张图,是我每次做架构设计时都会画的。它清晰地展示了各层之间的依赖关系:
3.5 分层架构的接口设计原则
光有分层还不够,层与层之间的接口设计才是关键。我总结了三条原则:
- 接口要小:每个接口只做一件事。比如
HAL_UART_Send()只负责发送,不要在里面加校验逻辑。 - 接口要稳:接口一旦定义,尽量不要改。改接口意味着所有上层代码都要改。
- 接口要纯:不要有隐藏的全局状态。比如
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细节
- 依赖方向永远是单向的:从上到下