第三章 PX4源码结构:Firmware目录树、模块注册机制、uORB消息总线
好,咱们进入第三章。这一章我打算把PX4的源码结构给你拆开揉碎了讲清楚。很多新手拿到Firmware仓库,第一反应就是懵——文件夹太多了,不知道从哪看起。我当年也一样,对着屏幕发呆了好几天。
其实你只要抓住三个核心:目录树怎么组织、模块怎么注册进去、模块之间怎么通信。搞懂这三样,你就能在PX4里自由穿梭了。
3.1 Firmware目录树:别被文件夹吓到
先说说整体布局。PX4的源码根目录下,有几个关键文件夹你必须记住:
- src/ —— 所有模块的源代码都在这里。驱动、控制器、通信协议,全在里头。
- boards/ —— 板级支持包。你用的Pixhawk、CUAV、Holybro,每种板子的配置和引脚定义都在这里。
- platforms/ —— 平台抽象层。NuttX、POSIX、QURT,不同操作系统的适配代码。
- msg/ —— uORB消息定义文件。所有话题的IDL描述都在这里。
- cmake/ —— 构建系统的配置文件。编译选项、模块开关,都在这里控制。
我个人习惯,拿到一个新板子,先看boards/目录。为什么呢?因为板级配置决定了你的驱动能不能跑起来。比如你要写一个SPI气压计驱动,你得先确认板子上SPI总线挂在哪、片选引脚是哪个。这些信息都在board_config.h和default.cmake里。
核心原则:驱动开发,80%的时间花在配置上,20%才是写代码。别搞反了。
再看src/目录,它下面按功能分了子目录:
src/drivers/—— 各种外设驱动。baro、imu、gps、pwm,应有尽有。src/modules/—— 高层功能模块。比如姿态估计、位置控制、导航。src/lib/—— 通用库。数学工具、滤波算法、参数系统。
你写SPI驱动,主要就在src/drivers/下面干活。我建议你直接找一个现成的SPI驱动,比如bmp388,把它的结构复制一份,改吧改吧就能用。嗯,这招叫「站在巨人的肩膀上」。
3.2 模块注册机制:让PX4认识你的驱动
写好了驱动代码,怎么让PX4在启动时加载它?这就涉及到模块注册机制了。
PX4的模块注册,说白了就是一个宏魔法。你只需要在驱动文件的末尾加上一行:
// 声明这是一个PX4模块
PX4_MODULE("bmp388");
然后,在CMakeLists.txt里把模块加进去:
px4_add_module(
MODULE drivers__baro__bmp388
MAIN bmp388
STACK_MAIN 2048
SRCS
bmp388.cpp
DEPENDS
px4_work_queue
)
这里要注意几个关键字段:
- MODULE —— 模块的唯一标识,用双下划线分隔层级。比如
drivers__baro__bmp388表示它在drivers/baro/下面。 - MAIN —— 模块的主入口函数名。启动时系统会调用
bmp388_main这个函数。 - STACK_MAIN —— 主线程的栈大小。SPI驱动一般2048字节就够了,但如果你要处理大量数据,建议设大一点。
- DEPENDS —— 依赖的其他模块或库。比如你用了工作队列,就得依赖
px4_work_queue。
避坑指南:我曾经犯过一个低级错误——MODULE路径写错了,结果编译出来的模块死活加载不上。后来发现是大小写问题。PX4对路径大小写敏感,drivers__baro__bmp388和drivers__Baro__bmp388是两个完全不同的模块。
注册完成后,你可以在启动脚本里用bmp388 start来启动它。启动脚本一般在ROMFS/px4fmu_common/init.d/目录下。
3.3 uORB消息总线:模块间的「快递员」
模块之间怎么交换数据?比如你的气压计驱动读到了高度数据,怎么告诉姿态估计模块?
答案是uORB。它是PX4内部的消息总线,所有模块都通过它来发布和订阅数据。
uORB的工作流程很简单:
- 定义消息 —— 在
msg/目录下写一个.msg文件,描述数据结构。 - 发布消息 —— 驱动把数据塞进消息结构体,然后调用
orb_publish()发出去。 - 订阅消息 —— 其他模块调用
orb_subscribe(),然后定期orb_copy()拿数据。
举个例子,气压计驱动发布高度数据:
// 定义消息结构体
struct sensor_baro_s {
uint64_t timestamp;
float pressure;
float temperature;
};
// 发布消息
orb_advert_t baro_pub = orb_advertise(ORB_ID(sensor_baro), nullptr);
sensor_baro_s baro_data;
baro_data.timestamp = hrt_absolute_time();
baro_data.pressure = read_pressure();
baro_data.temperature = read_temperature();
orb_publish(ORB_ID(sensor_baro), baro_pub, &baro_data);
姿态估计模块订阅它:
int baro_sub = orb_subscribe(ORB_ID(sensor_baro));
sensor_baro_s baro_data;
orb_copy(ORB_ID(sensor_baro), baro_sub, &baro_data);
// 现在baro_data里就有最新的气压和温度了
注意:uORB是异步的。发布者只管发,不管有没有人收。订阅者拿到的数据可能是几毫秒前的。如果你需要实时性很高的数据,比如IMU,建议用更高频率的发布策略。
uORB还有一个很实用的特性——多实例。比如你有两个气压计,可以分别发布到sensor_baro0和sensor_baro1。订阅者可以选择订阅其中一个,或者两个都订阅然后做融合。
3.4 知识体系总览
说了这么多,我画了一张图帮你理清思路。这张图展示了PX4源码的核心结构,以及目录树、模块注册、uORB三者之间的关系。
你看,这三个部分其实是环环相扣的。目录树决定了你的代码放在哪,模块注册告诉系统怎么加载它,uORB则让模块之间能互相说话。缺一个,你的驱动就跑不起来。
我的建议:刚开始学的时候,别急着写代码。先把src/drivers/下面一个简单的SPI驱动通读一遍,比如bmp388或者ms5611。跟着它的注册流程走一遍,再试着改一改,发布一个自定义的uORB消息。这样走一轮,你对整个框架就有感觉了。
嗯,这一章的内容就到这里。记住这三个核心,后面写驱动的时候你会感谢自己今天花时间搞懂了它们。