一个从大学就开始折腾航模、后来搞了几年无人机开发的人,我见过太多同学拿到 Pixhawk 之后做的第一件事,就是把 PX4 源码从 GitHub 上 clone 下来,然后盯着src/modules目录发呆两小时,最后默默删掉仓库,关掉电脑。
飞控二次开发,真不是这么玩的。
这个领域确实容易让人迷失方向:网上一搜“飞控二次开发”,出来的不是源码仓库就是晦涩难懂的论文;再点开几个视频,发现大家都在玩树莓派、移植视觉、跑自主飞行,好像每个人都已经搭好了完整的无人机系统。但实际上,飞控二次开发是有清晰路径的,而且它比其他工业软件(比如 Creo、NX、QGIS 那些二次开发)要直观得多——因为你能立刻看到飞机在天上飞。
这篇文章我想把飞控二次开发的两条主流路径一次讲透:一条是外挂树莓派,不碰飞控内部逻辑,靠串口通信在外围做文章;另一条是自定义模块,直接住进飞控固件里,改的是控制算法和任务逻辑。适合刚准备入门的航模爱好者、无人机开发者、以及想把 ROS 和飞控结合起来的同学参考。
1. 为什么劝你别一上来就啃源码
先说说我的观点:源码不是不能啃,而要等跑通流程之后再啃。
很多新手以为二次开发 = 读源码 + 改源码。这个想法在飞控领域尤其容易翻车,原因是飞控的代码复杂度远高于普通应用软件。以 PX4 为例,它包含传感器驱动、状态估计(EKF)、姿态控制、位置控制、导航任务、通信协议、安全机制等一大堆子系统,整个仓库几十万行 C/C++ 代码,实际编译出来的固件还依赖实时操作系统(NuttX)。别说读懂,想找到“改哪一行能起效”都是个技术活。
还有一个更现实的问题:源码改了,你怎么验证?
飞控不像普通的软件,改坏了重新装一个就能继续。它跑在 4~6 个旋翼上面,任何逻辑错误都可能导致炸机。如果你连飞控和地面站的通信都还没打通,连机载日志都不会看,就直接去改 SIM 模式的源码,出了问题你根本不知道是自己写错了还是系统本来就那样。
所以我一直建议初学者把二次开发分成两个阶段:
- 第一阶段:不改飞控内部,通过对外接口(串口、MAVLink 协议、MAVSDK)控制它。
- 第二阶段:对系统有一定理解之后,再进入源码层,写自定义模块。
这两个阶段不是对立的,而是递进的。很多人担心“外挂树莓派”是不是太低级,其实完全不是。你看工业界真正落地的无人机方案,绝大多数都是“飞控+机载电脑”架构:树莓派、Jetson 或者香橙派作为上位机,飞控作为执行单元。真正做到源码级别的二次开发,反而集中在飞控厂商和极少数算法团队。
2. 飞控二次开发的完整路径地图
在进入具体操作之前,先梳理一下飞控二次开发的四个层级,这样你就能明白自己现在站在哪里,下一步该往哪走。
2.1 四个开发层级,对应不同能力要求
- 第一层:参数级。通过地面站改 PID、改飞行模式、调电调和遥控器曲线。这是最基础的一层,几乎每个人都经历过。
- 第二层:外设级。给飞控接 GPS、数传、摄像头云台、光流传感器、LED 灯带。这需要懂一些硬件接线和串口配置,但不需要动飞控内部逻辑。
- 第三层:外挂计算单元(机载电脑)。通过树莓派、Jetson 等外部计算设备连接飞控。这是目前应用最广、投资回报最高的一种二次开发方式,也是本文第一条路径的重点。
- 第四层:源码与自定义模块。直接修改或新增飞控固件中的模块,比如自定义控制律、传感器融合算法、特殊任务逻辑。这是本文第二条路径的重点。
很多人一上来就把目标定在第四层,忽略了前三层的积累。这就像新司机非要学漂移,连油门刹车都没踩熟,结果可想而知。
2.2 两条主线:外挂树莓派 vs 自定义模块
一句话总结这两条路线的区别:外挂树莓派是在飞控外面加一个“外脑”,自定义模块是给飞控本身“换脑”。
外挂树莓派的好处非常明显:开发语言用 Python,生态极其丰富,图像处理、机器学习、ROS 库都能直接用;写错了代码不会影响飞控稳定性,最坏结果就是树莓派重启一下;调试起来也方便,连个屏幕看输出、SSH 上去跑脚本都行。它的缺点是高频率、高实时性的任务做不了,毕竟串口通信本身有延迟,而且飞控和树莓派是两套独立的系统,协同工作会有一定的架构复杂度。
自定义模块则完全不同。它是把代码编译进飞控固件,直接跑在 STM32 等单片机芯片上,实时性有硬件级保证。比如你想实现一个新的滤波算法、一条自定义的飞行任务,或者想在姿态控制里加一个补偿项,这些绕开飞控内部是做不到的。但代价是开发门槛高:需要懂 C/C++、懂嵌入式编译、懂 uORB 消息机制,还要承担刷固件失败导致飞控变砖的风险。
我个人给你的建议是:如果你过去的工作重心在应用层,比如视觉避障、航线规划、物流配送、电力巡检,优先走外挂树莓派路线;如果你对飞控本身感兴趣,想深入系统内部,那自定义模块这条路早晚要过。
3. 路径一:外挂树莓派,用 Python 给飞控加外挂
这是我认为最容易跑通、也最容易被忽视的一条路。它的本质很简单:树莓派通过串口和飞控通信,飞控把姿态、位置、电压等状态告诉树莓派,树莓派再根据你的逻辑发出控制指令。
拿一套常见的穿越机配置举例:LQRC APEX 小胡子 5 寸机架 + SpeedyBee F405 飞控 + 55A 电调,这是很多玩家跑视觉穿越机的标配。F405 飞控从型号上就能看出来用的是 STM32F405 芯片,算力有限,跑不了复杂的视觉算法,但它可以稳定地执行姿态控制。你在它上面挂一个树莓派,树莓派负责把摄像头画面算得出哪里有障碍物,再把“该往左躲”的决策通过串口告诉飞控,飞控负责真正把飞机飞过去。这就是“外挂”的核心价值:各干各的擅长的事。
3.1 硬件接线:一根杜邦线解决通信连接
树莓派和飞控之间的通信,最简单可靠的方式是 UART 异步串口。树莓派 4B 或 5 的 GPIO 排针上,14 脚是 TXD、15 脚是 RXD,把这两个脚接到飞控的 TELEM 串口上,再连通地线,就完成了物理连接。
注意三个坑:
- 发射接接收,接收接发射。树莓派的 TXD 要接飞控的 RX,树莓派的 RXD 要接飞控的 TX。很多第一次接线的人栽在这里。
- 一定要共地。树莓派的 GND 和飞控的 GND 要连在一起,否则串口数据会出现乱码。
- 飞控串口电平一般是 3.3V,树莓派的 GPIO 也是 3.3V,可以直连。如果飞控板子比较老,要注意确认电平是否兼容,不匹配就需要电平转换模块。
接好线之后,在树莓派端查一下端口名,通常是/dev/ttyAMA0或/dev/serial0。如果用的是树莓派 4B/5,默认硬件串口可能被蓝牙占用,需要做一步处理,后面会细说。
3.2 飞控端串口配置,别漏掉这两个关键项
硬件接好了,飞控还得告诉它“老子的这个串口要用来跟树莓派说话”。
以 PX4 飞控为例,连接 QGroundControl 后,进入参数设置界面,找到MAV_1_CONFIG,把它设置为你接线的那个串口编号(比如 TELEM2)。然后设置MAV_1_BAUD为 57600。两个参数都要改,只改一个飞控不会正常输出 MAVLink 数据。
如果是 ArduPilot 固件,对应的是SERIAL2_PROTOCOL=2、SERIAL2_BAUD=57。参数名略有差异,但逻辑一模一样:启用串口、指定协议、设置波特率。
如果你用的是 Betaflight 穿越机飞控,情况稍有不同。Betaflight 默认用 MSP 协议和外部设备通信,在 CLI 里输入resource查看可用 UART,然后用serial 1 0 115200 57600 0 115200这种命令把串口配置成 MSP 模式。树莓派端再配合msp相关的 Python 库来读取姿态。这一块略显折腾,但原理上还是串口通信那点事。
3.3 树莓派端装好环境:pymavlink 是主角
树莓派端的工作,本质上是把 MAVLink 协议用起来。
MAVLink 是 Pixhawk 项目组制定的无人机通信协议,它定义了飞控和地面站、机载电脑之间通信的消息格式。好消息是你不需要手写协议解析,Python 生态里已经有现成的库:pymavlink和dronekit。
在树莓派上执行:
sudo apt update sudo apt install python3-pip sudo usermod -a -G dialout $USER pip3 install pymavlink pyserialusermod那一步是把当前用户加入dialout组,否则你很可能遇到串口权限不够的问题。改完组之后需要重新登录或者重启树莓派。
如果 pip 安装速度慢,可以把 pip 源换成国内镜像,比如清华源或阿里云源,具体做法是编辑~/.pip/pip.conf。这个属于基础操作,就不展开了。
3.4 实战:读取姿态数据,先让数据动起来
环境装完之后,第一件事不是急着写控制逻辑,而是先确认树莓派能收到飞控的数据。
我这里给一个最小可用的读取代码:
from pymavlink import mavutil # 串口设备名和波特率要和飞控端配置一致 master = mavutil.mavlink_connection('/dev/ttyAMA0', baud=57600) # 等待飞控心跳包 master.wait_heartbeat() print("心跳连接成功,飞控已上线") while True: msg = master.recv_match(type='ATTITUDE', blocking=True) if msg: print(f"roll: {msg.roll:.2f} pitch: {msg.pitch:.2f} yaw: {msg.yaw:.2f}")运行之后,如果串口线和飞控参数都配置正确,你会看到终端里不断滚出当前的姿态角数据。手动晃动一下飞控,数值会跟着变化。看到这个画面,说明“外挂”这条路已经打通了一半。
如果你手边有树莓派官方摄像头 OV5647,还可以同时把画面推上来自主的视觉任务:树莓派计算视野里的目标位置,再把偏差换算成控制指令发给飞控。很多视觉避障和循迹飞行的原型就是这么搭出来的。
3.5 实战:从树莓派发送控制指令
读数据只是第一步,真正的二次开发是要让树莓派作为“大脑”去指挥飞控。
这里以一个常见的解锁并起飞动作为例:
import time from pymavlink import mavutil master = mavutil.mavlink_connection('/dev/ttyAMA0', baud=57600) master.wait_heartbeat() # 切换飞控模式到 GUIDED(引导模式) mode = 'GUIDED' mode_id = master.mode_mapping()[mode] master.set_mode(mode_id) master.arm_disarm(True) time.sleep(2) # 发送起飞指令,目标高度 10 米 master.mav.command_long_send( master.target_system, master.target_component, mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, 0, 0, 0, 0, 0, 0, 0, 10 )如果你想做更细粒度的航点控制,比如让飞机飞到指定的经纬度和高度,可以发送SET_POSITION_TARGET_GLOBAL_INT消息。这类消息的参数比较多,建议去查 pymavlink 的官方文档,这里不展开详细字段了。
提示:在室内测试时一定要绑好机架或者去掉桨叶,相信我,每个人在第一次跑通控制脚本时都会手忙脚乱。
关于代码里没有解释清楚的协议细节,我补充一个经验:MAVLink 消息分为请求和命令两类,很多新手只记得发命令,却忘了检查飞控是否真的执行了。调试阶段建议把recv_match打印出来,看飞控返没返回COMMAND_ACK,只有收到 ACK 才代表命令真正被接受。
另外,如果你的飞控是 Betaflight 系统的,上面这套 MAVLink 代码不适用,记得找 MSP 协议的库。
4. 路径二:自定义模块,真正住进飞控内部
外挂树莓派玩熟之后,你会发现有些事在外部做不了:比如你想给飞控加一个“检测到剧烈震动时自动降低油门响应”的功能,或者你想实现一种飞控本身没有的飞行模式。这时候就需要进入自定义模块的世界了。
这条路径以 PX4 为例来讲,因为 PX4 的模块化设计在开源飞控里是最清晰的,非常适合学习。
4.1 先用仿真跑起来:开发环境搭建
自定义模块开发的第一步,不是买硬件,而是把仿真环境跑起来。
PX4 官方支持 Gazebo 仿真。在你的开发机(不需要是树莓派)上执行:
git clone --recursive https://github.com/PX4/PX4-Autopilot.git cd PX4-Autopilot make px4_sitl gazebo第一次编译会下载工具链和依赖库,时间根据网络情况从十几分钟到一个小时不等。如果卡在下载环节,优先检查网络,并考虑使用国内镜像源加速。
仿真环境启动后,你会看到一个虚拟的无人机在 Gazebo 里出现,这个时候你就可以在不炸机的前提下,随便折腾代码,重启模拟器就恢复了。我个人强烈建议:自定义模块开发的前期,所有测试都在仿真中进行,等逻辑稳定后再刷到真机上。
4.2 摸清模块长什么样:uORB 通信模型
PX4 内部模块之间不直接调用函数,而是通过一个叫 uORB 的消息总线进行通信。每个模块要么订阅(subscribe)自己关心的话题,要么发布(publish)自己产出的数据。
举个例子:姿态估计模块计算完飞机姿态后,会发布vehicle_attitude话题。姿态控制模块订阅这个话题,拿到姿态数据后计算控制量,再发布给电机驱动模块。整个过程像一个公开的布告栏:任何人可以把信息贴上去,任何人也可以来读。
理解这个模型,是自定义模块开发的关键。你不需要关心别人怎么实现的,只需要知道你需要的消息在哪个话题上,以及它包含什么字段。常用的话题包括:
vehicle_attitude:姿态四元数vehicle_local_position:本地位置和速度sensor_combined:传感器原始数据battery_status:电池电压和剩余电量
4.3 写一个自定义模块需要准备什么
一个最基本的 PX4 自定义模块,通常包含三个部分:模块入口程序、CMakeLists.txt 编译配置文件、启动脚本。
模块入口程序的简化示意:
#include <px4_platform_common/px4_config.h> #include <px4_platform_common/tasks.h> #include <uORB/uORB.h> #include <uORB/topics/vehicle_attitude.h> extern "C" __EXPORT int my_hello_main(int argc, char *argv[]); int my_hello_main(int argc, char *argv[]) { int sub = orb_subscribe(ORB_ID(vehicle_attitude)); struct vehicle_attitude_s att{}; while (true) { orb_copy(ORB_ID(vehicle_attitude), sub, &att); PX4_INFO("roll: %f pitch: %f", (double)att.roll, (double)att.pitch); px4_usleep(100000); } return 0; }CMakeLists.txt 里声明模块:
px4_add_module( MODULE modules__my_hello MAIN my_hello STACK_MAIN 2000 SRCS my_hello.cpp DEPENDS )这段代码的作用是订阅姿态话题,然后每 100 毫秒打印一次滚转和俯仰角。虽然是简化版,但它体现了自定义模块的核心结构:入口函数、订阅、在循环里处理数据。
正式的 PX4 模块会基于ModuleBase类编写,支持start/stop/status等命令行指令,方便在 PX4 控制台里动态管理。这里不展开写完整类框架,你到对应版本的src/examples目录里找一个 hello 模块参考即可。
4.4 编译部署流程与版本注意点
模块代码写好后,把它放到src/modules/my_hello/目录下,然后执行:
make px4_fmu-v5编译完成后会生成固件文件。刷固件可以用 QGroundControl 的地面站界面选择“自定义固件”手动刷入,也可以用编译环境直接上传。注意:你编出来的固件必须和飞控板型号匹配,比如 SpeedyBee F405 就不能刷 px4_fmu-v5 这种针对 Pixhawk 4 的固件。刷错固件轻则飞控不启动,重则变砖。
启动自定义模块这一步也容易踩坑。不同 PX4 版本里启动脚本的位置不一样,有的在ROMFS/px4fmu_common/init.d/,有的在ROMFS/px4fmu_common/init.d-posix/,还有的可能在板级配置目录里。最靠谱的做法是参照你当前版本中其他模块的注册方式,照着抄一遍。
注意:PX4 的版本更新速度很快,跨版本之间的模块 API、编译系统、启动脚本目录都有变动。网上搜到的教程大概率是旧版本的,遇到编译报错先检查版本兼容性。
5. 两条路径怎么选?我给出一个参考决策表
很多人在群里问“我有树莓派,要不要再买一块 Pixhawk”、“我该学 Python 还是学 C++ 做飞控二次开发”。这种问题其实没有标准答案,但可以从实际需求倒推。
| 判断维度 | 外挂树莓派 | 自定义模块 |
|---|---|---|
| 你擅长的语言 | Python、ROS | C/C++、嵌入式 |
| 改动的位置 | 飞控外部逻辑 | 飞控固件内部 |
| 实时性要求 | 一般,串口有延迟 | 高,直接进调度器 |
| 典型应用 | 视觉避障、航线规划、环境感知 | 自定义控制律、传感器融合、故障检测 |
| 故障代价 | 树莓派重启即可 | 飞控失灵,可能需要重新刷固件 |
| 上手时间 | 顺利的话半天跑通 demo | 保守估计需要一周以上 |
| 硬件需求 | 额外一台树莓派/机载电脑 | 一块支持对应固件的飞控板 |
说实话,这两条路径并不互斥,反而是配合使用的。我有一个很典型的经历:最开始我用树莓派给飞控写了一个“电量低于 20% 自动返航”的外挂脚本,跑得很欢乐。后来发现串口链路偶尔抽风,如果树莓派死机了,整个返航逻辑也跟着失效。之后我把同样的逻辑下沉为飞控内部的一个自定义模块,虽然开发周期长了不少,但可靠性高多了。
所以我的建议是:先走通外挂路线,把任务逻辑、控制指令的来龙去脉搞清楚,等你的需求对实时性、可靠性提出更高要求时,再考虑把它迁移成自定义模块。
6. 常见问题与排查技巧实录
做飞控二次开发,遇到的问题五花八门,但真正卡住绝大多数人的就那几个。我把这些年踩过的坑整理成一张速查表:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 树莓派收不到飞控心跳 | 接线错误、未配置串口参数、波特率不一致 | 先检查 TX/RX 是否交叉,再检查飞控端MAV_1_CONFIG和MAV_1_BAUD,最后确认波特率是否匹配 |
| 串口打开提示 Permission denied | 当前用户不在 dialout 组 | 执行sudo usermod -a -G dialout $USER,重新登录后生效 |
| 树莓派串口数据乱码 | 未共地、波特率错误 | 确认 GND 是否连通,树莓派和飞控波特率设为一致 |
| 树莓派 4B 串口没有输出 | 硬件串口被蓝牙占用 | 在/boot/config.txt中增加dtoverlay=disable-bt,然后重启 |
| 刷固件后飞控无反应 | 固件和板子型号不匹配 | 确认你使用的固件目标和你飞控的主控芯片一致 |
| 仿真启动后画面卡顿 | Gazebo 资源占用高 | 降低仿真分辨率,关掉其他大型软件 |
| 树莓派突然重启 | 供电不足 | 树莓派 4B 对 5V 电流要求高,不要只靠飞控的 BEC 供电,用独立电源模块 |
这里重点展开几个容易忽略的细节:
第一个是树莓派 4B 的串口问题。树莓派 4B 的硬件 UART 默认分配给了蓝牙模块,如果不在/boot/config.txt里加上dtoverlay=disable-bt,你用/dev/ttyAMA0收到的数据可能全是噪音。树莓派 5 的默认状态又不太一样,拿到新板子先查一下当前系统的串口映射情况再动手。
第二个是供电问题。SpeedyBee F405 这类穿越机飞控的 BEC 主要是给接收机、图传供电的,电流余量不会太大。树莓派 4B 满载时可以吃到 3A 的电流,直接从飞控取电非常危险,轻则树莓派重启,重则把飞控的电调供电部分烧掉。我的做法是给树莓派单独配一个 5V/5A 的 BEC,或者直接用独立的充电宝供电,跟动力系统彻底隔离。
第三个是 GPS 场景。很多人把树莓派 3B+ 接了 GPS 模块做定位,但忘了 GPS 模块和飞控之间可能存在频率冲突,导致搜星很慢。树莓派端的 GPS 和飞控端的 GPS 最好错开安装位置,避免互相干扰。
第四个是摄像头权限问题。树莓派接 OV5647 摄像头后,如果运行 OpenCV 提示无法打开设备,多半是没启用摄像头接口,或者当前用户没有 video 组权限。在树莓派终端执行sudo raspistill -o test.jpg,如果这个都出不了图,就说明摄像头驱动层面有问题,别急着调代码。
7. 一点个人经验:先跑通,再深入
做飞控二次开发这几年,我最大的体会是:这个领域不缺少资料,缺少的是循序渐进的节奏感。
很多人在第一个星期就试图搞清楚 PX4 所有模块的源码,结果被 EKF、卡尔曼滤波、控制分配这些名词劝退。而如果你换一个思路,先让树莓派和飞控对上话,再用 MAVLink 下发指令,亲眼看到飞机按照你的命令行进动作,这种正反馈会激发你持续深入的动力。等到外挂逻辑写得顺手了,你对飞控工作方式的理解已经足够支撑你去读源码、写模块了。
还有一个实用技巧想分享:无论走哪条路,都要养成看日志的习惯。外挂树莓派阶段,把你脚本里的关键变量都记录到文件里;自定义模块阶段,善用 PX4 的日志系统和logger模块。真机飞行前几天,在仿真环境里反复测试你写的所有边缘场景。我认识不少开发者,就是靠着一份详尽的日志,在一次炸机之后定位到了飞控内部一个极其隐蔽的时序问题。
飞控二次开发这条路并不难,难的是别在一开始就用错方法。从外挂树莓派开始,让 Python 成为你探索天空的入口;等你想往深了走,再一头扎进源码,那时你看到的将不是一个陌生的代码海洋,而是一套你已经熟悉其行为的系统,正在等待你用代码给它注入新的灵魂。