4. 传感器驱动模型:PX4驱动分层与设备注册探测

好,咱们今天聊聊PX4的传感器驱动模型。说实话,这部分内容是我当年刚接触PX4时最头疼的一块。代码结构看着挺清晰,但真要自己写一个驱动,总觉得无从下手。后来我花了一周时间,把整个I2C磁力计的驱动流程走了一遍,才算真正摸透了。

说白了,PX4的驱动模型就是一套标准化的“接入流程”。不管你是I2C、SPI还是UART的传感器,都得按照这个流程来。这样做的好处很明显——代码复用率高,维护起来也省心。

4.1 PX4驱动分层架构

先看整体架构。PX4的驱动分三层,我习惯这么理解:

  • 总线层(Bus):负责和硬件打交道。读写寄存器、控制片选、处理中断,这些都是它的活。
  • 设备层(Device):封装具体的传感器操作。比如读陀螺仪数据、配置加速度计量程。
  • 驱动层(Driver):对接PX4的uORB消息系统。把传感器数据打包成话题,供飞控使用。

嗯,这里要注意:三层之间是单向依赖的。驱动层调用设备层,设备层调用总线层。反过来不行。

核心原则:总线层只负责“怎么读写”,设备层负责“读写什么”,驱动层负责“数据怎么用”。各司其职,别越界。

4.2 总线接口:I2C、SPI、UART

这三种总线,我在项目中都踩过坑。一个一个说。

4.2.1 I2C总线

I2C的特点是引脚少(SCL、SDA),但速度慢。PX4里I2C驱动通常用于磁力计、气压计这类低速传感器。

我曾经在一个项目中,I2C总线老是超时。查了半天,发现是总线电容太大,上拉电阻没选对。后来换了4.7kΩ的电阻,问题解决。

// I2C设备注册示例
class BMI088_I2C : public BMI088
{
public:
    BMI088_I2C(const I2CSPIDriverConfig &config);
    // ...
};

// 在探测函数中
int BMI088_I2C::probe()
{
    // 读取芯片ID
    uint8_t id = 0;
    int ret = read(REG_CHIP_ID, &id, 1);
    
    if (ret != OK || id != EXPECTED_ID) {
        PX4_ERR("chip id mismatch: %02x", id);
        return -EIO;
    }
    
    return OK;
}

4.2.2 SPI总线

SPI速度快,全双工,适合IMU这类高速传感器。但SPI的接线多(MOSI、MISO、SCLK、CS),每个设备还要独占一个片选。

我记得有一次调试SPI陀螺仪,数据总是跳变。用示波器一看,MISO线上的信号有毛刺。原来是SPI时钟频率设得太高,降了一档就好了。

小技巧:SPI通信时,片选信号拉低后,最好加一小段延时再开始传数据。有些传感器对片选建立时间有要求。

4.2.3 UART总线

UART常用于GPS、激光雷达这类外部设备。PX4里UART驱动相对简单,主要是配置波特率、数据位、停止位。

你想想看,UART没有时钟线,全靠双方约定速率。所以波特率必须一致,否则数据全是乱码。我刚开始做时,就犯过这个低级错误。

4.3 设备注册与探测流程

这部分是驱动开发的核心。PX4有一套标准的探测流程,我画了个图,方便你理解:

PX4传感器设备注册与探测流程 1. 驱动入口:module_start() 2. 创建I2CSPIDriver实例 3. 调用probe()探测设备 4. 探测成功? 成功 5. 注册到uORB,开始发布数据 失败 5. 清理资源,返回错误

这个流程看着简单,但每个步骤都有讲究。我重点说说probe()函数。

4.3.1 probe()函数怎么写

probe()的核心任务就是确认设备是否存在、通信是否正常。通常的做法是读芯片ID寄存器。

