☰
PX4飞控源码解析:从环境搭建到uORB任务调度机制
2026/10/5 3:48:33 网站建设 项目流程

兄弟们,今天开始搞点硬核的,咱们来聊聊PX4飞控的代码。

最近后台不少朋友在问,说网上PX4的资料虽然多,但大部分都停留在“怎么搭环境”、“怎么起飞”的层面,一旦想深入看看代码到底怎么跑的,就感觉两眼一抹黑。正好,我也准备把这块的学习心得整理成一个系列,这篇就算开篇。

需要提前说明的是,这个系列不是要把每个文件、每个函数都过一遍——那没人看得下去,也没那个必要。我更想把 PX4 这套代码的骨架、运行逻辑,以及我们学习时容易卡住的关键节点拎出来,掰开揉碎了讲。这篇第一部分,咱们不急着看代码,先把编译环境搞定,把整个源码结构和任务调度机制琢磨透。

只有环境跑通了,代码在手里能编译、能仿真、能调试,后面聊具体模块(比如姿态控制、位置估计)才有意义。不然你拿着一堆源码干瞪眼,看两天就放弃,那就太可惜了。毕竟这玩意儿,从“放弃”到“精通”之间,差的往往就是一个能跑起来的环境。

1. 为什么第一篇必须先过环境这道坎

很多初学者有个误区,觉得“代码解析”嘛,那不得直接从 main 函数开始一行行读?环境搭建这种粗活,有什么技术含量?

我当初也是这么想的,结果被现实狠狠上了一课。

1.1 编译都过不了,你分析个寂寞

PX4 是一个大型的 C/C++ 混合项目,依赖了上百个第三方库和工具链。它的构建系统基于 CMake,但封装了一层自己的px4命令行工具。这意味着,如果你对它的编译流程没有一个基础的认知,你连代码文件在哪、头文件怎么找、CMakeLists 怎么组织都搞不明白。

更重要的是,PX4 的代码是强依赖硬件抽象层的。你以为你在看“姿态控制”的算法代码,结果里面一大半是在调uORB消息、访问参数、跟驱动打交道。如果环境没跑通,你无法通过仿真去验证你读的代码是不是真的在“干活”,那读代码就成了纯粹的“看天书”。

1.2 仿真跑起来,代码才是活的

这个系列所有篇幅,都会尽量以Gazebo 仿真环境为依托。为什么?

因为仿真是验证代码逻辑最快的手段。你改一个控制参数,跑一下仿真看响应曲线,比自己盲目猜然后上真机炸机要靠谱一万倍。而且,仿真环境下你可以在 IDE 里打断点,可以实时打印日志,这比看静态代码理解得快多了。

所以,我们的第一步,就是在 Ubuntu 上搭一个干净、可复现的编译和仿真环境。

2. Ubuntu 下从零跑起 PX4 编译链(避坑实录)

PX4 官方其实给了一键搭建脚本,但实际跑下来,尤其是在国内网络环境下,坑是真的多。我以Ubuntu 20.04 + PX4 v1.13.3为例(目前资料最丰富、兼容性最好的组合),带大家走一遍我验证过无数次的流程。

2.1 依赖安装:别用一键脚本,自己来更稳

官方那个ubuntu.sh脚本,它会帮你装很多很多依赖,包括 Qt、OpenCV 之类。但问题在于,它装的版本可能和你系统里已有的冲突,而且下载速度感人。

我的建议是手动安装核心依赖,够用就行。

# 更新软件源 sudo apt update sudo apt upgrade -y # 安装基础编译工具 sudo apt install -y \ build-essential \ cmake \ git \ ninja-build \ python3-pip \ python3-venv \ arm-none-eabi-gcc \ gcc-arm-linux-gnueabihf \ g++-arm-linux-gnueabihf \ protobuf-compiler \ libeigen3-dev \ libopencv-dev \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ libjsoncpp-dev \ libtinyxml2-dev \ libboost-all-dev \ libprotoc-dev \ libudev-dev

这里有个非常关键的坑:Python 环境。

PX4 的构建过程中会调用大量的 Python 脚本(比如处理 uORB 消息生成代码)。系统自带的 Python 3.8 虽然能用,但如果你之后要用一些机载计算机的额外功能,很容易出现包冲突。我的经验是,用python3-venv建一个虚拟环境来装 PX4 工具链里的 Python 依赖。

