驱动固件架构设计:分层架构的核心思想
做红外探测器驱动开发这些年,我最大的体会就是——架构设计决定了你能走多远。很多新手一上来就写代码,功能跑通了就完事。但等到项目迭代、平台切换、性能调优的时候,那种「面条式代码」会让你痛不欲生。
我个人习惯,拿到一个红外探测器芯片,第一件事不是写驱动,而是画架构图。把层次理清楚,把接口定下来,后面的事情就顺了。
为什么一定要分层?
你想想看,红外探测器驱动要面对什么?
- 底层硬件:I2C/SPI时序、寄存器操作、中断处理
- 中间逻辑:数据采集、温度补偿、坏像素校正
- 上层应用:图像输出、参数配置、调试接口
如果不分层,这些代码混在一起,改一个地方可能牵动全身。我在项目中遇到过,就因为换了一个MCU平台,整个驱动代码几乎重写——就是因为当初没做分层。
三层架构:HAL层、驱动层、应用层
我推荐的分层方案是这样的:
HAL层:把硬件藏起来
HAL层说白了就是「硬件包装器」。它的职责是:让上层代码不知道自己在操作什么硬件。
举个例子,同样是I2C读写,STM32和ESP32的API完全不同。但如果你在HAL层封装一下:
/* HAL层接口定义 - hal_sensor.h */
#ifndef __HAL_SENSOR_H__
#define __HAL_SENSOR_H__
#include <stdint.h>
/* I2C读写操作 */
int32_t hal_i2c_write(uint8_t dev_addr, uint8_t reg_addr,
uint8_t *data, uint16_t len);
int32_t hal_i2c_read(uint8_t dev_addr, uint8_t reg_addr,
uint8_t *data, uint16_t len);
/* GPIO控制 */
void hal_gpio_set(uint8_t pin, uint8_t level);
uint8_t hal_gpio_get(uint8_t pin);
/* 延时函数 */
void hal_delay_ms(uint32_t ms);
uint32_t hal_get_tick_ms(void);
/* 中断注册 */
typedef void (*hal_irq_callback_t)(void);
int32_t hal_irq_register(uint8_t irq_num, hal_irq_callback_t cb);
#endif /* __HAL_SENSOR_H__ */
驱动层:核心业务逻辑
驱动层是整固件的「大脑」。它负责:
- 初始化流程:上电时序、寄存器配置、校准参数加载
- 数据采集:帧同步、数据读取、格式转换
- 图像处理:非均匀性校正、坏像素替换、温度补偿
- 状态管理:工作模式切换、错误处理、功耗控制
这里要注意,驱动层只调用HAL层的接口,不直接操作硬件寄存器。这样做的好处是——换平台时,驱动层代码几乎不用改。
我曾经接手过一个项目,探测器驱动跑在FreeRTOS上,后来要移植到裸机环境。因为驱动层和HAL层分得清楚,我只花了2天就搞定了移植。要是当初不分层,估计得折腾两周。
/* 驱动层接口定义 - driver_sensor.h */
#ifndef __DRIVER_SENSOR_H__
#define __DRIVER_SENSOR_H__
#include <stdint.h>
/* 探测器状态 */
typedef enum {
SENSOR_STATE_IDLE = 0,
SENSOR_STATE_INIT,
SENSOR_STATE_RUNNING,
SENSOR_STATE_SLEEP,
SENSOR_STATE_ERROR
} sensor_state_t;
/* 帧数据回调 */
typedef void (*frame_callback_t)(uint16_t *frame_data,
uint32_t frame_size);
/* 驱动API */
int32_t sensor_init(void);
int32_t sensor_start(frame_callback_t cb);
int32_t sensor_stop(void);
int32_t sensor_set_mode(uint8_t mode);
int32_t sensor_get_temperature(float *temp);
#endif /* __DRIVER_SENSOR_H__ */
应用层:面向用户
应用层是用户直接打交道的部分。它把驱动层的功能包装成易用的API或命令。
比如:
app_get_image()— 获取一帧图像app_set_gain(uint8_t gain)— 设置增益app_enable_ffc()— 启动快门校正
应用层还可以做协议解析,比如把探测器数据打包成UDP包发送出去,或者通过串口输出调试信息。
模块划分:怎么切才合理?
模块划分没有标准答案,但我有个经验法则:按功能职责切,不按文件切。
一个典型的红外探测器驱动,可以分成这些模块:
| 模块名 | 职责 | 所属层 |
|---|---|---|
| hal_i2c | I2C总线读写,适配不同MCU | HAL层 |
| hal_gpio | GPIO控制,中断管理 | HAL层 |
| hal_timer | 定时器、延时、时间戳 | HAL层 |
| drv_sensor | 探测器初始化、寄存器配置 | 驱动层 |
| drv_frame | 帧数据采集、缓存管理 | 驱动层 |
| drv_correction | 非均匀性校正、坏像素处理 | 驱动层 |
| drv_temp | 温度读取、温度补偿 | 驱动层 |
| app_main | 用户接口、命令解析 | 应用层 |
| app_protocol | 数据打包、通信协议 | 应用层 |
接口定义规范:约定大于配置
接口规范是团队协作的「宪法」。我总结了几个关键点:
- 命名统一:HAL层用
hal_前缀,驱动层用drv_前缀,应用层用app_前缀。一看就知道属于哪一层。 - 返回值规范:统一用
int32_t返回,0表示成功,负数表示错误码。正数用于特殊状态。 - 参数传递:尽量用指针传递大数据块,避免值拷贝。但要注意指针有效性检查。
- 回调机制:驱动层通过回调通知应用层,而不是轮询。这样效率高,也符合事件驱动思想。
int、uint8_t、int32_t,结果在32位和16位平台上出现隐式类型转换问题。后来我强制规定:所有接口参数和返回值,必须显式指定宽度,比如 uint8_t、int32_t,绝不用 int。
一个完整的调用链路
来看看实际运行时,数据是怎么流动的:
/* 应用层:获取一帧图像 */
void app_get_image(void)
{
/* 调用驱动层接口 */
sensor_start(my_frame_callback);
}
/* 驱动层:帧回调 */
static void my_frame_callback(uint16_t *frame, uint32_t size)
{
/* 驱动层处理完数据,回调应用层 */
app_on_frame_ready(frame, size);
}
/* 驱动层内部:读取一帧数据 */
static int32_t drv_read_frame(uint16_t *buffer)
{
/* 调用HAL层读取原始数据 */
hal_i2c_read(SENSOR_ADDR, REG_FRAME_DATA,
(uint8_t *)buffer, FRAME_SIZE);
/* 做校正处理 */
drv_apply_correction(buffer);
return 0;
}
你看,每一层各司其职,调用关系清晰。出了问题,定位也快——硬件问题查HAL层,逻辑问题查驱动层,用户问题查应用层。
最后说两句
架构设计这件事,说白了就是「先想清楚再动手」。我见过太多人上来就写代码,写到后面发现架构撑不住了,又回头重构。嗯,那滋味可不好受。
分层架构不是银弹,但它能让你在项目变大、团队变多、平台切换时,少掉很多头发。记住:好的架构是改出来的,不是设计出来的。先搭一个简单的分层框架,然后在迭代中不断优化。
公众号:蓝海资料掘金营,微信deep3321