4、编写第一个I2C传感器驱动(以BMP280为例)

好,咱们今天来点真格的。

前面几章讲了PX4的驱动架构、调度器、uORB这些基础概念。说实话,光看理论容易犯困。我当年学PX4的时候,也是看了好几遍文档,感觉懂了,一上手写代码就懵了。

所以这一章,咱们直接拿一个真实的传感器——BMP280,手把手写一个I2C驱动。你跟着我走一遍,后面再写其他传感器,套路基本都一样。

4.1 硬件连接:先别急着写代码

写驱动之前,第一件事是什么?不是翻数据手册,是看硬件怎么接的。

我个人习惯,先确认三件事:

  • 供电:BMP280支持1.71V~3.6V,大部分飞控板用3.3V。注意别接到5V上,会烧。
  • I2C总线:SDA和SCL,确认挂在哪条总线上。比如STM32F4的I2C1还是I2C2。
  • 地址:BMP280的I2C地址由SDO引脚决定。接地是0x76,接VDDIO是0x77。
注意: 我曾经遇到过一块飞控板,BMP280的SDO默认接了上拉,地址是0x77。结果我按数据手册默认0x76去写,死活读不到数据。排查了半天,最后拿万用表一量才发现。所以,硬件连接一定要先确认,别想当然。

硬件确认好了,咱们再看数据手册。

4.2 数据手册解读:只看关键部分

BMP280的数据手册有50多页,你不需要全看。我一般只看这几块:

章节 内容 重要性
1. 产品概述 功能框图、引脚定义 必看
2. 电气特性 供电电压、I2C时序 必看
3. 寄存器映射 所有寄存器的地址和含义 核心
4. 操作模式 正常模式、睡眠模式、强制模式 必看
5. 校准系数 用于补偿温度和压力 核心

说白了,你写驱动只需要知道三件事:

  1. 怎么初始化:写哪些寄存器,写什么值。
  2. 怎么读数据:读哪些寄存器,数据格式是什么。
  3. 怎么校准:用校准系数把原始数据转成物理值。

嗯,就这么多。别被数据手册吓到。

4.3 驱动框架选择:PX4里怎么写?

PX4的传感器驱动,官方推荐用I2CSPIDriver框架。这个框架封装了I2C和SPI的通用操作,你只需要实现几个关键函数:

  • probe():探测设备是否存在
  • init():初始化传感器
  • read():读取传感器数据

你想想看,这个框架把底层的I2C读写、调度、uORB发布都帮你搞定了。你只需要关注传感器本身的操作。是不是很省事?

核心思路: 驱动 = 探测 + 初始化 + 循环读取。就这么简单。

4.4 实现probe函数:先确认设备在不在

probe()是驱动的第一步。它的任务很简单:往I2C总线上发一个读ID的命令,看看设备有没有回应。

BMP280有一个ID寄存器,地址是0xD0,读出来应该是0x58。

int BMP280::probe()
{
    // 读取芯片ID
    uint8_t id;
    int ret = readRegister(0xD0, &id, 1);
    
    if (ret != OK) {
        PX4_ERR("BMP280 probe failed: no response");
        return -EIO;
    }
    
    if (id != 0x58) {
        PX4_ERR("BMP280 wrong ID: 0x%02X", id);
        return -ENODEV;
    }
    
    PX4_INFO("BMP280 found, ID: 0x%02X", id);
    return OK;
}

这里有个小细节:readRegister()是框架提供的封装函数,它会自动处理I2C的写地址+读数据的时序。你不需要自己拼I2C消息。

小技巧: 我习惯在probe成功时打印一条INFO日志。这样调试的时候,一看启动日志就知道传感器有没有被正确识别。

4.5 实现init函数:让传感器开始工作

probe通过后,就该初始化了。BMP280的初始化其实很简单:

  1. 读取校准系数(存储在OTP寄存器中)
  2. 设置工作模式(正常模式、采样率等)

校准系数有12个,每个都是16位有符号整数。它们存储在寄存器0x88到0xA1之间。我建议一次性读出来,存到结构体里。