# 创建虚拟环境(放在 home 目录,方便找) python3 -m venv ~/px4_venv # 激活(每次编译前都要激活) source ~/px4_venv/bin/activate pip install --upgrade pip pip install pyserial empy toml numpy jinja2 pyyaml kconfiglib

注意:empy这个库是生成 PX4 内部代码的关键,老版本和 Python 3.8 有兼容性问题,一定要安装最新版。我当时被em模块报错折磨了两个晚上,最后发现是empy版本太老。

2.2 源码拉取:避开子模块下载的大坑

PX4 的代码托管在 GitHub,它是一个带有很多子模块(submodule)的仓库。直接git clone --recursive在大部分网络环境下都会卡死。

我的做法是分步走:

# 先普通克隆主干代码(不带递归) git clone https://github.com/PX4/PX4-Autopilot.git cd PX4-Autopilot # 先只看一下需要的分支 git branch -a # 我们切到 v1.13.3 这个稳定版 git checkout v1.13.3 # 初始化子模块,但不急着更新 git submodule init # 只更新与平台相关的子模块(这会大大减少下载量) # 这里直接更新所有子模块,因为仿真需要 Gazebo 相关的模型 # 国内网络建议设置代理,或者使用国内镜像加速 git submodule update --recursive

关于子模块下载慢的问题,虽然不方便展开讲,但我可以提供一个思路:去 Gitee(码云)上搜索 PX4 的镜像仓库。很多热心人士会定期同步,你从 Gitee 克隆会快很多,然后再把子模块的 remote 地址改回来。这个操作是合规的,只是换个托管平台而已,能省下大量的等待时间。

2.3 首次编译:看着进度条,心态要稳

环境变量搞定后,进入编译环节。PX4 使用自己封装好的编译器指令:

# 激活python虚拟环境 source ~/px4_venv/bin/activate # 回到 PX4 根目录 cd ~/PX4-Autopilot # 编译仿真固件(不带硬件架构,就是跑在PC上的) make px4_sitl gazebo-classic

第一次编译的时间取决于你的 CPU,一般会持续 20~40 分钟。期间你会看到大量的 C++ 源码在编译,这很正常,别胡思乱想。

这里我要重点提一下make px4_sitl gazebo-classic这条命令干了什么:

  1. 它会去编译 NuttX 相关的工具链,生成构建文件。
  2. 它会扫描所有模块的CMakeLists.txt,生成代码。
  3. 它会编译所有模块,最后链接成一个可执行文件px4。
  4. 它会启动 Gazebo 经典版模拟器,加载一个默认的四旋翼模型。

如果编译到 90% 给你报个错,不要慌,绝大多数情况是缺依赖库。最典型的是eigen3路径不对,你只需要:

sudo ln -s /usr/include/eigen3/Eigen /usr/include/Eigen

这个软链接问题,基本是 Ubuntu 20.04 和 22.04 的通病,一步搞定。

3. 用仿真环境验证工具链:别急着飞,先看懂这帮进程

编译成功后,你会在终端里看到一堆[ERR],别慌,那是蜂鸣器驱动在找硬件,没有很正常。耐心的等待,直到看到:

[INFO] [px4] Startup script found, executing... [INFO] [px4] Creating symbolic link /dev/ttyS0 [INFO] [px4] Startup script returned successfully [INFO] [px4] Startup script found, executing... [INFO] [px4] Creating symbolic link /dev/ttyS1 [INFO] [px4] Startup script returned successfully [INFO] [px4] Startup script found, executing...

以及 Gazebo 窗口的弹出,里面有一个默认的四旋翼模型。

3.1 QGC 地面站连接:确认通信链路

这里要装一个 QGroundControl(QGC),它是我们和 PX4 通信的“仪表盘”。虽然代码解析阶段不一定非得用地面站,但它能让你直观地看到飞控的实时状态,比如姿态、GPS、电量等。

QGC 连接本地仿真的原理,其实是 PX4 通过 UDP 协议把 MAVLink 数据包发送到本地端口(默认14550)。QGC 监听这个端口,就能看到飞控数据。

