第三章 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.hdefault.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__bmp388drivers__Baro__bmp388是两个完全不同的模块。

注册完成后,你可以在启动脚本里用bmp388 start来启动它。启动脚本一般在ROMFS/px4fmu_common/init.d/目录下。

3.3 uORB消息总线:模块间的「快递员」

模块之间怎么交换数据?比如你的气压计驱动读到了高度数据,怎么告诉姿态估计模块?

答案是uORB。它是PX4内部的消息总线,所有模块都通过它来发布和订阅数据。

uORB的工作流程很简单:

  1. 定义消息 —— 在msg/目录下写一个.msg文件,描述数据结构。
  2. 发布消息 —— 驱动把数据塞进消息结构体,然后调用orb_publish()发出去。
  3. 订阅消息 —— 其他模块调用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_baro0sensor_baro1。订阅者可以选择订阅其中一个,或者两个都订阅然后做融合。

3.4 知识体系总览

说了这么多,我画了一张图帮你理清思路。这张图展示了PX4源码的核心结构,以及目录树、模块注册、uORB三者之间的关系。

PX4源码核心结构 Firmware 目录树 src/ ├── drivers/ │ ├── baro/ │ │ └── bmp388 │ ├── imu/ │ └── gps/ ├── modules/ ├── lib/ boards/ ├── px4_fmu-v5/ ├── cuav_v5/ msg/ ├── sensor_baro.msg ├── sensor_accel.msg cmake/ platforms/ 模块注册机制 1. 声明模块 PX4_MODULE("bmp388") 2. 配置CMake px4_add_module( MODULE drivers__baro__bmp388 MAIN bmp388 STACK_MAIN 2048 SRCS bmp388.cpp ) 3. 启动脚本 bmp388 start 4. 主入口函数 int bmp388_main(int argc, char *argv[]) uORB 消息总线 消息定义 (.msg) sensor_baro.msg uint64 timestamp float32 pressure float32 temperature 发布者 (Publisher) orb_advertise() orb_publish() 订阅者 (Subscriber) orb_subscribe() orb_copy() 特性 异步通信 多实例支持 编译链接 数据交换

你看,这三个部分其实是环环相扣的。目录树决定了你的代码放在哪,模块注册告诉系统怎么加载它,uORB则让模块之间能互相说话。缺一个,你的驱动就跑不起来。

我的建议:刚开始学的时候,别急着写代码。先把src/drivers/下面一个简单的SPI驱动通读一遍,比如bmp388或者ms5611。跟着它的注册流程走一遍,再试着改一改,发布一个自定义的uORB消息。这样走一轮,你对整个框架就有感觉了。

嗯,这一章的内容就到这里。记住这三个核心,后面写驱动的时候你会感谢自己今天花时间搞懂了它们。


专注资料整理