4. 传感器驱动开发:I2C读写函数封装、传感器初始化代码、像素数据读取与校验、帧同步机制

好,咱们进入第四章。这一章,说白了就是让传感器「开口说话」。

你想想看,热成像传感器再厉害,它也就是个黑盒子。你得通过 I2C 总线跟它握手,告诉它「兄弟,开始工作」,然后它把一帧一帧的温度数据吐给你。这个过程,就是驱动开发的核心。

我个人习惯把驱动拆成四块:I2C 读写封装、传感器初始化、像素数据读取与校验、帧同步机制。咱们一块一块啃。

4.1 I2C 读写函数封装

热成像传感器,比如 MLX90640、AMG8833,基本都是 I2C 接口。I2C 协议本身不复杂,但坑不少。

我建议你不要直接调用 HAL 库的 I2C 函数。为什么?因为项目后期要移植到不同 MCU,或者要加调试信息,你一个个改会疯掉。

正确的做法是:封装一层

/* i2c_drv.h */
typedef struct {
    int (*read)(uint8_t dev_addr, uint16_t reg_addr, uint8_t *data, uint16_t len);
    int (*write)(uint8_t dev_addr, uint16_t reg_addr, uint8_t *data, uint16_t len);
} i2c_bus_t;

/* 具体实现(以 STM32 HAL 为例) */
static int i2c_read_hal(uint8_t dev_addr, uint16_t reg_addr, uint8_t *data, uint16_t len) {
    HAL_I2C_Mem_Read(&hi2c1, dev_addr << 1, reg_addr, I2C_MEMADD_SIZE_16BIT, data, len, 100);
    return 0;
}

static int i2c_write_hal(uint8_t dev_addr, uint16_t reg_addr, uint8_t *data, uint16_t len) {
    HAL_I2C_Mem_Write(&hi2c1, dev_addr << 1, reg_addr, I2C_MEMADD_SIZE_16BIT, data, len, 100);
    return 0;
}

i2c_bus_t i2c_bus = {
    .read = i2c_read_hal,
    .write = i2c_write_hal
};
我的经验: 封装时一定要加超时处理。我曾经在一个项目中,传感器偶尔会挂死,I2C 一直拉低 SCL 线。如果没有超时,整个系统就卡死了。加个 100ms 超时,挂死后复位一下总线,问题就解决了。

4.2 传感器初始化代码

初始化,说白了就是往寄存器里写一堆配置值。但这里有个关键点:不同传感器的初始化流程完全不同

以 MLX90640 为例,它的初始化步骤大致是:

  1. 读取芯片 ID,确认通信正常
  2. 设置刷新率(0.5Hz ~ 64Hz)
  3. 配置 ADC 分辨率
  4. 加载出厂校准参数(EEPROM)
  5. 启动连续采集模式
int mlx90640_init(i2c_bus_t *bus, uint8_t addr) {
    uint16_t id;
    int ret;

    /* 1. 读取芯片 ID,验证通信 */
    ret = bus->read(addr, 0x2400, (uint8_t *)&id, 2);
    if (ret != 0 || id != 0x2400) {
        return -1;  /* 通信失败 */
    }

    /* 2. 设置刷新率为 8Hz */
    uint16_t ctrl_reg = 0x0000;
    ctrl_reg |= (0x03 << 4);  /* 8Hz */
    bus->write(addr, 0x800D, (uint8_t *)&ctrl_reg, 2);

    /* 3. 配置 ADC 为 18 位模式 */
    uint16_t adc_cfg = 0x0400;
    bus->write(addr, 0x800E, (uint8_t *)&adc_cfg, 2);

    /* 4. 加载 EEPROM 校准参数(略,后续章节详述) */
    /* 5. 启动连续采集 */
    ctrl_reg |= 0x0001;  /* 使能采集 */
    bus->write(addr, 0x800D, (uint8_t *)&ctrl_reg, 2);

    return 0;
}
注意: 初始化完成后,不要立即读取数据。传感器需要一段「稳定时间」,通常是 2~5 帧的时间。我见过有人初始化完就立刻读数据,结果读出来全是 0。加个 500ms 延时,或者等帧同步信号来了再读,就稳了。