这个过程中你会发现,飞控的代码是“活”的。它内部的mavlink模块在不停的和地面站交互,状态估计模块在跑数据融合,而这一切,都发生在你眼前。

3.2 命令行里的黑魔法:commander和list

在已经成功启动 PX4 SITL 的终端窗口里(不是 Gazebo 窗口),输入:

pxh> commander takeoff

你会发现 Gazebo 里的无人机突然就飞起来了!这就是代码的力量。

此时,如果你输入:

pxh> list

屏幕上会罗列出当前 PX4 内部正在运行的所有任务/模块。

你会看到类似wq、log_writer、navigator、mc_pos_control、mc_att_control、sensors、ekf2等等。这些任务就是我们接下来要“解析”的主角。

很多人觉得 PX4 代码难啃,就是因为它不是简单的函数调用,而是一个多进程(尽管是单核多线程)的消息驱动系统。你看到一个函数,你不知道它是谁调用的,也不知道它什么时候被调用。搞清楚uORB消息和任务调度,是解开 PX4 代码迷宫的第一把钥匙。

4. PX4 源码层的第一眼:模块、任务与通信

现在环境跑通了,我们来聊聊代码。打开你下载的PX4-Autopilot文件夹,别被一眼望不到头的目录吓着,抓住主脉络。

4.1 不要停留在src,先看msg和cmake

很多同学一上来就钻到src/modules/mc_att_control里面看姿态控制代码,结果被满天飞的orb_publish、orb_subscribe劝退。

我的建议是,先花一天时间看两个地方:msg/目录和cmake/目录。

  • msg/目录下的.msg文件:这是 PX4 内部通信的“语言”定义。每个文件定义了一种数据类型。比如vehicle_attitude.msg定义了飞行器姿态消息的字段。所有模块之间的数据交换,都是通过发布/订阅(publish/subscribe)这些消息完成的。

  • cmake/目录下的configs/文件夹:这里定义了不同的板级配置。你会看到px4_fmu-v5_default.cmake(对应 Pixhawk 4)等文件。框图的编译开关都在这里,比如CONFIG_MODULE_COMMANDER之类的宏,你需要在对应的配置文件中打开,模块才会被编译。

理解了这两个地方,你看源码就不是看“流水账”,而是带着问题看:

  • 这个模块发布了什么消息?
  • 它订阅了什么消息?
  • 谁在它的 CMakeLists 里定义了它,它的优先级怎么样?

4.2 模块的“统一入口”:ModuleBase和启动脚本

PX4 的每个模块,例如mc_att_control,它的主文件通常叫mc_att_control_main.cpp。里面会定义一个类,比如MavlinkStream,但最终会继承一个全系统统一的基础类ModuleBase。

这个ModuleBase定义了模块的标准生命周期接口:task_spawn(启动线程)、task_main(线程主循环)、custom_command(处理命令行传入的参数)等。

所以在 PX4 的世界里,启动一个进程/任务,不是去main()函数里疯狂写逻辑,而是:

  1. 实例化一个模块类。
  2. 调用module.task_spawn()创建新线程。
  3. 新线程执行task_main(),在里面写一个while(!should_exit())的大循环。

这个while循环的写法,是理解PX4代码精髓的关键。

4.3 uORB 通信机制:为什么它比直接函数调用好

我在第一次读代码时,最大的疑惑是:为什么 PX4 不直接调用函数,非要用uORB这套机制?

  • 解耦:姿态控制模块不需要知道姿态数据是从「IMU驱动」来的,还是从「仿真器」来的。它只需要订阅sensor_combined或vehicle_attitude消息就行。生产者和消费者互不干涉,代码变得很干净。
  • 实时性:uORB 内部用了无锁队列机制(px4::atomic_bool等),使得多线程通信不需要加锁,开销极低,非常符合飞控这种对延迟敏感的场景。
  • 可视化:你可以通过uorb top命令在 PX4 的终端(pxh)里实时查看哪条消息被发布得最频繁,哪个模块订阅了它。

所以,你在读代码时,看到一个函数干了一堆orb_publish操作,别觉得它是在“发消息”,它就是把这个模块的计算结果,通过uORB总线“广播”出去,告诉整个系统:本模块的新鲜数据出炉了。

