菜单

uwidjw
uwidjw
发布于 2025-11-23 / 1 阅读
0
0

代码重构-可移植的驱动

STM32HAL库强耦合驱动

硬汉哥有句话我记得很清楚:“嵌入式开发的痛是兼容性”。最开始我的驱动代码底层与 STM32 的 HAL 库强耦合,比如会在文件开头extern I2C_HandleTypeDef hi2c1来引入 HAL 库中 i2c 的句柄,再编写驱动底层的原子操作:

static HAL_StatusTypeDef SHT30_I2C_Write(SHT30_HandleTypeDef *dev, uint16_t *data) {
    uint8_t tx_buf[2] = {0};
    tx_buf[0] = (uint8_t)((*data >> 8) & 0xFF);
    tx_buf[1] = (uint8_t)(*data & 0xFF);
    if (HAL_I2C_Master_Transmit_IT(dev->i2c_handle, dev->address, tx_buf, 2) != HAL_OK) {
        return HAL_ERROR;
    }

    if (osSemaphoreAcquire(i2c_transaction_semaphoreHandle, 100) != osOK) {
        return HAL_TIMEOUT;
    }

    return HAL_OK;
}

static HAL_StatusTypeDef SHT30_I2C_Read(SHT30_HandleTypeDef *dev,
                                        uint8_t *data, uint16_t len) {
    if (HAL_I2C_Master_Receive_IT(dev->i2c_handle, dev->address, data, len) != HAL_OK) {
        return HAL_ERROR;
    }

    if (osSemaphoreAcquire(i2c_transaction_semaphoreHandle, 100) != osOK) {
        return HAL_TIMEOUT;
    }

    return HAL_OK;
}

最后在原子操作之上完成这个模块的驱动编写。对于都使用 HAL 库的 STM32 来讲这当然没有问题,切换不同的 STM32MCU、更换不同的 i2c 外设等都不存在问题。但是如果我的驱动需要移植到其他公司的 MCU 呢?如果新的系统跑在裸机上呢?这时候代码就不兼容了,甚至于新的系统中不会有 HAL_OK 的定义,这样移植就会需要修改驱动中的绝大部分内容。解决这个问题的方法在于设计与底层松耦合的驱动。

函数指针

为了实现松耦合的驱动,我们要抽象出设备所需要的功能。以 SHT30 等一众 i2c 设备为例,我们只需要三个功能:

  • i2c 写入

  • i2c 读取

  • 延时函数

那么问题就非常清晰了,我们可以新建一个头文件,在其中定义一个 i2c 写入函数指针

typedef Sensor_Status_t (*Sensor_Write_Func)(uint8_t dev_addr, uint16_t reg_addr, uint16_t reg_len, uint8_t *data, uint16_t len);

配合一个typedef

typedef struct {
    Sensor_Write_Func  write;
    Sensor_Read_Func   read;
    Sensor_Delay_Func  delay;
} Sensor_IO_Interface_t;

实现设备的注册

static Sensor_Status_t STM32_I2C_Write(uint8_t dev_addr, uint16_t reg_addr, uint16_t reg_len, uint8_t *data, uint16_t len)
{
    ...//原先驱动的原子操作,在其他平台上使用其他的适配代码
}
...

//生成任意平台的i2c控制句柄
void Platform_Get_I2C1_Interface(Sensor_IO_Interface_t *io)
{
    if (io != NULL) {
        io->write = STM32_I2C_Write;
        io->read  = STM32_I2C_Read;
        io->delay = STM32_Delay;
    }
}

//注册设备驱动
Sensor_IO_Interface_t stm32_i2c1_io;
Platform_Get_I2C1_Interface(&stm32_i2c1_io);
SHT30_Init(&stm32_i2c1_io);

就可以愉快的在任何平台上使用一套相同的代码驱动设备了。SHT30 的驱动函数都类似:

Sensor_Status_t SHT30_WriteCmd(SHT30_HandleTypeDef *dev, uint16_t *cmd) {
    uint8_t cmd_buf[2];
    cmd_buf[0] = (uint8_t)((*cmd >> 8) & 0xFF);
    cmd_buf[1] = (uint8_t)(*cmd & 0xFF);
    return dev->io.write(dev->address, 0, 0, cmd_buf, 2);
}

保持了与 i2c 通信控制的松耦合,业务代码完成适配后直接调用即可。这是设计模式中的适配器模式,驱动写死了,但是提供了对应的接口,移植到其他系统上时需要编写一个适配器代码来使用驱动。

interface_port文件

完成了驱动函数的适配,那么不同平台上的类型适配呢?我们新建一个interface_port文件,通过控制宏来适配不同的类型,比如:

#if defined(STM32U575xx) || defined(STM32L051xx) || 1

    #include "main.h"

    typedef HAL_StatusTypeDef Sensor_Status_t;

    #define SENSOR_OK       HAL_OK
    #define SENSOR_ERROR    HAL_ERROR
    #define SENSOR_BUSY     HAL_BUSY
    #define SENSOR_TIMEOUT  HAL_TIMEOUT

    typedef HAL_StatusTypeDef Lora_Status_t;

    #define LORA_OK       HAL_OK
    #define LORA_ERROR    HAL_ERROR
    #define LORA_BUSY     HAL_BUSY
    #define LORA_TIMEOUT  HAL_TIMEOUT

#elif defined(ESP_PLATFORM)

    #include "esp_err.h"
    
    // ESP32 的返回值是 esp_err_t (int)
    typedef esp_err_t Sensor_Status_t;
    
    #define SENSOR_OK       ESP_OK
    #define SENSOR_ERROR    ESP_FAIL
    // ...

#endif

就实现了 STM32U575xx、STM32L051xx、ESP 等设备的适配。

完成以上工作后,就可以得到一个松耦合易移植的设备驱动了。


评论