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 知识体系总览
下面这张图是我自己画的,帮你把这三个关键模块串起来:
这张图展示了三个核心模块的协作关系。调度器决定什么时候运行哪个模块,模块之间通过uORB交换数据,参数系统则让所有模块的配置可以在线调整。
避坑指南:我曾经在移植PX4到新硬件时,忽略了调度器的优先级配置。结果传感器数据采集被日志任务抢占,导致姿态估计延迟。后来我花了整整两天才定位到问题。所以,拿到一个新平台,第一件事就是检查调度优先级。
好了,这一章的内容就到这里。你只要把目录结构、uORB、参数系统、调度器这四个东西搞明白,PX4源码就不再是天书了。下一章我们会深入姿态控制模块,看看代码到底是怎么算的。