int MySensor::probe()
{
    // 第一步:复位传感器(可选)
    write(REG_RESET, 0xB6);
    usleep(10000);  // 等10ms
    
    // 第二步:读芯片ID
    uint8_t chip_id = 0;
    int ret = read(REG_CHIP_ID, &chip_id, 1);
    
    if (ret != OK) {
        PX4_ERR("read chip id failed");
        return -EIO;
    }
    
    // 第三步:验证ID
    if (chip_id != MY_SENSOR_CHIP_ID) {
        PX4_ERR("chip id mismatch: got 0x%02x, expected 0x%02x", 
                chip_id, MY_SENSOR_CHIP_ID);
        return -EINVAL;
    }
    
    PX4_INFO("sensor detected, chip id: 0x%02x", chip_id);
    return OK;
}

避坑指南:我曾经在probe()里忘了加延时,结果复位后立刻读ID,读回来的全是0xFF。后来加了10ms延时,问题解决。记住:传感器复位需要时间,别急着读。

4.3.2 设备注册的完整流程

从module_start()到设备真正开始工作,中间还有几个关键步骤:

  1. 解析参数:从启动脚本中获取总线号、设备地址、频率等参数。
  2. 创建驱动实例:调用new操作符,分配内存。
  3. 调用init():初始化硬件,配置寄存器。
  4. 调用probe():确认设备存在。
  5. 注册到调度器:让驱动进入PX4的任务调度循环。
  6. 开始数据采集:周期性读取传感器数据,发布到uORB。

嗯,这里有个细节:init()和probe()的顺序。有些驱动把初始化放在probe()里,有些分开。我个人习惯把硬件配置放在init()里,probe()只做通信验证。这样职责更清晰。

4.4 实战:写一个I2C磁力计驱动

光说不练假把式。咱们以AK09916磁力计为例,看看完整的驱动怎么写。

// ak09916_i2c.cpp
class AK09916_I2C : public AK09916
{
public:
    AK09916_I2C(const I2CSPIDriverConfig &config);
    virtual ~AK09916_I2C() = default;
    
    static I2CSPIDriverBase *instantiate(const I2CSPIDriverConfig &config,
                                          int runtime_instance);
    static int print_usage(const char *reason);
    
private:
    int probe() override;
    int init() override;
    
    // 数据采集
    void RunImpl() override;
};

// 探测函数
int AK09916_I2C::probe()
{
    // AK09916的I2C地址是0x0C
    uint8_t who_am_i = 0;
    int ret = read(REG_WIA, &who_am_i, 1);
    
    if (ret != OK || who_am_i != 0x48) {
        PX4_ERR("AK09916 not found: WIA=0x%02x", who_am_i);
        return -EIO;
    }
    
    PX4_INFO("AK09916 detected");
    return OK;
}

// 初始化函数
int AK09916_I2C::init()
{
    int ret = I2C::init();  // 先调父类init
    
    if (ret != OK) {
        return ret;
    }
    
    // 配置传感器:连续测量模式,100Hz
    write(REG_CNTL2, 0x10);
    usleep(10000);
    
    return OK;
}

// 数据采集
void AK09916_I2C::RunImpl()
{
    // 读取磁力计数据
    uint8_t buf[6];
    read(REG_HXL, buf, 6);
    
    // 解析数据
    int16_t mx = (buf[1] << 8) | buf[0];
    int16_t my = (buf[3] << 8) | buf[2];
    int16_t mz = (buf[5] << 8) | buf[4];
    
    // 发布到uORB
    sensor_mag_s mag_data = {};
    mag_data.x = mx;
    mag_data.y = my;
    mag_data.z = mz;
    mag_data.timestamp = hrt_absolute_time();
    
    _mag_pub.publish(mag_data);
}

个人经验:写驱动时,我习惯在probe()成功和失败时都打印日志。这样调试时一眼就能看出设备有没有被正确识别。别小看这几行日志,关键时刻能救命。

4.5 总结一下

PX4的驱动模型,说白了就是一套标准化的“接入流程”。不管什么传感器,都得走probe() -> init() -> RunImpl()这条路。你只要把这三步写好,驱动就基本成型了。

我刚开始做驱动开发时,总想着“我这个传感器特殊,得用特殊方法”。后来发现,老老实实按PX4的套路来,反而最省事。框架已经帮你处理好了调度、同步、错误处理这些麻烦事,你只需要关注传感器本身的操作。

嗯,今天就聊到这儿。下次咱们聊聊传感器校准,那又是另一番天地了。


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