一次"换芯片"引发的重构
前年我接手了一个项目,前团队用 STM32F103 做了一款数据采集器,代码量大概两万行。项目要升级,主控换成 STM32G474,因为需要更高的 ADC 采样率和浮点运算能力。
硬件工程师改完板子,软件团队开始移植。按常理,STM32F1 到 STM32G4,都是 ST 的芯片,HAL 库也兼容,应该一两周就能搞定。
结果移植了三周,还没跑通。
排查发现,问题出在驱动的分层结构上。前团队写代码时,把业务逻辑和硬件操作混在了一起。举几个典型例子:
例子一: 一个读取温度的函数,里面直接操作了 ADC 的寄存器,同时也做了温度换算和滤波计算。换芯片后 ADC 寄存器变了,温度换算和滤波逻辑也要跟着改。
例子二: 一个 UART 发送函数,里面直接调用了 HAL_UART_Transmit,同时嵌入了 Modbus 协议帧的构造。换芯片后 HAL 库版本变了,函数签名有变化,协议部分也跟着受影响。
例子三: 一个 Flash 存储模块,把 SPI Flash 的读写和文件系统逻辑写在同一个文件里,而且直接引用了具体的 GPIO 引脚定义。换芯片后引脚变了,整个文件都要改。
根本问题:没有清晰的分层。 硬件相关的代码和业务逻辑耦合在一起,换硬件就得改业务代码。
这次移植最后花了五周。其中三周是在剥离耦合。如果一开始就分好层,移植可能只需要三天。
这就是驱动分层的价值:把"变化的部分"和"不变的部分"隔离开。 硬件会变,但业务逻辑尽量不变。分层的目的,就是让硬件变化时,只需要改最少的地方。
一、分层的核心原则:依赖方向
什么是"好的分层"
分层不是简单地"把代码分成几个文件夹"。分层要解决的核心问题是:当底层变化时,上层要不要改。
一个好的分层结构,应该满足:
-
上层依赖下层的接口,不依赖下层的实现。
-
下层不知道上层的存在。
-
硬件相关的代码集中在最底层,上层代码不直接操作寄存器。
用一张图表示:

