I2C设备驱动模型:从uORB到设备注册

好,咱们直接进入正题。这一章我打算聊聊PX4里I2C设备驱动的核心模型。说实话,我刚接触PX4那会儿,最头疼的就是搞不清uORB、设备注册、探测初始化这些概念到底怎么串起来的。你想想看,一个传感器从硬件上电到最终输出数据,中间要经过多少环节?

我个人习惯把整个流程拆成三块来看:消息机制、设备生命周期、探测与初始化。这三块搞明白了,写驱动就是套模板的事。

核心观点:PX4的I2C驱动本质上是「硬件操作 + uORB消息发布」的组合。驱动不直接跟应用层对话,而是通过uORB这个「消息总线」来传递数据。

uORB消息机制:驱动与应用之间的桥梁

uORB是什么?说白了就是一个轻量级的发布-订阅消息系统。驱动采集到数据后,往某个主题(topic)上发布消息;其他模块(比如姿态估计、位置控制)订阅这个主题,就能拿到数据。

我在项目中遇到过一个问题:两个传感器都发布同样的主题,结果数据打架了。后来才意识到,uORB的发布者ID必须唯一。嗯,这里要注意,每个驱动实例都要有独立的orb_id。

常用的传感器uORB主题有这些:

主题名称 数据结构 典型传感器
sensor_accel sensor_accel_s 加速度计
sensor_gyro sensor_gyro_s 陀螺仪
sensor_mag sensor_mag_s 磁力计
sensor_baro sensor_baro_s 气压计
distance_sensor distance_sensor_s 超声波/激光测距

发布消息的代码其实很简单,核心就三步:

// 1. 定义消息结构体
struct sensor_accel_s accel_data = {};

// 2. 填充数据
accel_data.timestamp = hrt_absolute_time();
accel_data.x = raw_x;
accel_data.y = raw_y;
accel_data.z = raw_z;

// 3. 发布到uORB
orb_publish(orb_id(sensor_accel), _pub, &accel_data);

你可能会问:为什么不直接函数调用?因为uORB解耦了生产者和消费者。驱动挂了,应用层不会直接崩溃;应用层换了,驱动也不用改。这就是消息机制的价值。

设备驱动注册与注销流程

PX4的设备驱动遵循Linux的设备模型思路——注册、探测、注销。但实现上更轻量,毕竟飞控的MCU资源有限。

我建议你把驱动看作一个「类」,注册就是实例化。每个I2C设备对应一个驱动对象,这个对象继承自I2C基类。

注册流程大致如下:

  1. 驱动入口:通常是module_start函数,被调度器调用
  2. 实例化驱动对象:new一个驱动类,传入I2C总线号、设备地址
  3. 探测设备:调用probe方法,确认设备是否存在
  4. 初始化设备:配置寄存器、设置采样率、校准等
  5. 注册到设备框架:调用register_driver,让系统知道这个设备
  6. 启动数据采集:开启定时器或中断,循环读取数据

注销流程正好反过来:

  1. 停止数据采集(关闭定时器/中断)
  2. 从设备框架注销(unregister_driver
  3. 释放驱动对象(delete)

我的经验:注销流程最容易出问题的是资源释放顺序。我曾经在关闭定时器之后才释放I2C总线,结果定时器回调里还在操作总线,直接hardfault。记住:先停中断,再释放资源。

I2C设备探测与初始化

探测(probe)是驱动里最关键的环节。说白了就是:我怎么知道这个I2C地址上真的有设备?

常用的探测方法有:

  • 读设备ID寄存器:大多数传感器都有WHO_AM_I或CHIP_ID寄存器,读出来跟预期值比对
  • 写读验证:写一个已知值到某个寄存器,再读回来确认
  • ACK检测:发送设备地址后检查是否有ACK响应(最基础的方式)

我建议优先用读ID寄存器的方式,最可靠。我曾经遇到一个坑:某个传感器在ACK检测时能响应,但读ID寄存器返回0xFF,后来发现是电源没稳定。所以探测时最好加个重试机制。

初始化部分通常包括:

  • 复位设备(软复位或硬件复位)
  • 配置工作模式(连续采集、单次触发等)
  • 设置采样率和滤波器
  • 使能数据就绪中断(如果需要)
  • 校准(偏移、增益等)

来看一个典型的探测初始化代码片段:

int MySensor::probe()
{
    // 读WHO_AM_I寄存器
    uint8_t id = 0;
    int ret = read_reg(REG_WHO_AM_I, &id, 1);
    
    if (ret != OK) {
        PX4_ERR("probe failed: no ACK");
        return -EIO;
    }
    
    if (id != EXPECTED_ID) {
        PX4_ERR("probe failed: wrong ID 0x%02x", id);
        return -ENODEV;
    }
    
    PX4_INFO("sensor detected, ID=0x%02x", id);
    return OK;
}

int MySensor::init()
{
    // 先探测
    int ret = probe();
    if (ret != OK) return ret;
    
    // 软复位
    write_reg(REG_RESET, 0x01);
    usleep(10000); // 等待复位完成
    
    // 配置采样率
    write_reg(REG_CTRL1, 0x60); // 100Hz, 使能所有轴
    
    // 配置滤波器
    write_reg(REG_CTRL2, 0x04); // 50Hz低通滤波
    
    // 使能数据就绪中断
    write_reg(REG_CTRL3, 0x01);
    
    return OK;
}

注意:I2C通信有速率限制。PX4默认的I2C总线频率通常是100kHz或400kHz。如果你的传感器需要高速通信,记得检查总线时钟配置。另外,I2C的pull-up电阻也很关键,我遇到过因为电阻太大导致通信不稳定的情况。

知识体系总览

下面这张图是我自己整理的I2C驱动核心逻辑,你可以对照着理解:

PX4 I2C设备驱动核心逻辑 硬件层 I2C总线 · 传感器芯片 · 中断引脚 · 电源管理 驱动层 探测(probe) 读ID · ACK检测 初始化(init) 配置寄存器 · 校准 数据采集 定时器/中断 · 读取 消息层 (uORB) sensor_accel · sensor_gyro · sensor_mag · 自定义主题 数据流向:硬件 → 驱动(探测→初始化→采集) → uORB消息 → 应用层 注册/注销

从这张图你能看到,整个流程是单向的:硬件产生原始数据,驱动负责采集和转换,最后通过uORB发布出去。注册和注销贯穿驱动层的整个生命周期。

好了,这一章的内容就这些。uORB消息机制、设备注册注销、探测初始化,这三块是I2C驱动的基石。下一章我会拿一个具体的传感器(比如MS5611气压计)来演示完整的驱动开发过程,到时候你就知道这些概念怎么落地了。


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