int BMP280::init()
{
    int ret;
    
    // 1. 读取校准系数
    struct bmp280_calib_s calib;
    ret = readRegister(0x88, (uint8_t *)&calib, sizeof(calib));
    if (ret != OK) {
        PX4_ERR("read calibration failed");
        return ret;
    }
    
    // 2. 设置配置寄存器
    // 0x2C = 0b00101100
    // bit7-5: t_sb=000 (0.5ms standby)
    // bit4-2: filter=100 (16x filter)
    // bit0: spi3w_en=0 (4-wire SPI)
    ret = writeRegister(0xF5, 0x2C);
    if (ret != OK) return ret;
    
    // 3. 设置测量控制寄存器
    // 0x3F = 0b00111111
    // bit7-5: osrs_t=001 (1x oversampling)
    // bit4-2: osrs_p=111 (16x oversampling)
    // bit1-0: mode=11 (normal mode)
    ret = writeRegister(0xF4, 0x3F);
    if (ret != OK) return ret;
    
    PX4_INFO("BMP280 initialized");
    return OK;
}

你可能会问:为什么配置值是0x2C和0x3F?这些值是我根据数据手册的推荐配置算出来的。说白了,就是采样率、滤波、功耗之间的平衡。我一般选一个中等配置,够用就行。

注意: 写寄存器之后,建议加一个延时。BMP280在模式切换后需要一段时间稳定。我一般加10ms的延时。曾经有一次我忘了加,读出来的数据全是0,排查了半天才发现是时序问题。

4.6 实现read函数:拿到温度和压力

初始化完成后,传感器会持续测量。你只需要定时去读结果寄存器就行。

BMP280的压力和温度数据分别存储在:

  • 压力:寄存器0xF7 (MSB), 0xF8 (LSB), 0xF9 (XLSB)
  • 温度:寄存器0xFA (MSB), 0xFB (LSB), 0xFC (XLSB)

每个值都是20位有符号整数,左对齐。读取后需要右移4位得到真正的20位值。

int BMP280::read()
{
    uint8_t data[6];
    int ret = readRegister(0xF7, data, 6);
    if (ret != OK) {
        return ret;
    }
    
    // 解析原始数据
    int32_t raw_press = ((int32_t)data[0] << 12) | 
                        ((int32_t)data[1] << 4) | 
                        ((int32_t)data[2] >> 4);
    
    int32_t raw_temp = ((int32_t)data[3] << 12) | 
                       ((int32_t)data[4] << 4) | 
                       ((int32_t)data[5] >> 4);
    
    // 用校准系数补偿
    float temperature = compensate_temperature(raw_temp);
    float pressure = compensate_pressure(raw_press);
    
    // 发布到uORB
    sensor_baro_s baro_report = {};
    baro_report.timestamp = hrt_absolute_time();
    baro_report.pressure = pressure;
    baro_report.temperature = temperature;
    
    _baro_pub.publish(baro_report);
    
    return OK;
}

补偿函数的具体实现,数据手册里给了公式。说白了就是一堆乘法和加法,把原始值转成摄氏度和帕斯卡。代码我就不贴了,你照着数据手册抄就行。

经验之谈: 补偿计算建议用浮点数。虽然嵌入式里浮点运算慢一点,但BMP280的采样率一般只有几十Hz,CPU完全扛得住。用整数运算虽然快,但容易溢出,调试起来很麻烦。

4.7 完整驱动结构图

下面这张图,是我画的一个驱动整体流程。你看一眼,心里就有数了。

BMP280 I2C驱动整体流程 1. 硬件连接 确认供电、I2C总线、地址 2. 数据手册解读 寄存器映射、校准系数 3. 驱动框架选择 I2CSPIDriver框架 4. probe() 探测设备 读ID寄存器(0xD0) → 确认0x58 5. init() 初始化 读校准系数 → 配置寄存器 6. read() 循环读取 读原始数据 → 补偿 → 发布uORB

你看,整个流程就是一条直线。从硬件确认,到数据手册,再到三个核心函数。没有复杂的跳转,没有花哨的技巧。

4.8 总结一下

写一个I2C传感器驱动,说白了就是三步:

  • probe:确认设备在不在
  • init:告诉设备怎么工作
  • read:定时拿数据,补偿,发出去

你把这个套路记住了,以后写任何I2C传感器,无非就是换一组寄存器地址、换一套补偿公式。框架和流程完全一样。

嗯,这一章就到这儿。代码你可以直接拿去用,但建议自己手敲一遍。只有亲手写过,才能真正理解。


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