3、传感器驱动开发(I2C):I2C总线协议回顾、PX4 I2C驱动框架、编写一个I2C温度传感器驱动

各位同学,欢迎来到第三章。

这一章我们聊聊I2C传感器驱动。说实话,I2C在飞控里用得特别多——气压计、磁力计、部分IMU,都是走I2C的。你想想看,一个飞控板上可能挂着四五个I2C设备,如果驱动写得不好,总线冲突、数据错乱、启动失败……这些坑我都踩过。

今天我们就拿一个温度传感器来练手。从协议回顾,到PX4的驱动框架,再到手写一个完整驱动,一步步来。

3.1 I2C总线协议回顾

先快速过一遍I2C的核心要点。如果你已经很熟了,可以跳过这节。但我建议你还是扫一眼,因为后面写驱动时,很多细节会跟协议强相关。

3.1.1 物理层与电气特性

  • 两线制:SCL(时钟线)和SDA(数据线),都是开漏输出,需要上拉电阻。
  • 地址:7位或10位地址。飞控里99%是7位地址,比如0x76、0x77。
  • 速率:标准模式100kHz,快速模式400kHz。PX4默认用400kHz,但有些传感器扛不住,得降速。
注意: 我曾经遇到过一块传感器,手册写支持400kHz,结果在高温下频繁丢ACK。后来降到100kHz才稳定。所以量产时,建议留个降速的接口。

3.1.2 通信时序

I2C的通信流程其实很简单:

  1. 主机发送START条件(SCL高时SDA从高变低)
  2. 主机发送7位地址 + 读写位(0写,1读)
  3. 从机回复ACK(拉低SDA)
  4. 传输数据:每字节后跟一个ACK
  5. 主机发送STOP条件(SCL高时SDA从低变高)

嗯,这里要注意:读操作中间通常要加一个RESTART条件,而不是STOP再START。很多新手在这里写错,导致读出来的数据全是0xFF。

3.1.3 寄存器读写模式

大多数I2C传感器都采用寄存器映射的方式。写一个寄存器:

START + 设备地址(写) + ACK + 寄存器地址 + ACK + 数据 + ACK + STOP

读一个寄存器:

START + 设备地址(写) + ACK + 寄存器地址 + ACK + 
RESTART + 设备地址(读) + ACK + 数据 + NACK + STOP

说白了,就是先告诉传感器你要读哪个寄存器,然后再重新发起一次读操作。我刚开始写驱动时,忘了加RESTART,结果读回来的数据永远是上一个寄存器的值。排查了半天,最后用逻辑分析仪才看到问题。

3.2 PX4 I2C驱动框架

PX4的I2C驱动框架,核心就两个东西:I2C基类和I2CSPIDriver基类。我们重点讲I2C基类,因为它是所有I2C驱动的根。

3.2.1 I2C基类

定义在 src/lib/drivers/device/i2c.h 里。关键成员:

  • _device_id:设备ID,用于uORB消息标识
  • _address:7位I2C地址
  • _bus:总线编号(比如内部总线是1,外部是2)
  • _retries:重试次数,默认3次

核心方法就两个:

int transfer(const uint8_t *send, unsigned send_len,
             uint8_t *recv, unsigned recv_len);
int read_reg(uint8_t reg, uint8_t *data, unsigned len);
int write_reg(uint8_t reg, uint8_t data);

transfer是底层接口,直接操作I2C总线。read_regwrite_reg是封装好的便捷函数。我个人习惯是直接用read_regwrite_reg,除非你需要自定义时序。

3.2.2 驱动生命周期

一个标准的PX4 I2C驱动,生命周期是这样的:

  1. 探测(probe):读取芯片ID寄存器,确认设备存在
  2. 初始化(init):配置传感器工作模式、量程、采样率
  3. 循环读取(cycle):定时读取数据,发布到uORB
  4. 错误处理:通信失败时重试或重启

关键点: probe阶段一定要做。我见过有人跳过probe,结果驱动加载到了错误的地址上,数据全是乱的。probe就是你的第一道防线。

3.2.3 调度方式

PX4支持两种调度方式:

  • 轮询模式:在cycle()里主动调用ScheduleNow()ScheduleDelayed()
  • 中断模式:传感器通过DRDY引脚通知,驱动在中断处理中读取

温度传感器一般用轮询就够了,100ms读一次,完全够用。

3.3 编写一个I2C温度传感器驱动

好,理论说完了,我们动手写一个。选一个常见的温度传感器——TMP102。它便宜、简单、寄存器少,非常适合教学。

3.3.1 传感器简介

参数
I2C地址0x48(AD0接地时)
温度寄存器0x00,16位,左对齐
配置寄存器0x01,控制转换模式
分辨率12位(0.0625°C)
转换时间典型26ms

3.3.2 驱动文件结构

src/drivers/temperature/tmp102/ 下创建:

  • CMakeLists.txt:构建文件
  • tmp102.hpp:头文件
  • tmp102.cpp:实现文件

3.3.3 头文件设计

#pragma once

#include <drivers/device/i2c.h>
#include <uORB/topics/sensor_temperature.h>

class TMP102 : public device::I2C
{
public:
    TMP102(int bus, int address);
    virtual ~TMP102() = default;

    int init() override;
    int probe() override;
    void cycle() override;

private:
    int _measure();
    float _convert_raw_to_celsius(uint16_t raw);