关键点: 箭头方向是单向的,上层依赖下层,下层不知道上层。
常见的错误分层
错误一:没有 HAL 层,驱动直接操作寄存器。
// 驱动层直接操作寄存器
void UART_SendByte(uint8_t byte)
{
while (!(USART1->SR & USART_SR_TXE));
USART1->DR = byte;
}
换芯片后,USART1、USART_SR_TXE、USART_SR_TXE 这些都要改。如果这个函数被上层调用了几百次,改起来就是灾难。
错误二:HAL 层太厚,把业务逻辑塞进去。
// HAL 层里做了业务逻辑
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 解析 Modbus 协议
if (ParseModbusByte(huart->Instance->DR)) {
HandleModbusCommand();
}
}
HAL 回调函数应该是纯粹的硬件事件通知,不应该包含协议解析。换芯片时,这个回调函数可能被重写,协议逻辑就丢了。
错误三:驱动层直接调用应用层函数。
// 驱动层反向依赖应用层
void Sensor_ReadComplete(void)
{
// 驱动层调用应用层的函数
App_OnSensorDataReady();
}
这违反了依赖方向。驱动层应该只提供数据,不关心谁用、怎么用。
二、HAL 层:该多薄
HAL 层的职责边界
HAL 层是唯一允许直接操作寄存器的层。它的职责是:
屏蔽芯片差异。 上层调用 HAL_UART_Send(),不关心是 STM32 还是 NXP,不关心是 UART1 还是 UART2。
提供最小可用的操作接口。 初始化、发送、接收、控制。
不包含业务逻辑。 不做协议解析、不做数据换算、不做状态管理。
判断 HAL 层是否合理的一个标准:换一颗芯片,需要改哪些文件? 如果只改 HAL 层的文件,说明分层合理。如果驱动层、中间件层、应用层都要改,说明 HAL 层太薄或者没分好。
HAL 接口的设计原则
原则一:接口要稳定。
HAL 接口一旦定义,就要尽量稳定。因为上层代码依赖它。如果接口频繁变化,上层就要跟着改,分层就失去了意义。
原则二:接口要最小化。
只暴露上层需要的东西。不要把所有寄存器操作都封装成 HAL 函数。比如:
// 好的 HAL 接口:最小化
typedef struct {
uint32_t baudrate;
uint8_t data_bits;
uint8_t stop_bits;
uint8_t parity;
} UART_Config_t;
int HAL_UART_Init(uint8_t port, const UART_Config_t *config);
int HAL_UART_Send(uint8_t port, const uint8_t *data, uint32_t len);
int HAL_UART_Receive(uint8_t port, uint8_t *data, uint32_t len);
int HAL_UART_SendAsync(uint8_t port, const uint8_t *data, uint32_t len,
void (*callback)(int result));
// 不好的 HAL 接口:暴露太多细节
void HAL_UART_SetBaudrate(USART_TypeDef *instance, uint32_t baud);
void HAL_UART_EnableTX(USART_TypeDef *instance);
void HAL_UART_EnableRX(USART_TypeDef *instance);
void HAL_UART_SetWordLength(USART_TypeDef *instance, uint8_t bits);
// … 几十个函数,上层要自己组合
原则三:用句柄(Handle)而不是全局变量。
// 好的做法:用句柄
typedef struct UART_Handle UART_Handle_t;
UART_Handle_t *HAL_UART_Open(uint8_t port, const UART_Config_t *config);
int HAL_UART_Send(UART_Handle_t *handle, const uint8_t *data, uint32_t len);
void HAL_UART_Close(UART_Handle_t *handle);
// 不好的做法:用全局变量
extern UART_HandleTypeDef huart1;
extern UART_HandleTypeDef huart2;
句柄的好处是:支持多实例,便于测试(可以传入 mock 句柄),避免全局变量带来的耦合。
HAL 层的实现示例
以 UART 为例,HAL 层的实现:
// hal_uart.h —— 接口定义
#ifndef HAL_UART_H
#define HAL_UART_H
#include <stdint.h>
typedef enum {
HAL_UART_PORT_1 = 0,
HAL_UART_PORT_2,
HAL_UART_PORT_MAX
} HAL_UART_Port_t;
typedef struct {
uint32_t baudrate;
uint8_t data_bits; // 7, 8, 9
uint8_t stop_bits; // 1, 2
uint8_t parity; // 0=none, 1=odd, 2=even
} HAL_UART_Config_t;
typedef void (*HAL_UART_RxCallback_t)(uint8_t byte, void *user_data);
typedef void (*HAL_UART_TxCompleteCallback_t)(int result, void *user_data);
int HAL_UART_Init(HAL_UART_Port_t port, const HAL_UART_Config_t *config);
int HAL_UART_RegisterRxCallback(HAL_UART_Port_t port,
HAL_UART_RxCallback_t callback,
void *user_data);
int HAL_UART_Send(HAL_UART_Port_t port, const uint8_t *data, uint32_t len);
int HAL_UART_SendAsync(HAL_UART_Port_t port, const uint8_t *data, uint32_t len,
HAL_UART_TxCompleteCallback_t callback, void *user_data);
int HAL_UART_DeInit(HAL_UART_Port_t port);
#endif
// hal_uart_stm32.c —— STM32 实现
#include "hal_uart.h"
#include "stm32g4xx_hal.h"
static UART_HandleTypeDef huarts[HAL_UART_PORT_MAX];
static HAL_UART_RxCallback_t rx_callbacks[HAL_UART_PORT_MAX];
static void *rx_user_data[HAL_UART_PORT_MAX];
static uint8_t rx_byte[HAL_UART_PORT_MAX];
int HAL_UART_Init(HAL_UART_Port_t port, const HAL_UART_Config_t *config)
{
if (port >= HAL_UART_PORT_MAX) return -1;
huarts[port].Instance = (port == HAL_UART_PORT_1) ? USART1 : USART2;
huarts[port].Init.BaudRate = config->baudrate;
huarts[port].Init.WordLength = (config->data_bits == 8) ? UART_WORDLENGTH_8B : UART_WORDLENGTH_9B;
huarts[port].Init.StopBits = (config->stop_bits == 1) ? UART_STOPBITS_1 : UART_STOPBITS_2;
huarts[port].Init.Parity = (config->parity == 0) ? UART_PARITY_NONE :
(config->parity == 1) ? UART_PARITY_ODD : UART_PARITY_EVEN;
huarts[port].Init.Mode = UART_MODE_TX_RX;
huarts[port].Init.HwFlowCtl = UART_HWCONTROL_NONE;
huarts[port].Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huarts[port]) != HAL_OK) {
return -1;
}
// 启动接收中断
HAL_UART_Receive_IT(&huarts[port], &rx_byte[port], 1);
return 0;
}
// HAL 回调:只做数据传递,不做业务处理
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
for (int i = 0; i < HAL_UART_PORT_MAX; i++) {
if (huart->Instance == huarts[i].Instance) {
if (rx_callbacks[i]) {
rx_callbacks[i](rx_byte[i], rx_user_data[i]);
}
HAL_UART_Receive_IT(&huarts[i], &rx_byte[i], 1);
break;
}
}
}
// hal_uart_mock.c —— 用于单元测试的 mock 实现
int HAL_UART_Init(HAL_UART_Port_t port, const HAL_UART_Config_t *config)
{
// 记录调用,返回成功
mock_record_call("HAL_UART_Init", port, config);
return 0;
}
int HAL_UART_Send(HAL_UART_Port_t port, const uint8_t *data, uint32_t len)
{
mock_record_send(port, data, len);
return 0;
}
这个设计的价值: 应用层和驱动层只依赖 hal_uart.h。换芯片时,只需要换 hal_uart_stm32.c 为 hal_uart_nxp.c,上层代码一行不改。单元测试时,用 hal_uart_mock.c 替换,不需要真实硬件。
HAL 层该多薄:一个判断标准
HAL 层应该薄到"只做硬件操作,不做任何决策"。
具体来说:

一个简单的测试: 如果把 HAL 层的代码打印出来给一个不懂业务的人看,他应该只能看到"硬件操作",看不到"业务逻辑"。
三、驱动层:设备驱动怎么做
驱动层的职责
驱动层在 HAL 层之上,负责把一个具体的设备(传感器、Flash、屏幕)封装成可用的接口。它知道设备的特性(比如某个传感器的寄存器地址、某个 Flash 的扇区大小),但不涉及业务逻辑。
驱动层的核心任务是:
设备初始化序列。 比如某个传感器需要先写配置寄存器,再等待一段时间,再读 ID 确认。
数据读写。 把设备的原始数据转换成有意义的物理量(比如 ADC 值转温度)。
错误处理。 处理设备特有的错误(通信超时、校验失败)。
状态管理。 跟踪设备的状态(已初始化、忙、错误)。
驱动层接口设计示例
以 SPI Flash 驱动为例:
// drv_flash.h —— 驱动接口
#ifndef DRV_FLASH_H
#define DRV_FLASH_H
#include <stdint.h>
#include <stdbool.h>
typedef struct DrvFlash DrvFlash_t;
typedef struct {
uint32_t sector_size; // 扇区大小
uint32_t total_size; // 总容量
uint32_t page_size; // 页大小
} DrvFlash_Info_t;
// 创建/销毁
DrvFlash_t *DrvFlash_Create(void);
void DrvFlash_Destroy(DrvFlash_t *flash);
// 初始化
int DrvFlash_Init(DrvFlash_t *flash);
// 读写
int DrvFlash_Read(DrvFlash_t *flash, uint32_t addr, uint8_t *buf, uint32_t len);
int DrvFlash_Write(DrvFlash_t *flash, uint32_t addr, const uint8_t *buf, uint32_t len);
int DrvFlash_EraseSector(DrvFlash_t *flash, uint32_t addr);
// 信息
int DrvFlash_GetInfo(DrvFlash_t *flash, DrvFlash_Info_t *info);
// 状态
bool DrvFlash_IsReady(DrvFlash_t *flash);
#endif
// drv_flash_w25qxx.c —— W25Qxx 系列 Flash 驱动实现
#include "drv_flash.h"
#include "hal_spi.h"
#include "hal_gpio.h"
#define W25Q_CMD_READ_ID 0x9F
#define W25Q_CMD_READ_DATA 0x03
#define W25Q_CMD_PAGE_PROGRAM 0x02
#define W25Q_CMD_SECTOR_ERASE 0x20
#define W25Q_CMD_READ_STATUS 0x05
#define W25Q_CMD_WRITE_ENABLE 0x06
struct DrvFlash {
HAL_SPI_Handle_t *spi;
HAL_GPIO_Pin_t cs_pin;
DrvFlash_Info_t info;
bool initialized;
};
// 私有函数:发送命令
static int flash_send_cmd(DrvFlash_t *flash, uint8_t cmd)
{
HAL_GPIO_Write(flash->cs_pin, 0);
int ret = HAL_SPI_Transfer(flash->spi, &cmd, NULL, 1);
HAL_GPIO_Write(flash->cs_pin, 1);
return ret;
}
// 私有函数:等待忙状态结束
static int flash_wait_ready(DrvFlash_t *flash, uint32_t timeout_ms)
{
uint32_t start = HAL_GetTick();
uint8_t status;
do {
HAL_GPIO_Write(flash->cs_pin, 0);
uint8_t cmd = W25Q_CMD_READ_STATUS;
HAL_SPI_Transfer(flash->spi, &cmd, NULL, 1);
HAL_SPI_Transfer(flash->spi, NULL, &status, 1);
HAL_GPIO_Write(flash->cs_pin, 1);
if (!(status & 0x01)) return 0; // 不忙了
if (HAL_GetTick() – start > timeout_ms) {
return -2; // 超时
}
} while (1);
}
int DrvFlash_Init(DrvFlash_t *flash)
{
// 读取 ID,确认设备存在
HAL_GPIO_Write(flash->cs_pin, 0);
uint8_t cmd = W25Q_CMD_READ_ID;
uint8_t id[3];
HAL_SPI_Transfer(flash->spi, &cmd, NULL, 1);
HAL_SPI_Transfer(flash->spi, NULL, id, 3);
HAL_GPIO_Write(flash->cs_pin, 1);
if (id[0] == 0x00 || id[0] == 0xFF) {
return -1; // 设备不存在
}
// 根据 ID 填充 info
flash->info.sector_size = 4096;
flash->info.page_size = 256;
flash->info.total_size = 8 * 1024 * 1024; // 8MB
flash->initialized = true;
return 0;
}
int DrvFlash_Write(DrvFlash_t *flash, uint32_t addr, const uint8_t *buf, uint32_t len)
{
if (!flash->initialized) return -1;
// 写使能
flash_send_cmd(flash, W25Q_CMD_WRITE_ENABLE);
// 页编程
HAL_GPIO_Write(flash->cs_pin, 0);
uint8_t cmd = W25Q_CMD_PAGE_PROGRAM;
HAL_SPI_Transfer(flash->spi, &cmd, NULL, 1);
uint8_t addr_bytes[3] = {addr >> 16, addr >> 8, addr};
HAL_SPI_Transfer(flash->spi, addr_bytes, NULL, 3);
HAL_SPI_Transfer(flash->spi, (uint8_t *)buf, NULL, len);
HAL_GPIO_Write(flash->cs_pin, 1);
// 等待写完
return flash_wait_ready(flash, 1000);
}
驱动层的关键点:
-
只依赖 HAL 接口(hal_spi.h、hal_gpio.h),不直接操作寄存器
-
封装了设备的特性(W25Q 的命令集、状态寄存器)
-
提供清晰的错误码
-
不涉及业务逻辑(不关心写入的是什么数据)
驱动层如何做到"可替换"
一个常见的需求是:产品有多个型号,用的 Flash 型号不同(W25Q64、W25Q128、GD25Q64)。如果驱动层写死了 W25Q,换型号就要改代码。
解决方案:驱动接口 + 具体实现分离。
// drv_flash.h —— 通用接口
typedef struct DrvFlash DrvFlash_t;
// 驱动操作表(虚函数表)
typedef struct {
int (*init)(DrvFlash_t *flash);
int (*read)(DrvFlash_t *flash, uint32_t addr, uint8_t *buf, uint32_t len);
int (*write)(DrvFlash_t *flash, uint32_t addr, const uint8_t *buf, uint32_t len);
int (*erase_sector)(DrvFlash_t *flash, uint32_t addr);
} DrvFlash_Ops_t;
struct DrvFlash {
const DrvFlash_Ops_t *ops;
void *priv; // 具体驱动的私有数据
};
// 通用 API
int DrvFlash_Init(DrvFlash_t *flash) {
return flash->ops->init(flash);
}
int DrvFlash_Read(DrvFlash_t *flash, uint32_t addr, uint8_t *buf, uint32_t len) {
return flash->ops->read(flash, addr, buf, len);
}
// drv_flash_w25qxx.c —— W25Q 实现
static const DrvFlash_Ops_t w25q_ops = {
.init = w25q_init,
.read = w25q_read,
.write = w25q_write,
.erase_sector = w25q_erase_sector,
};
DrvFlash_t *DrvFlash_CreateW25Q(HAL_SPI_Handle_t *spi, HAL_GPIO_Pin_t cs)
{
DrvFlash_t *flash = malloc(sizeof(DrvFlash_t));
flash->ops = &w25q_ops;
flash->priv = malloc(sizeof(W25Q_Priv_t));
// 初始化 priv
return flash;
}
应用层只依赖 DrvFlash_t 和通用 API,不关心具体是哪个型号。 在初始化时根据硬件版本选择不同的创建函数:
DrvFlash_t *flash;
if (hardware_version == HW_V1) {
flash = DrvFlash_CreateW25Q(spi, cs);
} else {
flash = DrvFlash_CreateGD25Q(spi, cs);
}
DrvFlash_Init(flash);
四、中间件层:通用服务的复用
中间件层的定位
中间件层是跨项目复用的通用服务。它们不依赖具体硬件,也不依赖具体业务。典型的中间件包括:
-
环形缓冲区(Ring Buffer)
-
协议解析器(Modbus、JSON、自定义协议)
-
日志系统
-
文件系统(FatFS、LittleFS)
-
状态机框架
-
事件总线
中间件层的特点:
与硬件无关。 不包含任何寄存器操作。
与业务无关。 不包含具体的业务逻辑。
可独立测试。 可以在 PC 上编译运行,不需要 MCU。
接口稳定。 一旦定义,很少变化。
中间件层的示例:环形缓冲区
// ring_buffer.h —— 中间件层
#ifndef RING_BUFFER_H
#define RING_BUFFER_H
#include <stdint.h>
#include <stdbool.h>
typedef struct {
uint8_t *buf;
uint32_t size;
volatile uint32_t head; // 写指针
volatile uint32_t tail; // 读指针
} RingBuffer_t;
void RingBuffer_Init(RingBuffer_t *rb, uint8_t *buf, uint32_t size);
bool RingBuffer_Put(RingBuffer_t *rb, uint8_t byte);
bool RingBuffer_Get(RingBuffer_t *rb, uint8_t *byte);
uint32_t RingBuffer_Count(RingBuffer_t *rb);
void RingBuffer_Flush(RingBuffer_t *rb);
#endif
// ring_buffer.c
#include "ring_buffer.h"
void RingBuffer_Init(RingBuffer_t *rb, uint8_t *buf, uint32_t size)
{
rb->buf = buf;
rb->size = size;
rb->head = 0;
rb->tail = 0;
}
// 单生产者调用(ISR 或单个任务)
bool RingBuffer_Put(RingBuffer_t *rb, uint8_t byte)
{
uint32_t next_head = (rb->head + 1) % rb->size;
if (next_head == rb->tail) {
return false; // 满了
}
rb->buf[rb->head] = byte;
rb->head = next_head;
return true;
}
// 单消费者调用(单个任务)
bool RingBuffer_Get(RingBuffer_t *rb, uint8_t *byte)
{
if (rb->head == rb->tail) {
return false; // 空了
}
*byte = rb->buf[rb->tail];
rb->tail = (rb->tail + 1) % rb->size;
return true;
}
这个环形缓冲区可以在 PC 上单元测试:
// test_ring_buffer.c
void test_ring_buffer_basic(void)
{
uint8_t buf[10];
RingBuffer_t rb;
RingBuffer_Init(&rb, buf, 10);
// 写入 5 个字节
for (int i = 0; i < 5; i++) {
assert(RingBuffer_Put(&rb, i) == true);
}
assert(RingBuffer_Count(&rb) == 5);
// 读取 5 个字节
for (int i = 0; i < 5; i++) {
uint8_t byte;
assert(RingBuffer_Get(&rb, &byte) == true);
assert(byte == i);
}
assert(RingBuffer_Count(&rb) == 0);
}
中间件层的价值: 一次编写,多个项目复用。一次测试,所有项目受益。
五、应用层:业务逻辑的归属
应用层该做什么
应用层是业务逻辑的归属地。它知道"要做什么",不关心"怎么做"。比如:
-
数据采集流程:先读传感器,再滤波,再判断阈值,再上报
-
协议处理:收到 Modbus 请求,解析,执行,构造响应
-
状态机:设备的状态转换逻辑
-
任务调度:创建哪些任务,优先级怎么定
应用层不直接操作硬件,也不直接调用 HAL。 它通过驱动层的接口访问设备,通过中间件层提供的服务处理数据。
应用层与驱动层的交互
应用层通过驱动接口获取数据,然后做业务处理:
// app_sensor.c —— 应用层
#include "drv_temperature.h"
#include "ring_buffer.h"
#include "filter.h"
static DrvTemperature_t *temp_sensor;
static RingBuffer_t temp_history;
void App_SensorInit(void)
{
temp_sensor = DrvTemperature_Create(HAL_I2C_PORT_1, 0x48);
DrvTemperature_Init(temp_sensor);
static uint8_t history_buf[64];
RingBuffer_Init(&temp_history, history_buf, 64);
}
void App_SensorTask(void *pvParameters)
{
for (;;) {
// 通过驱动接口读取原始数据
float raw_temp;
if (DrvTemperature_Read(temp_sensor, &raw_temp) == 0) {
// 应用层做业务处理:滤波
float filtered = Filter_Apply(&temp_filter, raw_temp);
// 应用层做业务判断:阈值告警
if (filtered > TEMP_ALARM_THRESHOLD) {
App_TriggerAlarm(ALARM_HIGH_TEMP);
}
// 应用层决定:是否上报
if (ShouldReport(filtered)) {
App_ReportTemperature(filtered);
}
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
关键点: 应用层知道"温度超过阈值要告警",但不知道"温度传感器是 I2C 接口还是 SPI 接口"。驱动层知道"怎么读 I2C 寄存器",但不知道"读了之后要判断阈值"。
应用层与中间件层的交互
应用层使用中间件提供的服务,但不关心中间件的实现:
// app_protocol.c —— 应用层
#include "modbus.h" // 中间件层
#include "drv_uart.h"
static Modbus_t modbus;
static DrvUart_t *uart;
void App_ProtocolInit(void)
{
uart = DrvUart_Create(HAL_UART_PORT_1, 115200);
DrvUart_Init(uart);
Modbus_Init(&modbus, MODBUS_SLAVE, 0x01);
}
void App_ProtocolTask(void *pvParameters)
{
uint8_t byte;
uint8_t response[256];
uint32_t response_len;
for (;;) {
// 从驱动层读取数据
if (DrvUart_ReadByte(uart, &byte, 100) == 0) {
// 交给中间件层的 Modbus 解析器
ModbusResult_t result = Modbus_ParseByte(&modbus, byte);
if (result == MODBUS_FRAME_COMPLETE) {
// 应用层处理业务逻辑
App_HandleModbusRequest(&modbus, response, &response_len);
// 通过驱动层发送响应
DrvUart_Write(uart, response, response_len);
}
}
}
}
六、分层带来的收益:一个真实对比
回到开头那个移植项目。如果一开始就分好层,移植的工作量会是什么样?
需要改的文件:

实际移植时间: 大约 3 天。其中 2 天是重写 HAL 层,1 天是调试。
对比没有分层的版本: 5 周。其中 3 周在剥离耦合,2 周在调试。
分层带来的收益是 10 倍以上的效率差异。 而且这个收益在每次换芯片、每次升级硬件时都会重复获得。
七、常见误区与踩坑记录
误区一:HAL 层太厚,变成"业务层"
有些项目的 HAL 层里塞满了业务逻辑,比如:
// HAL 层里做了协议解析
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (ParseModbusByte(…)) {
HandleModbusCommand(…);
}
}
问题: 换芯片时,这个回调函数要重写,业务逻辑就丢了。或者换协议时,要改 HAL 层,而 HAL 层本不该关心协议。
正确做法: HAL 回调只做数据传递,协议解析放在中间件层,业务处理放在应用层。
误区二:驱动层直接调用应用层
// 驱动层反向依赖
void DrvSensor_OnDataReady(float value)
{
App_OnSensorData(value); // 驱动层调用应用层
}
问题: 依赖方向反了。驱动层应该只提供数据,不关心谁用。
正确做法: 驱动层提供回调注册接口,应用层注册回调:
// 驱动层
typedef void (*DrvSensor_Callback_t)(float value, void *user_data);
void DrvSensor_RegisterCallback(DrvSensor_t *sensor,
DrvSensor_Callback_t callback,
void *user_data);
// 应用层
void App_OnSensorData(float value, void *user_data)
{
// 业务处理
}
DrvSensor_RegisterCallback(sensor, App_OnSensorData, NULL);
误区三:中间件层依赖硬件
有些中间件(比如日志系统)为了性能,直接操作 UART 寄存器。这破坏了中间件层的可移植性。
正确做法: 中间件层通过接口与硬件交互。比如日志系统提供一个"输出函数"接口,由应用层注入具体的 UART 发送函数:
// 中间件层
typedef void (*Log_OutputFunc_t)(const char *str);
void Log_Init(Log_OutputFunc_t output);
void Log_Info(const char *fmt, …);
// 应用层
void App_LogOutput(const char *str)
{
DrvUart_Write(uart, (const uint8_t *)str, strlen(str));
}
Log_Init(App_LogOutput);
误区四:分层过度,接口太多
分层是为了降低耦合,不是为了"看起来专业"。如果一个小项目只有三个文件,硬要分成 HAL/驱动/中间件/应用四层,反而增加了复杂度。
判断标准: 如果这个项目永远不会换芯片,也不会复用代码,那分两层就够了(硬件层 + 业务层)。分层程度应该与项目的复杂度、生命周期、复用需求匹配。
误区五:接口不稳定,频繁变化
HAL 接口一旦定义,就应该尽量稳定。如果因为某个项目的特殊需求频繁修改接口,说明接口设计有问题。
正确做法: 接口设计时考虑通用性。如果某个项目有特殊需求,通过扩展接口(而不是修改接口)来满足。比如:
// 基础接口
int HAL_UART_Send(HAL_UART_Port_t port, const uint8_t *data, uint32_t len);
// 扩展接口(不修改基础接口)
int HAL_UART_SendAsync(HAL_UART_Port_t port, const uint8_t *data, uint32_t len,
HAL_UART_TxCompleteCallback_t callback, void *user_data);
八、本篇小结
驱动分层的核心是隔离变化。硬件会变,业务逻辑尽量不变。分层的目的,是让硬件变化时,只需要改最少的地方。
第一,分层的核心原则是依赖方向。 上层依赖下层的接口,不依赖下层的实现。下层不知道上层的存在。硬件相关的代码集中在 HAL 层,上层代码不直接操作寄存器。
第二,HAL 层要薄。 只做硬件操作,不做任何决策。HAL 层是唯一允许直接操作寄存器的层,它屏蔽芯片差异,提供最小可用的操作接口。
第三,驱动层封装设备特性。 它知道设备的寄存器地址、初始化序列、错误处理方式,但不涉及业务逻辑。通过"接口 + 实现分离",支持多型号设备的可替换。
第四,中间件层提供跨项目复用的通用服务。 环形缓冲区、协议解析器、日志系统、文件系统,这些与硬件无关、与业务无关的代码,应该独立成层,便于测试和复用。
第五,应用层是业务逻辑的归属地。 它知道"要做什么",不关心"怎么做"。通过驱动层接口访问设备,通过中间件层服务处理数据。
分层的收益是复利式的。 每次换芯片、每次升级硬件、每次新增型号,分层带来的效率差异都会重复获得。
下一篇,我们讲资源受限下的日志系统设计。这是模块一的最后一篇,也是把前面所有内容串起来的一篇。我会展示怎么用环形缓冲区 + DMA + 低优先级日志任务,设计一个在 64KB RAM 的 MCU 上运行、不影响实时性、又能保留足够现场信息的日志系统。
思考题
你的项目里,驱动层有没有直接操作寄存器?如果有,换芯片时需要改多少地方?
HAL 层应该提供"同步发送"还是"异步发送"接口?还是两者都要?
如果中间件层需要分配内存(比如动态创建对象),应该由谁提供内存分配函数?
欢迎在评论区留下你的答案。下一篇见。
💖 点赞 + 收藏 + 关注
如果这篇文章让你对驱动分层有了清晰的认识,点赞让更多同行看到,收藏方便随时查阅,关注不错过下一篇《资源受限下的日志系统设计》。
网硕互联帮助中心






评论前必须登录!
注册