3、飞控源码结构解析:PX4源码目录结构、关键模块(uORB、参数系统、调度器)介绍

说实话,刚接触PX4源码那会儿,我也挺懵的。几千个文件,几十个文件夹,从哪看起?

后来我踩了不少坑,慢慢摸清了门道。今天我就带你捋一捋PX4的骨架——目录结构、uORB、参数系统、调度器。搞懂这四个东西,你再看源码就不会迷路了。

3.1 PX4源码目录结构:别被文件夹吓到

先看顶层目录。我习惯把它分成三块:核心代码、平台适配、构建工具。

目录名 作用 我重点关注什么
src/ 所有飞控模块源码 姿态估计、位置估计、控制律
platforms/ 不同硬件平台适配层 STM32、NuttX、Linux
boards/ 具体板级配置 Pixhawk系列、CUAV系列
msg/ uORB消息定义文件 传感器、状态、控制指令
cmake/ 构建脚本 模块开关、编译选项

我个人建议,新手先看 src/modules/msg/。前者是功能模块,后者是模块之间的“通信协议”。

核心目录速览

  • src/modules/ekf2 —— 扩展卡尔曼滤波器,姿态估计的核心
  • src/modules/mc_att_control —— 多旋翼姿态控制
  • src/modules/commander —— 飞行模式管理
  • src/lib/ —— 数学库、滤波器、几何变换

嗯,这里要注意:src/lib/ 里的代码很多模块都会用,比如四元数运算、坐标系转换。你写自己的控制算法时,大概率也会用到它们。

3.2 uORB:模块间的“快递系统”

uORB 是 PX4 的进程间通信机制。说白了,就是让各个模块能互相传数据。

为什么不用全局变量?我在项目中遇到过,全局变量多了,调试起来简直噩梦。你改了一个值,不知道哪个模块会受影响。uORB 就解决了这个问题——每个模块只订阅它需要的话题,发布它负责的数据。

3.2.1 消息定义

所有消息定义在 msg/ 目录下,以 .msg 结尾。比如 sensor_accel.msg

# sensor_accel.msg
uint64 timestamp          # 时间戳,微秒
uint32 device_id          # 设备ID
float32[3] x              # X轴加速度,m/s^2
float32[3] y              # Y轴加速度
float32[3] z              # Z轴加速度
uint16 samples            # 采样数

编译时,这些 .msg 文件会自动生成对应的 C++ 结构体。你不需要手动写序列化代码。

3.2.2 发布与订阅

看一个实际例子。假设你想发布一个自定义的加速度数据:

// 发布者
#include <uORB/uORB.h>
#include <uORB/topics/sensor_accel.h>

struct sensor_accel_s accel_data;
accel_data.timestamp = hrt_absolute_time();
accel_data.x[0] = 0.0f;
accel_data.y[0] = 0.0f;
accel_data.z[0] = -9.81f;

orb_advert_t pub = orb_advertise(ORB_ID(sensor_accel), &accel_data);
orb_publish(ORB_ID(sensor_accel), pub, &accel_data);

订阅端更简单:

// 订阅者
int sub_fd = orb_subscribe(ORB_ID(sensor_accel));
struct sensor_accel_s accel_data;
orb_copy(ORB_ID(sensor_accel), sub_fd, &accel_data);

我的经验:调试时可以用 orb_check() 检查是否有新数据,避免轮询浪费CPU。我曾经在低端MCU上没注意这个,结果CPU占用率飙到80%。

3.3 参数系统:不用重新编译就能调参

做飞控最烦什么?调参!

如果每次改个PID系数都要重新编译、烧录,那效率太低了。PX4的参数系统就是为了解决这个问题。

3.3.1 参数定义

参数定义在 src/modules/<module>/params.c.yaml 文件中。比如姿态控制器的参数:

/**
 * @group Multicopter Attitude Control
 * Roll axis proportional gain
 * @min 0.0
 * @max 10.0
 * @unit 1/s
 */
PARAM_DEFINE_FLOAT(MC_ROLL_P, 6.5f);

这里 MC_ROLL_P 就是参数名,默认值6.5。你可以在QGroundControl里实时修改它。

3.3.2 参数使用

在代码里获取参数:

// 获取参数
param_t handle = param_find("MC_ROLL_P");
float roll_p;
param_get(handle, &roll_p);

// 更新控制律
attitude_control(roll_p, pitch_p, yaw_p);

注意:参数修改后不会立即生效。你需要调用 param_set() 并等待下一次控制循环。我曾经在紧急情况下直接改参数,结果飞控没响应,差点炸机。

3.4 调度器:谁先跑,谁后跑

PX4 的调度器负责决定每个模块什么时候运行。它有两种模式:

  • 固定频率调度:比如姿态控制以250Hz运行
  • 事件驱动调度:有新数据来了才运行

看一个实际的任务定义:

// 在模块的 main.cpp 中
int MulticopterAttitudeControl::task_spawn(int argc, char *argv[]) {
    // 创建任务,以250Hz运行
    _task_id = px4_task_spawn_cmd(
        "mc_att_control",
        SCHED_DEFAULT,
        SCHED_PRIORITY_ATTITUDE_CONTROL,
        2048,  // 栈大小
        run_trampoline,
        nullptr
    );
    return _task_id;
}

调度器会维护一个任务列表,按优先级和时间片分配CPU。优先级高的任务(比如姿态控制)会优先执行。

调度优先级参考

  • 最高:传感器数据采集(1000Hz)
  • 高:姿态估计与控制(250-500Hz)
  • 中:位置估计与控制(50-100Hz)
  • 低:日志记录、遥测(10-50Hz)

你想想看,如果日志记录占用了太多CPU,姿态控制就会延迟,飞控性能直接下降。所以调度器的设计很关键。

3.5 知识体系总览

下面这张图是我自己画的,帮你把这三个关键模块串起来:

PX4 源码核心模块关系图 调度器 决定模块运行时机 uORB 消息总线 模块间数据交换 参数系统 运行时参数调整 姿态控制模块 位置估计模块 传感器驱动 调度器决定模块何时运行 → 模块通过uORB交换数据 → 参数系统提供运行时配置

这张图展示了三个核心模块的协作关系。调度器决定什么时候运行哪个模块,模块之间通过uORB交换数据,参数系统则让所有模块的配置可以在线调整。

避坑指南:我曾经在移植PX4到新硬件时,忽略了调度器的优先级配置。结果传感器数据采集被日志任务抢占,导致姿态估计延迟。后来我花了整整两天才定位到问题。所以,拿到一个新平台,第一件事就是检查调度优先级。

好了,这一章的内容就到这里。你只要把目录结构、uORB、参数系统、调度器这四个东西搞明白,PX4源码就不再是天书了。下一章我们会深入姿态控制模块,看看代码到底是怎么算的。


专注资料整理