    int _read_reg(uint8_t reg, uint16_t &val);
    int _write_reg(uint8_t reg, uint16_t val);

    int _retries{3};
    orb_advert_t _temperature_pub{nullptr};
};

注意看,我继承了device::I2C,然后重写了init()probe()cycle()。这是PX4驱动最标准的写法。

3.3.4 probe实现

int TMP102::probe()
{
    uint16_t id = 0;
    int ret = _read_reg(0x00, id);  // 读温度寄存器,看能否通信

    if (ret != PX4_OK) {
        PX4_ERR("TMP102 probe failed: no ACK");
        return -EIO;
    }

    // TMP102没有专门的ID寄存器,我们通过读温度值来验证
    // 如果读回来的值在合理范围内,认为设备存在
    if (id == 0xFFFF || id == 0x0000) {
        PX4_ERR("TMP102 probe failed: invalid data");
        return -ENODEV;
    }

    PX4_INFO("TMP102 detected on bus %d", _bus);
    return PX4_OK;
}

这里有个小技巧:TMP102没有芯片ID寄存器,所以用读温度值来验证。如果读回来全是0或全是1,说明总线有问题或者地址不对。

3.3.5 init实现

int TMP102::init()
{
    int ret = I2C::init();  // 调用基类init,设置总线速率等

    if (ret != PX4_OK) {
        return ret;
    }

    // 配置寄存器:连续转换模式,12位分辨率
    uint16_t config = 0x60A0;  // 连续转换,CR1=0, CR0=0
    ret = _write_reg(0x01, config);

    if (ret != PX4_OK) {
        PX4_ERR("TMP102 config failed");
        return ret;
    }

    // 启动循环,100ms间隔
    ScheduleOnInterval(100000);  // 单位微秒

    return PX4_OK;
}

嗯,这里要注意:ScheduleOnInterval的参数是微秒。100ms就是100000微秒。我刚开始写的时候写成了100,结果驱动跑得跟疯了一样,CPU占用率直接拉满。

3.3.6 cycle实现

void TMP102::cycle()
{
    uint16_t raw_temp = 0;
    int ret = _read_reg(0x00, raw_temp);

    if (ret != PX4_OK) {
        PX4_ERR("TMP102 read failed, retrying...");
        return;
    }

    float temp_c = _convert_raw_to_celsius(raw_temp);

    // 发布到uORB
    sensor_temperature_s report{};
    report.temperature = temp_c;
    report.timestamp = hrt_absolute_time();

    if (_temperature_pub == nullptr) {
        _temperature_pub = orb_advertise(ORB_ID(sensor_temperature), &report);
    } else {
        orb_publish(ORB_ID(sensor_temperature), _temperature_pub, &report);
    }
}

float TMP102::_convert_raw_to_celsius(uint16_t raw)
{
    // TMP102数据是12位左对齐,右移4位得到12位值
    int16_t temp_raw = static_cast<int16_t>(raw) >> 4;
    return temp_raw * 0.0625f;
}
经验之谈: 温度转换公式一定要对照数据手册验证。我遇到过有人把左对齐当右对齐处理,结果温度差了16倍。用逻辑分析仪抓一下原始数据,跟手册对一遍,最保险。

3.3.7 私有读写函数

int TMP102::_read_reg(uint8_t reg, uint16_t &val)
{
    uint8_t buf[2] = {0};
    int ret = read_reg(reg, buf, 2);

    if (ret == PX4_OK) {
        val = (buf[0] << 8) | buf[1];
    }

    return ret;
}

int TMP102::_write_reg(uint8_t reg, uint16_t val)
{
    uint8_t buf[2] = {
        static_cast<uint8_t>(val >> 8),
        static_cast<uint8_t>(val & 0xFF)
    };
    return write_reg(reg, buf, 2);
}

这里我封装了一层,把16位寄存器的读写包装成函数。这样上层代码更干净,也方便以后扩展。

3.4 驱动调试与验证

驱动写完了,怎么验证对不对?我一般按这个顺序来:

  1. i2cdetect:先确认I2C地址对不对。在nsh终端运行 i2cdetect -b 1,看0x48有没有出现。
  2. 手动读写:用 i2cgeti2cset 直接操作寄存器,验证通信正常。
  3. 驱动加载:insmod驱动,看probe是否成功。
  4. uORB监听:用 listener sensor_temperature 看数据是否更新。
  5. 对比验证:用另一个温度计(比如BMP280的温度)对比,看差值是否在合理范围内。
避坑指南: 我曾经在调试时发现温度值跳动很大,排查了半天,结果是电源纹波太大。传感器对电源很敏感,尤其是模拟输出的传感器。建议在传感器电源引脚加一个10uF+0.1uF的去耦电容。

3.5 本章小结

这一章我们做了三件事:

  • 回顾了I2C协议的核心要点,特别是寄存器读写时序
  • 分析了PX4的I2C驱动框架,重点讲了I2C基类和生命周期
  • 手写了一个TMP102温度传感器驱动,从probe到cycle完整实现

说实话,I2C驱动在PX4里算是比较简单的。但简单不代表可以马虎。probe一定要做,错误处理一定要有,数据转换一定要验证。这些习惯养成了,后面写SPI驱动、UART驱动,都会顺手很多。

好,这一章就到这里。代码我已经放在课程仓库里了,你们可以拉下来编译试试。有问题随时在群里问我。


专注资料整理