2、PX4驱动框架深度解析:设备驱动模型、I2C/SPI总线框架、DriverFramework核心类、模块注册与启动流程

好,咱们进入正题。这一章我打算把PX4的驱动框架彻底拆开,让你看看里面到底是怎么运转的。说实话,我刚接触PX4那会儿,也被这套框架绕得有点晕。但摸清楚之后,你会发现它设计得其实挺巧妙。

2.1 设备驱动模型:一切皆文件

PX4的驱动模型,核心思想就四个字——一切皆文件。你想想看,在Linux里操作一个传感器,是不是打开一个设备节点,然后read/write?PX4在RTOS上也沿用了这套思路。

每个传感器驱动注册成功后,会在/dev目录下创建一个设备节点。比如磁力计就是/dev/mag0/dev/mag1。上层模块根本不需要知道底层是I2C还是SPI,它只管打开文件、读写数据。

核心要点:驱动只负责两件事——初始化硬件、发布uORB消息。设备节点是给调试和上层工具用的,真正的数据流转走的是uORB总线。

我个人习惯把驱动分成三层:

  • 硬件抽象层:直接操作寄存器、GPIO、中断
  • 总线适配层:封装I2C/SPI的读写细节
  • 应用接口层:提供open/close/ioctl/read/write

嗯,这里要注意,PX4的驱动不强制你用设备节点。很多内部驱动直接通过uORB发布数据,连/dev节点都不创建。这在资源紧张的飞控上很常见。

2.2 I2C/SPI总线框架:别重复造轮子

我在项目中遇到过好几次,有人自己写I2C读写函数,结果时序不对,传感器死活不响应。其实PX4已经把总线操作封装好了,你直接调用就行。

2.2.1 I2C总线

I2C驱动的基础类是I2C,定义在src/lib/drivers/device/i2c.h。你只需要继承它,实现probe()init()方法。

class MySensor : public device::I2C
{
public:
    MySensor() : I2C(DRV_MAG_DEVTYPE, MODULE_NAME, I2C_BUS_1, ADDRESS, 400000) {}
    
    int probe() override {
        // 读WHO_AM_I寄存器验证设备
        uint8_t id;
        if (read_reg(REG_WHO_AM_I, &id, 1) != OK) return -EIO;
        return (id == EXPECTED_ID) ? OK : -ENODEV;
    }
    
    int init() override {
        int ret = I2C::init();
        if (ret != OK) return ret;
        // 配置传感器寄存器
        write_reg(REG_CTRL, 0x0F);
        return OK;
    }
};

这里有个坑——I2C地址是7位的,左对齐。我曾经因为地址写错,浪费了一整天。你传参时记得把7位地址左移1位,或者直接用I2C::set_device_address()

避坑指南:PX4的I2C总线号是从0开始的。但实际硬件上,I2C_BUS_1可能对应的是STM32的I2C1外设。查清楚板级定义再填参数。

2.2.2 SPI总线

SPI驱动类似,继承SPI类。区别在于SPI需要指定片选引脚和时钟极性。

class MySPISensor : public device::SPI
{
public:
    MySPISensor() : SPI(DRV_ACCEL_DEVTYPE, MODULE_NAME, SPI_BUS_1, CS_PIN, SPIDEV_MODE3, 10000000) {}
    
    int probe() override {
        // SPI读操作需要先发寄存器地址(最高位置1表示读)
        uint8_t cmd = REG_WHO_AM_I | 0x80;
        uint8_t id;
        transfer(&cmd, &id, 1);
        return (id == 0x69) ? OK : -ENODEV;
    }
};

SPI的传输函数transfer()是全双工的。你发一个字节的同时会收到一个字节。很多新手只关注发送,忽略了接收缓冲区的处理。我建议你每次调用都准备好接收缓冲区,哪怕你暂时不需要。

2.3 DriverFramework核心类:骨架与血肉

