驱动固件架构设计:分层架构的核心思想

做红外探测器驱动开发这些年,我最大的体会就是——架构设计决定了你能走多远。很多新手一上来就写代码,功能跑通了就完事。但等到项目迭代、平台切换、性能调优的时候,那种「面条式代码」会让你痛不欲生。

我个人习惯,拿到一个红外探测器芯片,第一件事不是写驱动,而是画架构图。把层次理清楚,把接口定下来,后面的事情就顺了。

为什么一定要分层?

你想想看,红外探测器驱动要面对什么?

  • 底层硬件:I2C/SPI时序、寄存器操作、中断处理
  • 中间逻辑:数据采集、温度补偿、坏像素校正
  • 上层应用:图像输出、参数配置、调试接口

如果不分层,这些代码混在一起,改一个地方可能牵动全身。我在项目中遇到过,就因为换了一个MCU平台,整个驱动代码几乎重写——就是因为当初没做分层。

核心原则:上层不关心底层怎么实现,底层不关心上层怎么使用。每一层只做好自己的事。

三层架构:HAL层、驱动层、应用层

我推荐的分层方案是这样的:

应用层 (Application Layer) 图像输出 · 参数配置 · 调试接口 · 用户命令 驱动层 (Driver Layer) 数据采集 · 温度补偿 · 坏像素校正 · 帧处理 HAL层 (Hardware Abstraction Layer) I2C/SPI读写 · GPIO控制 · 定时器 · 中断管理 红外探测器硬件 (Sensor Hardware) 接口标准化 硬件无关化

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层的接口要尽量「原子化」。一个函数只做一件事,比如读一个寄存器、写一个字节。别想着在HAL层做复杂逻辑,那是驱动层的事。

驱动层:核心业务逻辑

驱动层是整固件的「大脑」。它负责:

  • 初始化流程:上电时序、寄存器配置、校准参数加载
  • 数据采集:帧同步、数据读取、格式转换
  • 图像处理:非均匀性校正、坏像素替换、温度补偿
  • 状态管理:工作模式切换、错误处理、功耗控制

这里要注意,驱动层只调用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寄存器,结果平台一换,代码全废了。记住:应用层只能调用驱动层接口

模块划分:怎么切才合理?

模块划分没有标准答案,但我有个经验法则:按功能职责切,不按文件切

一个典型的红外探测器驱动,可以分成这些模块:

模块名 职责 所属层
hal_i2c I2C总线读写,适配不同MCU HAL层
hal_gpio GPIO控制,中断管理 HAL层
hal_timer 定时器、延时、时间戳 HAL层
drv_sensor 探测器初始化、寄存器配置 驱动层
drv_frame 帧数据采集、缓存管理 驱动层
drv_correction 非均匀性校正、坏像素处理 驱动层
drv_temp 温度读取、温度补偿 驱动层
app_main 用户接口、命令解析 应用层
app_protocol 数据打包、通信协议 应用层

接口定义规范:约定大于配置

接口规范是团队协作的「宪法」。我总结了几个关键点:

  1. 命名统一:HAL层用 hal_ 前缀,驱动层用 drv_ 前缀,应用层用 app_ 前缀。一看就知道属于哪一层。
  2. 返回值规范:统一用 int32_t 返回,0表示成功,负数表示错误码。正数用于特殊状态。
  3. 参数传递:尽量用指针传递大数据块,避免值拷贝。但要注意指针有效性检查。
  4. 回调机制:驱动层通过回调通知应用层,而不是轮询。这样效率高,也符合事件驱动思想。
避坑指南:我曾经在接口里混用了 intuint8_tint32_t,结果在32位和16位平台上出现隐式类型转换问题。后来我强制规定:所有接口参数和返回值,必须显式指定宽度,比如 uint8_tint32_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