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 等设备的适配。
完成以上工作后,就可以得到一个松耦合易移植的设备驱动了。