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 启动流程
驱动启动的完整链路是这样的:
- 系统启动 -> 执行
rcS脚本 - 脚本调用 ->
my_sensor start - 进入main函数 -> 解析参数,调用
start - 创建设备对象 ->
new MySensor() - 调用init() -> 内部调用
probe()检测硬件 - 注册设备节点 ->
register_class_devname() - 启动工作队列 ->
ScheduleOnInterval()开始周期性采集
这里有个细节——probe()失败不会导致系统崩溃。PX4会优雅地返回错误,然后脚本尝试下一个备选驱动。这就是为什么同一个传感器可以有多个驱动共存。
我曾经踩过的坑:在init()里调用了需要调度器的函数,结果系统还没初始化完,直接死锁。记住,init()阶段只做硬件初始化和资源申请,别启动任何周期性任务。等init()返回OK后,再在start()里启动工作队列。
2.5 知识体系总览
说了这么多,我画张图帮你理清关系。这张图展示了驱动框架的核心模块和它们之间的数据流向。
从这张图你能看到,数据从物理硬件出发,经过总线层到达驱动,驱动处理后通过uORB发布给上层应用。整个链路清晰,每一层各司其职。
嗯,这一章的内容就到这里。驱动框架这东西,光看文档容易晕,我建议你找个简单的传感器驱动(比如LIS3MDL磁力计)从头读一遍代码,对照着我讲的这些类和方法,很快就能上手。
公众号:蓝海资料掘金营,微信deep3321