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 这些。不加的话,编译的时候会报一堆莫名其妙的错误。我曾经有一次忘了加,折腾了半小时才发现。
3.2 目录结构解析:别被文件夹吓到
拿到源码后,你看到的第一眼肯定是密密麻麻的文件夹。别慌,核心就这几个:
| 目录 | 作用 | 我的经验 |
|---|---|---|
src/ |
所有源代码,包括驱动、模块、库 | 这是你主要要看的 |
msg/ |
uORB 消息定义文件 | 改消息结构就在这里 |
ROMFS/ |
文件系统镜像,启动脚本、参数默认值 | 启动流程出问题先查这里 |
boards/ |
板级支持包,不同硬件配置 | 移植新硬件主要改这里 |
build/ |
编译输出目录 | 不用手动改 |
src/ 下面又分几个子目录,我重点说一下:
src/modules/:各个功能模块,比如姿态估计、位置控制、GPS 驱动。每个模块都是一个独立的任务。src/drivers/:传感器、执行器的驱动。比如 IMU、磁力计、PWM 输出。src/lib/:通用库,比如数学库、滤波器、EKF 算法。src/systemcmds/:系统命令,比如top、dmesg,调试时很有用。
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 # 温度
模块发布消息和订阅消息的流程是这样的:
- 发布者:传感器驱动读取硬件数据,填充消息结构,调用
orb_publish()发布。 - 订阅者:姿态估计模块调用
orb_subscribe()订阅,然后轮询或等待新数据。 - 数据更新:发布者更新数据后,订阅者通过
orb_check()或poll()感知变化。
我遇到过一个问题:两个模块同时发布同一种消息,结果订阅者收到的数据是乱的。后来发现是消息 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 的核心架构:
你看,数据从硬件上来,经过驱动层变成 uORB 消息,然后各个模块订阅自己需要的消息,处理完再发布出去。最后应用层根据这些数据做出决策,比如切换飞行模式、记录日志。
这种架构的好处是解耦。你想换一个传感器驱动,只要它发布的消息格式不变,其他模块完全不用动。我在项目里换过好几次 IMU,每次都只改驱动层,上层代码一行没动。