
兄弟们今天开始搞点硬核的咱们来聊聊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这条命令干了什么它会去编译 NuttX 相关的工具链生成构建文件。它会扫描所有模块的CMakeLists.txt生成代码。它会编译所有模块最后链接成一个可执行文件px4。它会启动 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 地面站连接确认通信链路这里要装一个 QGroundControlQGC它是我们和 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()函数里疯狂写逻辑而是实例化一个模块类。调用module.task_spawn()创建新线程。新线程执行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.cppv1.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_MODULEy然后重新编译make px4_sitl gazebo-classic启动仿真后在pxh终端输入my_first_module如果看到了那句打印信息恭喜你你已经完全掌握了 PX4 模块的编写、编译和运行全流程。这个练习做完你对 PX4 的“模块生命周期”和“uORB通信”的认知就比只会敲命令的脚本小子强了一个档次。第一篇就这么多了。环境搭好、模块生命周期理解了、能自己跑通一个模块剩下的事情就顺理成章了。后面的文章我会挑那些大家最关心的、也最影响飞行性能的核心代码开刀比如 EKF2 状态估计、PID 调参背后的源码逻辑、多旋翼的混控器机制等等。看完这篇你可以做的事情是确保自己能在 Ubuntu 上编译出px4_sitl固件并顺利打开 Gazebo。试试list和uorb top命令感受一下任务调度的存在。把那 30 行hello module跑通体验一把面向硬核编程的快感。如果卡在哪一步过不去大概率是我前面提到的那些坑——Python 环境、软链接、子模块。在评论区把报错信息扔上来我看到会回复。