4.3 像素数据读取与校验

热成像传感器的像素数据,通常以 16 位有符号整数存储。比如 MLX90640 有 32x24 = 768 个像素,每个像素 2 字节,一帧就是 1536 字节。

读取方式有两种:

  • 逐行读取: 每次读一行(32 像素),共读 24 次。适合内存小的 MCU。
  • 整帧读取: 一次性读 1536 字节。速度快,但需要大缓冲区。

我个人推荐整帧读取,因为 I2C 每次启动都有开销,整帧读效率更高。

int mlx90640_read_frame(i2c_bus_t *bus, uint8_t addr, int16_t *frame_data) {
    uint8_t raw[1664];  /* 1536 + 128 额外校验数据 */
    int ret;

    ret = bus->read(addr, 0x0400, raw, 1664);
    if (ret != 0) {
        return -1;
    }

    /* 数据校验:检查帧尾的 CRC */
    uint16_t crc_calc = calc_crc16(raw, 1662);
    uint16_t crc_recv = (raw[1662] << 8) | raw[1663];
    if (crc_calc != crc_recv) {
        return -2;  /* CRC 校验失败 */
    }

    /* 提取像素数据(小端格式) */
    for (int i = 0; i < 768; i++) {
        frame_data[i] = (raw[i*2 + 1] << 8) | raw[i*2];
    }

    return 0;
}
关键点: 数据校验不能省。热成像数据用于测温,一个像素错了可能就导致误报。CRC 校验是底线。我遇到过 I2C 总线受干扰,数据中间跳变,CRC 直接帮我拦住了 3 次错误帧。

4.4 帧同步机制

帧同步,说白了就是「什么时候读数据是完整的」。

热成像传感器有两种帧同步方式:

方式 原理 优点 缺点
查询状态寄存器 轮询传感器状态位,检测新帧就绪 实现简单 占用 CPU,可能丢帧
硬件中断(INT 引脚) 传感器输出脉冲,触发 MCU 中断 实时性好,不占 CPU 需要额外引脚

我建议量产项目用硬件中断。为什么?因为轮询方式在帧率较高时(比如 32Hz),CPU 大部分时间都在查状态,干不了别的。

/* 中断服务函数示例 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == THERMAL_INT_PIN) {
        /* 设置标志位,主循环中处理 */
        frame_ready = 1;
    }
}

/* 主循环 */
while (1) {
    if (frame_ready) {
        frame_ready = 0;
        mlx90640_read_frame(&i2c_bus, 0x33, frame_buffer);
        process_frame(frame_buffer);  /* 温度计算、显示等 */
    }
}
避坑指南: 我曾经在一个项目中,中断引脚没有做去抖处理。传感器在帧切换时,INT 引脚会抖动,导致一次帧同步触发了多次中断。后来我在中断里加了个 1ms 的延时去抖,问题就解决了。嗯,细节决定成败。

4.5 本章知识体系

下面这张图,是我梳理的本章核心逻辑。你一看就明白:

传感器驱动开发核心流程 I2C 读写封装 抽象层 + 超时处理 传感器初始化 配置 + 校准加载 像素数据读取 整帧读取 + CRC 校验 帧同步 帧同步两种方式对比 方式一:查询状态寄存器 • 轮询传感器状态位 • 实现简单,无需额外引脚 • 占用 CPU,高帧率下效率低 方式二:硬件中断(推荐) • INT 引脚触发中断 • 实时性好,不占 CPU • 需额外引脚,注意去抖

这张图把整个流程串起来了。从 I2C 封装开始,到初始化、数据读取,最后用帧同步收尾。每一步都有坑,但只要你按这个框架走,基本不会出大问题。

好了,这一章就到这里。记住:驱动开发是硬件和软件的桥梁,写得好,后面所有上层应用都稳;写得烂,后面全是坑。


公众号:蓝海资料掘金营,微信deep3321