DriverFramework是PX4驱动的基础设施。它提供了几个核心类,你写的每个驱动几乎都会用到。

类名 作用 我常用的场景
device::Device 设备基类,管理生命周期 所有传感器驱动的父类
device::I2C I2C总线封装 气压计、磁力计这类低速传感器
device::SPI SPI总线封装 IMU、高速陀螺仪
device::CDev 字符设备接口 需要创建/dev节点的驱动
px4::WorkItem 工作队列任务 周期性数据采集和发布

说白了,你写驱动就是组合这些类。比如一个典型的IMU驱动:

  • 继承device::SPI处理总线通信
  • 继承px4::WorkItem实现定时采集
  • 内部使用uORB::Publication发布传感器数据

个人经验:别把太多逻辑塞进驱动类里。我习惯把数据处理(比如滤波、校准)放到独立的模块中,驱动只负责原始数据的采集和发布。这样调试起来清爽很多。

2.4 模块注册与启动流程:从代码到运行

驱动写好了,怎么让它跑起来?PX4有一套标准的模块注册机制。

2.4.1 模块注册

每个驱动模块都需要实现一个module结构体,告诉系统:我叫什么名字、支持哪些命令。

extern "C" __EXPORT int my_sensor_main(int argc, char *argv[]);

static int my_sensor_start() {
    // 创建设备实例
    MySensor *dev = new MySensor();
    if (dev->init() != OK) {
        delete dev;
        return -1;
    }
    return 0;
}

int my_sensor_main(int argc, char *argv[]) {
    if (argc < 2) {
        PX4_ERR("usage: my_sensor {start|stop|status}");
        return 1;
    }
    
    if (!strcmp(argv[1], "start")) {
        return my_sensor_start();
    }
    // ... 其他命令处理
}

然后在CMakeLists.txt里把源文件加入编译:

px4_add_module(
    MODULE modules__my_sensor
    MAIN my_sensor
    SRCS
        my_sensor.cpp
    DEPENDS
        drivers__device
)

2.4.2 启动流程

驱动启动的完整链路是这样的:

  1. 系统启动 -> 执行rcS脚本
  2. 脚本调用 -> my_sensor start
  3. 进入main函数 -> 解析参数,调用start
  4. 创建设备对象 -> new MySensor()
  5. 调用init() -> 内部调用probe()检测硬件
  6. 注册设备节点 -> register_class_devname()
  7. 启动工作队列 -> ScheduleOnInterval()开始周期性采集

这里有个细节——probe()失败不会导致系统崩溃。PX4会优雅地返回错误,然后脚本尝试下一个备选驱动。这就是为什么同一个传感器可以有多个驱动共存。

我曾经踩过的坑:在init()里调用了需要调度器的函数,结果系统还没初始化完,直接死锁。记住,init()阶段只做硬件初始化和资源申请,别启动任何周期性任务。等init()返回OK后,再在start()里启动工作队列。

2.5 知识体系总览

说了这么多,我画张图帮你理清关系。这张图展示了驱动框架的核心模块和它们之间的数据流向。

PX4驱动框架核心结构 应用层(导航、控制、日志) uORB 消息总线 传感器驱动层 IMU驱动 SPI + WorkItem 磁力计驱动 I2C + WorkItem 气压计驱动 I2C + WorkItem 硬件总线层(I2C / SPI / UART) 物理硬件(传感器芯片、GPIO、中断) 模块注册 start/stop/status 总线框架 I2C::transfer() SPI::transfer()

从这张图你能看到,数据从物理硬件出发,经过总线层到达驱动,驱动处理后通过uORB发布给上层应用。整个链路清晰,每一层各司其职。

嗯,这一章的内容就到这里。驱动框架这东西,光看文档容易晕,我建议你找个简单的传感器驱动(比如LIS3MDL磁力计)从头读一遍代码,对照着我讲的这些类和方法,很快就能上手。


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