3. PX4 固件架构:从源码到飞控核心

好,我们正式开始啃 PX4 的源码。说实话,很多新手一上来就被 PX4 的目录结构吓住了,觉得太复杂。其实你把它拆开看,就几个核心模块。今天我就带你把这层窗户纸捅破。

3.1 源码获取:别下错版本

先说说怎么拿到源码。我个人习惯直接用 git clone,这样后面切版本、看历史都方便。

git clone https://github.com/PX4/PX4-Autopilot.git --recursive
cd PX4-Autopilot

这里有个坑——--recursive 参数一定要加。PX4 依赖很多子模块,比如 mavlink、uavcan 这些。不加的话,编译的时候会报一堆莫名其妙的错误。我曾经有一次忘了加,折腾了半小时才发现。

小建议: 如果你只是学习,可以切到稳定版分支,比如 v1.13 或 v1.14。master 分支有时候会引入新 bug,不适合初学者。

3.2 目录结构解析:别被文件夹吓到

拿到源码后,你看到的第一眼肯定是密密麻麻的文件夹。别慌,核心就这几个:

目录 作用 我的经验
src/ 所有源代码,包括驱动、模块、库 这是你主要要看的
msg/ uORB 消息定义文件 改消息结构就在这里
ROMFS/ 文件系统镜像,启动脚本、参数默认值 启动流程出问题先查这里
boards/ 板级支持包,不同硬件配置 移植新硬件主要改这里
build/ 编译输出目录 不用手动改

src/ 下面又分几个子目录,我重点说一下:

  • src/modules/:各个功能模块,比如姿态估计、位置控制、GPS 驱动。每个模块都是一个独立的任务。
  • src/drivers/:传感器、执行器的驱动。比如 IMU、磁力计、PWM 输出。
  • src/lib/:通用库,比如数学库、滤波器、EKF 算法。
  • src/systemcmds/:系统命令,比如 top、dmesg,调试时很有用。
核心思路: PX4 的架构是模块化的。每个模块独立运行,通过 uORB 消息通信。你改一个模块,不影响其他模块。

3.3 uORB 消息机制:飞控的"神经网络"

uORB 是 PX4 内部的消息总线。说白了,就是各个模块之间怎么传数据。比如传感器模块读到 IMU 数据,怎么告诉姿态估计模块?就是通过 uORB。

消息定义在 msg/ 目录下,每个 .msg 文件定义一种消息类型。举个例子:

# sensor_accel.msg
uint64 timestamp        # 时间戳
uint32 device_id        # 设备ID
float32[3] x            # X轴加速度
float32[3] y            # Y轴加速度
float32[3] z            # Z轴加速度
float32 temperature     # 温度

模块发布消息和订阅消息的流程是这样的:

  1. 发布者:传感器驱动读取硬件数据,填充消息结构,调用 orb_publish() 发布。
  2. 订阅者:姿态估计模块调用 orb_subscribe() 订阅,然后轮询或等待新数据。
  3. 数据更新:发布者更新数据后,订阅者通过 orb_check() 或 poll() 感知变化。
注意: uORB 是单机内部的通信机制,不是跨设备的。如果你需要和地面站通信,那是 mavlink 的事,别搞混了。

我遇到过一个问题:两个模块同时发布同一种消息,结果订阅者收到的数据是乱的。后来发现是消息 ID 冲突了。所以,自定义消息时一定要确保 ID 唯一。

3.4 模块与任务调度:飞控的"操作系统"

PX4 跑在 NuttX 实时操作系统上。每个模块其实就是一个任务(task),由调度器统一管理。调度器决定哪个任务什么时候运行,运行多久。

模块的启动方式有两种:

  • 自动启动:在 ROMFS/px4fmu_common/init.d/ 下的启动脚本里配置。飞控上电后自动加载。
  • 手动启动:通过 uorb start 或 module_name start 命令手动启动。

每个模块的代码结构大致是这样的:

// 模块入口
extern "C" __EXPORT int module_main(int argc, char *argv[]);

int module_main(int argc, char *argv[])
{
    // 解析命令行参数
    if (argc < 2) {
        PX4_ERR("usage: module {start|stop|status}");
        return 1;
    }

    // 启动任务
    if (strcmp(argv[1], "start") == 0) {
        return Module::start();
    }

    // 停止任务
    if (strcmp(argv[1], "stop") == 0) {
        return Module::stop();
    }

    return 0;
}

模块内部通常有一个 run() 函数,里面是个死循环:

void Module::run()
{
    // 订阅需要的消息
    int sensor_sub = orb_subscribe(ORB_ID(sensor_accel));

    // 设置循环频率
    while (!should_exit()) {
        // 等待新数据
        orb_check(sensor_sub, &updated);
        if (updated) {
            orb_copy(ORB_ID(sensor_accel), sensor_sub, &data);
            // 处理数据
            process_data(data);
        }

        // 控制循环频率
        usleep(1000); // 1ms
    }
}
调试技巧: 在终端输入 top 命令,可以看到所有正在运行的任务和它们的 CPU 占用率。如果某个任务占用过高,可能是循环里没有加延时。

嗯,这里要注意:任务的优先级很重要。高优先级的任务会抢占低优先级的任务。比如传感器读取的优先级应该比日志记录高,否则飞控会反应迟钝。

3.5 整体架构图:一张图看懂

说了这么多,不如一张图来得直观。下面这张图展示了 PX4 的核心架构:

PX4 固件核心架构 硬件抽象层 (HAL) 设备驱动 (Drivers) uORB 消息总线 传感器模块 (IMU, GPS, 磁力计) 估计与控制 (EKF, 姿态控制, 位置控制) 执行器模块 (PWM, 混控器, 电机) 应用层 (Commander, Navigator, Logger) 数据流方向:硬件 → 驱动 → uORB → 模块 → 应用

你看,数据从硬件上来,经过驱动层变成 uORB 消息,然后各个模块订阅自己需要的消息,处理完再发布出去。最后应用层根据这些数据做出决策,比如切换飞行模式、记录日志。

这种架构的好处是解耦。你想换一个传感器驱动,只要它发布的消息格式不变,其他模块完全不用动。我在项目里换过好几次 IMU,每次都只改驱动层,上层代码一行没动。

总结一下: PX4 的架构就是"驱动采集数据 → uORB 传递数据 → 模块处理数据 → 应用决策"。理解了这个流程,你就掌握了 PX4 的精髓。