云计算百科
云计算领域专业知识百科平台

嵌入式驱动开发:量产级工程化实战(第 4 篇)——驱动分层——HAL 到底该多薄

一次"换芯片"引发的重构

前年我接手了一个项目,前团队用 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 层应该提供"同步发送"还是"异步发送"接口?还是两者都要?

  • 如果中间件层需要分配内存(比如动态创建对象),应该由谁提供内存分配函数?

  • 欢迎在评论区留下你的答案。下一篇见。


    💖 点赞 + 收藏 + 关注

    如果这篇文章让你对驱动分层有了清晰的认识,点赞让更多同行看到,收藏方便随时查阅,关注不错过下一篇《资源受限下的日志系统设计》。

     

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 嵌入式驱动开发:量产级工程化实战(第 4 篇)——驱动分层——HAL 到底该多薄
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!