5. 深入mc_att_control:姿态控制的源码解剖(上)

做为一个热身,我们先挑一个比较简单、但绝对核心的模块来练手:多旋翼姿态控制器(mc_att_control)。这个模块的任务很清晰:根据期望姿态(来自遥控器或任务规划器),结合当前姿态(来自状态估计器),计算出需要给电机输出的力矩。

5.1 模块的启动与主循环:从task_spawn到task_main

我们打开src/modules/mc_att_control/mc_att_control_main.cpp。

首先是模块的入口:

int MulticopterAttitudeControl::task_spawn(int argc, char *argv[]) { // 分配一个实例 MulticopterAttitudeControl *instance = new MulticopterAttitudeControl(); // 从参数管理器中获取模块的自定义参数,比如是否手动模式 instance->parameters_update(true); // 启动一个新线程,线程入口是 task_main_trampoline px4_task_spawn_cmd("mc_att_control", SCHED_DEFAULT, SCHED_PRIORITY_MAX - 5, PX4_STACK_SIZE_DFAULT, task_main_trampoline, (char * const *)argv); return PX4_OK; }

重点看SCHED_PRIORITY_MAX - 5,这说明姿态控制的优先级极高,仅次于最高优先级(预留给了关键的驱动/状态估计),几乎可以实时抢占其他任务。

再看task_main_trampoline,它会调用task_main()。task_main()里是一个标准的 PX4 模块循环:

void MulticopterAttitudeControl::task_main() { ... // 主循环 while (!should_exit()) { // 1. 等待新消息(vehicle_attitude_setpoint, vehicle_attitude 等) const unsigned timeout_ms = 10; if (!_attitude_setpoint_sub.updated() && !_vehicle_attitude_sub.updated()) { if (hrt_elapsed_time(&_last_run) < timeout_ms * 1000) { continue; // 没等到,就空转 } } // 2. 读取最新的订阅消息 _attitude_setpoint_sub.update(&_attitude_setpoint); _vehicle_attitude_sub.update(&_vehicle_attitude); ... // 3. 调用核心控制数学计算 _update_attitude_control(); } }

看到没有,这个while循环的逻辑极其清晰:等待最新的数据 -> 读到数据 -> 交给控制算法。

5.2 姿态控制的“心脏”:四元数与外环

真正干活的函数是_update_attitude_control()。里面核心是姿态误差的计算。PX4 这里用的不是简单的欧拉角相减,而是用四元数旋转来求误差,然后转为角速度控制量。

之前我在刚接触的时候,总觉得航向角(偏航角)控制在特殊姿态下容易发散,看这段代码才算有所理解。

其中有一行非常经典的代码,是用来生成期望姿态四元数:

// 这里是伪代码,实际会调用 attitude_control 库 math::Quatf qd = AttitudeControl::update_attitude_q(_vehicle_attitude, _attitude_setpoint, _rate_setpoint ...);

它会根据当前的姿态q和期望姿态q_d,求出两者之间的“差值四元数”q_e,然后利用这个差值构造出期望的角速度(rate setpoint)。这本质上是把姿态控制问题转化成了角速度控制问题。

所以姿态控制的外环是角度环,输出是期望角速度;内环是角速度环,输出是期望力矩。mc_att_control将期望力矩发布到actuator_controls消息中,由mixer模块把它映射到各个电机的PWM输出上去。

这一层一层的嵌套关系,就像剥洋葱,每一层只关心自己那点事。理解了mc_att_control怎么读取输入、怎么调用算法产生的输出,你会发现 PX4 里其他的控制模块无非都是这个模式的不同变种。

6. 给代码解析铺路的几个关键认知

既然这个系列会一直做下去,我觉得有必要在第一篇就把一些基础的“心法”传递给大家。否则你在看后面的文章时,会觉得我在念天书。

6.1 日志与调试:认识PX4_INFO和PX4_ERR

不要用printf在 PX4 里打印信息。PX4 封装了自己的日志宏,比如PX4_INFO("...")、PX4_WARN("...")、PX4_ERR("...")。这些宏会带上模块名、时间戳等信息,并且会输出到系统的日志文件(/fs/microsd/log)中,无论是在仿真环境还是真实飞控中,都是排查问题的重要工具。

如果你想在全代码搜索某个关键词,别用 VS Code 的全局搜索(太慢),用grep -rn "关键词" src/modules/ --include="*.cpp"这种命令行方式,效率高多了。

6.2 版本管理:看源码前先锁定版本

flutter 开发最烦的是环境依赖问题,PX4 也一样。我给的所有示例都基于v1.13.3。你在参考网上博客时,一定要先看清楚对方的 PX4 版本。

不同版本之间的模块代码结构差异非常大。比如v1.11之前,姿态控制的文件还不叫mc_att_control_main.cpp,可能叫mc_att_control.cpp;v1.14之后又引入了新的工作队列,代码结构又会变。如果你拿着旧教程对比新代码,除了让自己血压升高,学不到任何东西。锁定一个版本,跟到底,是最高效的方式。

6.3 调试的终极武器:uORB订阅与发布

想验证你的猜测,最快的方式不是在代码里胡乱改一顿然后重新编译(那太浪费时间了)。在 SITL 仿真终端里,你可以直接通过uorb指令查看消息流。

比如:

pxh> uorb top vehicle_attitude

它会实时刷新,告诉你这条消息的发布频率(Hz),以及订阅者数量。这会让你一下子明白各个模块之间的依赖关系。同时,你也能通过param set动态修改参数(比如MC_ROLL_P),而不用重新编译,真正做到实时调参。

7. 实战:亲手编写一个自定义模块,体验代码的生命周期

理论知识讲太多容易飘,这里我留个作业,也是本系列后续内容的预告。

进入src/examples/hello目录,简单的看一眼。然后照着它的样子,在src/examples/下新建一个文件夹my_first_module,分别创建CMakeLists.txt和my_first_module.cpp。

CMakeLists.txt 核心内容:

px4_add_module( MODULE modules__examples__my_first_module MAIN my_first_module STACK_MAIN 2000 COMPILE_FLAGS SRCS my_first_module.cpp DEPENDS )

my_first_module.cpp 骨架:

#include <px4_platform_common/px4_config.h> #include <px4_platform_common/tasks.h> #include <px4_platform_common/posix.h> #include <px4_platform_common/log.h> #include <uORB/uORB.h> #include <uORB/topics/vehicle_attitude.h> extern "C" __EXPORT int my_first_module_main(int argc, char *argv[]); int my_first_module_main(int argc, char *argv[]) { PX4_INFO("Hello, PX4! My first module!"); // 订阅一下姿态话题,看能不能读到数据 int sub_fd = orb_subscribe(ORB_ID(vehicle_attitude)); vehicle_attitude_s att{}; orb_copy(ORB_ID(vehicle_attitude), sub_fd, &att); PX4_INFO("Current roll (rad): %.2f", (double)att.roll); return 0; }

在configs/px4_sitl_default.cmake里加入一行:

CONFIG_MODULE_EXAMPLES__MY_FIRST_MODULE=y

然后重新编译:

make px4_sitl gazebo-classic

启动仿真后,在pxh>终端输入my_first_module,如果看到了那句打印信息,恭喜你,你已经完全掌握了 PX4 模块的编写、编译和运行全流程。

这个练习做完,你对 PX4 的“模块生命周期”和“uORB通信”的认知,就比只会敲命令的脚本小子强了一个档次。


第一篇就这么多了。环境搭好、模块生命周期理解了、能自己跑通一个模块,剩下的事情就顺理成章了。后面的文章,我会挑那些大家最关心的、也最影响飞行性能的核心代码开刀,比如 EKF2 状态估计、PID 调参背后的源码逻辑、多旋翼的混控器机制等等。

看完这篇,你可以做的事情是:

  1. 确保自己能在 Ubuntu 上编译出px4_sitl固件并顺利打开 Gazebo。
  2. 试试list和uorb top命令,感受一下任务调度的存在。
  3. 把那 30 行hello module跑通,体验一把面向硬核编程的快感。

如果卡在哪一步过不去,大概率是我前面提到的那些坑——Python 环境、软链接、子模块。在评论区把报错信息扔上来,我看到会回复。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询