简介:APM飞控源码是针对ArduCopter 3.2.1版本的裁剪版开源工程,适合无人机开发者、嵌入式爱好者及航模玩家学习飞行控制核心实现。该版本在保留APM关键功能的同时精简了冗余模块,重点涵盖姿态控制、高度保持、位置锁定、航点导航等算法,并提供PID控制器、传感器处理、飞行模式切换、地面站通信等关键模块的源码阅读路径。压缩包约24.8MB,文件总数暂未标注,内容以ArduCopter目录下的源码工程为主,建议使用Visual Studio配合插件管理编辑。值得留意的是,源码加入了中文注释,可帮助初学者快速理解函数作用、变量含义与关键算法逻辑。目前已有804人学习下载。通过研读和修改这些代码,读者能够掌握传感器数据融合、姿态估计、位置控制以及无线通信交互等技术细节,为进一步定制飞控功能或贡献开源社区打下扎实基础。
1. 拿到 APM 飞控源码之后,第一件事千万别急着看代码
很多刚接触到这个开源飞控项目的同学,第一步都是去解压源码包,然后试图从头到尾读一遍,结果通常是一周后还在libraries/目录里打转,连编译都没跑起来。做飞控二次开发,最忌讳的恰恰是“通读源码”——APM 的代码量摆在那里,模块之间耦合又深,没有一条明确的路线图,很容易在姿态解算和 EKF 滤波这些硬骨头里耗尽耐心。
这篇文章要解决的是实际问题:拿到 APM 飞控源码后,怎么在最短时间内建立起“能编译、能改参、能看日志、能验证效果”的闭环。适合刚接触这个项目、想做毕设或产品原型验证的开发者,也适合那些已经被编译错误折磨到怀疑人生的熟手。不管你是想改一套自定义的固定翼混控逻辑,还是想把卡尔曼滤波换成自己的模型,先把这个闭环跑通,后续所有工作才有附着点。
2. 从源码到可执行固件:认识 APM 工程的骨架
2.1 源码目录到底长什么样,哪些目录必须动、哪些不用管
默认的 APM 源码目录结构,第一眼看上去像一团乱麻,但真正需要你关心的其实就几块。libraries/是核心算法和驱动库,所有飞控逻辑、传感器驱动、通信协议都在这里;ArduCopter/、ArduPlane/、Rover/这些是不同载具类型的顶层应用目录,也就是存放setup()和loop()入口的地方;Tools/里是地面站配套工具、参数转换脚本和一些调试辅助程序。
新手最常见的误区是想把每个目录都看一遍,这完全没有必要。以四旋翼为例,你的主战场就在ArduCopter/和libraries/AP_Math/、libraries/AP_AttitudeControl/这几个目录。至于Tools/下的自动驾驶仪相关工具链,除了编译脚本,大部分目前都用不太上,知道它们存在就行。
进源码的第一个动作,建议先看ArduCopter/ArduCopter.cpp里的setup()函数。它会告诉你初始化顺序:先读取参数、再初始化传感器、然后校准、最后进入主循环。主循环loop()的逻辑相对固定,但里面的调度机制(schedule)决定了不同频率的任务分配,理解了这个才能明白为什么姿态控制是 400Hz、而导航是 10Hz。
2.2 本地编译环境搭建:装对工具链比写代码更重要
APM 官方推荐在 Linux 环境下编译,这主要是为了省掉 Windows 下各种路径和依赖的麻烦。常见的做法是在 Ubuntu 下用apt安装基础编译工具,然后拉取源码仓库并切换到稳定分支。我一般会推荐用 Docker 或虚拟机建立干净的编译环境,这样即使环境搞坏了,重新来一次也就几分钟的事,不用在宿主机上折腾半天。
这里给一套我在新机器上会一步步执行的最小命令序列,每一步都有它存在的理由,不是随便敲的。
# 1. 安装基础编译工具链 gcc-arm-none-eabi 是 STM32 交叉编译的核心 sudo apt update sudo apt install -y build-essential git python3-pip gcc-arm-none-eabi # 2. 拉取源码并切到指定稳定分支,master 分支可能处于开发状态,不建议直接使用 git clone https://github.com/ArduPilot/ardupilot.git cd ardupilot git checkout Copter-4.3.7 # 3. 初始化子模块,这一步下载的是各种库文件依赖 git submodule update --init --recursive这三步做完之后,环境其实还没有完全就绪。因为 APM 的构建还有一个隐性的依赖,如果缺了它,会在编译中途提示找不到 Python 模块。我一般会顺手装掉,别等到报错再回头查,那样更浪费时间。
# 4. 更新 pip 并安装 MAVProxy,这是后续模拟器验证和日志分析都需要的工具 pip3 install --upgrade pymavlink MAVProxy这套命令的核心在于:第 1 步保证交叉编译链存在;第 2 步锁定代码版本,避免开发分支的历史包袱;第 3 步补齐外部库;第 4 步为模拟器验证做铺垫。在实际做二次开发时,如果之后想提交代码回主干,所有改动都应该基于一个明确的稳定分支,不要基于 master 随意变更。
2.3 第一次编译:命令的参数怎么调,出现哪些输出才算成功
环境配好之后,第一次编译通常会有一种“听天由命”的感觉,因为日志滚动非常快,很多警告看起来又很吓人。不用慌,编译器的警告大部分可以安全忽略,真正需要关心的是最后是否生成了.elf和.bin文件。
# 进入四旋翼顶层目录 cd ArduCopter # 以默认的四旋翼配置编译,board 指定硬件平台 sim_vehicle.py -v ArduCopter --console --map这一段其实是模拟器验证的最小路径,而不是直接烧录固件。第一次我强烈建议先用模拟器把编译链和源码逻辑跑通,再上真机。因为模拟器里能看到 MAVLink 的输出、控制台界面和地图窗口,对确认“固件真的构建成功”比直接用 USB 连飞控板要直观得多。
# 如果只是纯粹编译固件,用这个命令 ./waf configure --board Pixhawk1 ./waf copterconfigure指定目标板,copter指定编译载具类型。如果你拿到源码后第一步就直接跑到这一步,被waf的各种报错绕晕的概率会低很多。成功的标志是看到Build failed附近没有红色错误行,并且在build/Pixhawk1/bin/下出现了arducopter.apj文件。
2.4 源码和固件烧录:学会用最保险的方式回到出厂状态
在源码上做修改之前,先搞清楚怎么把固件刷回去,这是给自己留的后悔药。编译生成的是.apj或者.bin文件,常见的刷写方法是用地面站的固件升级入口,或者用dfu-util命令通过 USB 直接烧写。
# 备份当前飞控参数,这一步任何时候改代码前都要做 param download 备份参数文件.param # 用命令行刷写编译好的固件 dfu-util -a 0 -d 0483:df11 --dfuse-address 0x08000000 -D build/Pixhawk1/bin/arducopter.apj注意这里的-d参数一定要和你手里的板子的 USB ID 匹配,不同批次的产品 ID 可能有差异。刷写完建议重新上电并做一次完整的传感器校准,因为固件版本变化后,之前存储在参数区的数据不一定完全兼容,尤其是加速度计和罗盘校准信息。最稳妥的做法是刷完新固件后,用地面站的“重置为默认参数”按钮清一遍,再开始调参。
3. 深入源码修改的实际操作:从改参数默认值到加一个自定义模式
3.1 找对改代码的入口:以飞行模式切换为例,定位源码的关键链路
当你需要在源码里加入一个自定义飞行模式时,上手最稳妥的路径是找一个现有的模式背靠背地照着改。不要从零手写调度逻辑,那是在给自己挖坑。飞行模式切换的核心入口在ArduCopter/mode.cpp的Copter::set_mode()函数里,它做两件事:停止当前模式的控制输出,然后调用新模式的run()方法。
我通常会先用搜索工具确认两个关键宏定义:一个是在defines.h里注册模式 ID,另一个是在mode.h里声明模式类。改完这两处,框架就已经认识新模式了,剩下的是实现它的具体逻辑。
// defines.h 中注册模式枚举,新增一个自定义模式 ID enum FlightMode : uint8_t { STABILIZE = 0, ACRO = 1, ALT_HOLD = 2, AUTO = 3, GUIDED = 4, LOITER = 5, RTL = 6, CIRCLE = 7, LAND = 9, DRIFT = 11, SPORT = 13, FLIP = 14, // 新增模式,紧接最后一个已有枚举,避免破坏现有序列 CUSTOM_MODE = 18, };这个枚举值涉及到地面站协议映射,选一个大数能避开地面站已经固化的模式名称冲突。修改完之后,你会发现在地面站里通过 MAVLink 发送模式切换指令时,如果值匹配的是 CUSTOM_MODE,飞控会接受这次切换请求,但前提是你得实现了对应的run()函数,否则飞控会在日志里报模式执行失败。
3.2 修改姿态控制的默认 PID 参数:从源码层面改变你的飞行手感
很多开发者的诉求不是新增功能,而是调整默认飞行手感。这个完全不一定要改源码,地面站直接改参数就行。但如果你做的是批量生产的原型机,希望在固件出厂时就有合适的参数,那就必须去改源码里的参数默认值了。
这些默认值一般集中在libraries/AP_Notify或者载具类型的Parameters.cpp文件里。以四旋翼的ArduCopter/Parameters.cpp为例,搜索Stabilize P相关关键字,你会看到类似这样的表:
// 姿态控制默认参数定义 AP_Grouped_Parameters PARAMETERS; // P 增益默认值,数值直接影响打杆响应快慢 const AP_Param::GroupInfo Parameters::var_info[] = { // @Param: RATE_P // @DisplayName: 姿态速率环 P 增益 AP_NESTEDGROUPINFO(AP_AttitudeControl, "ATC_", 3), };这里的AP_NESTEDGROUPINFO宏的作用是把AP_AttitudeControl类内的所有参数成员挂载到ATC_前缀下面。想去改默认值,就去定义该参数的初始值,例如把某个速率环的 P 增益默认值从原来的值改成一个更保守数字,只需修改参数构造函数里的初始化列表。但这种修改会影响所有使用默认参数的用户,所以只适合在针对性定制的固件里做,不适合随意动公共主干。
3.3 编译修改后的代码:增量编译和全量编译的取舍
改完代码之后编译,看起来是最简单的一步,但很多人会在这一步开始慌。因为第一次编译的时候很顺利,改了两行代码反而出现各种莫名报错,这通常和编译缓存有关。
waf默认会做增量编译,只重编修改过的模块,这对效率是友好的。但遇到以下情况时,增量编译会出现很奇怪的问题:头文件之间的依赖关系变了、宏定义的数值变了、新增了文件但构建系统没识别到。这时候不要迟疑,直接做一次全量清理重编,二三十秒的时间花得值。
# 清理整个构建产物,注意不要只在根目录执行,要在 waf 所在目录 ./waf clean ./waf configure --board Pixhawk1 ./waf copter如果清理之后还是报错,那就把构建信息输出到文件里,重点看第一条 error 而不是最后一条。有时真正的问题在日志最前面,后面跟着的一串钢印警告都是连带反应。还有就是不要忽略注解里的#warning,这类提示虽然不中断编译,但往往在告诉你你用的某个 API 已经废弃,编译器正在走兼容路径。
3.4 修改日志输出:在源码里埋点打印自定义调试信息
二次开发最痛苦的事之一就是“看不到中间变量的值”。地面站收到的 MAVLink 消息内容相对固定,想观察自己加的中间计算量,就要在源码里主动打日志。APM 用的是内部日志系统不是 printf,因为飞控板没有 stdout。
常用做法是调用logger.Write_Message()往事件日志里写一条字符串。这个动作的代价很低,但对排查逻辑非常关键。如果你改了混控器输出,建议在这段代码上加一行日志:
// 在混控器输出后写入关键控制量,用于事后分析 logger.Write_Message("Mixer output: roll=%f pitch=%f yaw=%f throttle=%f", roll_output, pitch_output, yaw_output, throttle_output);注意这种做法只适合低频调用,如果放在 400Hz 的姿态控制循环里,日志量会爆炸,很快塞满存储介质并且拖慢主循环。经验做法是:只在模式切换、状态机跳转、或者错误恢复这些事件型位置打日志,控制量这类高频数据用遥测下行做曲线观察,不要依赖本地日志。
4. 参数整定与仿真验证:改完代码后必须自己先飞一把
4.1 用软件在环仿真验证源码改动:不炸机的最快捷径
任何对飞控源码的改动,我的规矩都是必须先过软件在环仿真(Software In The Loop),它把调度和逻辑放进一个真实飞行的空气动力学模型里跑一遍,比装到真机上再试错安全得多。
# 启动软件在环仿真,加载自定义固件 sim_vehicle.py -v ArduCopter --map --console -f quad这里的-f参数指定机型,--console拉起地面站控制台界面。仿真起来之后,地面站会自动连接模拟串口,你可以像操作真机一样解锁、起飞、切模式。如果仿真里参数就震荡得不行,真机上大概率直接翻滚,不需要去室外冒险。
几轮仿真跑下来如果都没问题,下一步才值得考虑硬件在环或者真机。仿真环境虽然简化了部分传感器特性,但至少能验证代码调度、模式切换、混控输出这几层没有低级错误。我一般至少做三轮:起飞悬停一次、降落一次、加上一次负载突变的模拟。
4.2 核心参数表:P、I、D 在源码里对应哪些变量,怎么算初始值
做源码级修改的时候,脑海里必须有一张参数地图:地面站显示的数值对应源码里的哪个变量,它是如何参与控制律计算的。我常用的一张对照表如下:
| 地面站参数名 | 源码搜索关键字 | 控制作用 | 初始值调整方向 |
|---|---|---|---|
| ATC_ATC_RAT_RLL_P | rate_roll_p | 副翼轴角速度环比例增益 | 太小打杆迟钝,太大振荡 |
| ATC_ATC_RAT_RLL_I | rate_roll_i | 消除稳态误差,回中能力 | 积分饱和容易超调 |
| ATC_ANG_RLL_P | angle_roll_p | 外环角度环增益 | 决定姿态回中速度 |
| INS_ACCEL_OFFSET | _accel_offset | 加速度计零偏补偿 | 校准后自动写入 |
去找参数具体定义时,直接搜var_info表和对应的class AP_AttitudeControl头文件命名即可,和地面站里显示的名称是一一对应的,没有暗逻辑。
初始值的计算没有银弹。如果你是从零调一套新机型,我会建议把 P 压到默认值的 60%,I 保持默认,D 先设 0,然后逐步在仿真里往上加。很多人一上来就是满参数,结果飞行器在解锁瞬间就触发自我保护,反而浪费了时间。
4.3 仿真日志如何看:用mission planner的曲线图定位问题
仿真跑完之后最重要的动作是导日志。地面站会记录整个飞行的原始 IMU 数据和控制输出,回到地面站界面里,你会看到类似俯仰角速率环的目标值、实际值、输出值这三条曲线。判断稳定性就看目标值和实际值之间的滞后时间,以及输出曲线是否有高频抖动。
一个常见的翻车场景:仿真飞行过程中切到定高模式,高度出现持续振荡。很多人的第一反应是气压计标定问题,实际上很可能是高度环的 P 增益过于激进。这时候不要急着改传感器,先看日志里高度环输出曲线是不是频繁打到饱和边界,如果是,压缩 I 项增量,给输出留出余量。
# 用 pymavlink 一键解析仿真日志并输出关键曲线数据 from pymavlink import mavutil log = mavutil.mavlink_connection('logs/00000001.bin') # 读取 IMU 加速度数据 accel_x = [m.xacc for m in log.recv_match(type='IMU', blocking=False)] print("加速度 X 轴异常值数量:", sum(1 for x in accel_x if abs(x) > 3))这种脚本不用写得很复杂,它的核心价值是让你快速判断日志里是否出现了传感器饱和、数据异常跳变这类肉眼在曲线上不太容易察觉的小问题。
4.4 固件换名与版本标识:如何让地面站认出你编译的版本
很多开发者会遇到一个问题:自己编译的固件刷进去之后,地面站显示的还是原来的版本号,无法区分自己做的改动和原版固件。原因在于编译版本号是从代码里固定的字符串读取的,你没有改动它。
这个字符串在libraries/AP_Vehicle/相关文件里,搜FIRMWARE_VERSION宏定义就能看见。改它并不会影响飞控功能,只会让地面站显示一个新的版本标识,方便你有多个固件并存时快速区分。
// 自定义固件版本标识,建议加上个人后缀和日期,方便区分 #define FIRMWARE_VERSION 4, 3, 7, FIRMWARE_VERSION_TYPE_OFFICIAL #define THISFIRMWARE "ArduCopter V4.3.7 Custom-20240401"改完之后重新编译烧录,地面站版本号就会变成你自定义的名字。这个小习惯能避免很多乌龙:曾经有开发者刷错了固件文件,花费一整天排查为什么参数和代码逻辑对不上,最后发现是版本没区分,白白踩了个坑。
5. 源码二次开发避坑指南:这些血泪经验都很值钱
5.1 现象:编译通过但飞行器解锁后电机立即满油门转动
原因分析:这类问题最常见的原因是解锁前的安全检查被代码改动意外绕过。比如有人在set_mode()里改动代码时,把模式切换的限制条件删掉了,或者不小心调用了电机输出直接置高。电机输出在源码里通常受arming状态机保护,不动它的前提下,单纯改控制参数很少会造成这种问题。
解决办法:把mode.cpp里和arming有关的判断单独摘出来检查。具体做法是在改动之前,先把原仓库的git diff导出,逐行排查哪一行碰触到了电机输出。更快速的定位法是回退所有改动,先确认原始代码能正常解锁,再逐步把改动加回去,每次加完都编译跑一次仿真。从根上解决的方法是:养成在set_mode()加锁的习惯,任何模式切换前都强制检查motors->armed()状态,避免输出通道被短路。
5.2 现象:传感器校准数据无法保存,每次上电都要重新校准
原因分析:这种情况通常和参数存储区域的变更有关。当你修改固件后,如果代码里参数表结构的初始化顺序变了,飞控板上的闪存里旧参数和新的默认参数就会不匹配,于是飞控的表现是“默认参数无法覆盖到存储区”,表现得像“校准数据丢失”。另一种常见原因是编译时使用的硬件平台标识和烧录时的板子不一致,导致区块偏移错乱。
解决办法:刷完新固件后第一件事是连接地面站,执行“重置参数为默认值”的操作,擦除旧的参数区内容。然后立即做一次完整的传感器校准,确认写入成功。之后重启飞控再读参数,如果数值保持住了,说明存储机制恢复正常。为了避免这种问题反复出现,在做任何涉及参数表结构调整的改动之前,先备份参数文件并在修改后第一次刷机时强制做一次参数重置流程。
5.3 现象:修改了代码,但仿真中没有任何反应
原因分析:这是新手最容易碰到的情况,也是最容易忽略的问题:编译的固件版本和sim_vehicle.py启动时用的固件不匹配。你改了源码,编译出的新固件在build/目录下,但启动仿真的命令调用的不是这个新生成的文件,它可能仍然在用一个旧的缓存版本。另外一种可能性是代码逻辑修改的位置不在主控制链路里,压根没被调用到。
解决办法:确认sim_vehicle.py的起始路径是从源码的顶层目录执行的,并且在启动命令里明确指定--frame参数。如果命令没问题,就在改动处写一条明显的日志输出,看仿真启动后日志里是否出现这条消息。没有出现,说明你的代码确实没被执行,顺着调用链往上查。出现但逻辑没生效,那就是改动位置不对,要把注意力放到真正的控制循环路径上。
5.4 现象:刷了固件之后无人机上电没反应或飞控发热异常
原因分析:飞控发热异常听起来玄学,但背后往往是严重的基础错误,比如 USB 串口和主控的接线被反转、电源模块的输出电压和板子工作电压不匹配、或者固件里定义的传感器驱动对应的硬件接口和实际焊接不对应。上电没反应多数是因为刷入的固件和目标板型不一致,导致时钟初始化和外设初始化失败。
解决办法:第一步永远是把安全放在前面,断开所有电机和动力电源,只保留 USB 供电确认飞控板本身能工作。如果 USB 连接时地面站能识别,但外接电源时无响应,优先量测电源输入端的电压和纹波。缺少合适的示波器时,可以看电源模块旁边的指示灯是否点亮,不亮先查电源通道。如果 USB 和电源都不能识别,检查刷写时的板型参数,用dfu-util连接后读取板子的完整设备描述,和编译时--board参数交叉比对。大多数发热异常在排除供电接线问题之后就不再出现了。
5.5 现象:地面站能连上,但日志下载到一半就断开
原因分析:这是日志存储格式和下载通道的经典坑。高速写入日志会占用大量带宽通道,而下载日志又是同一通道,冲突就会造成连接中断。另一个诱因是飞控上电后一直在反复写错误日志,把存储区写满了,再触发下载时协议栈就卡住。
解决办法:不要用无线链路下载完整日志,USB 数据线是最靠谱的通道。如果下载到一半必断,可以先用脚本工具按时间范围分块下载,比如只下载最后三分钟的飞行日志做分析。同时检查日志类型里是否开启了调试级别的记录,那个数据量是指数级增长的,第一次排查时建议只开核心 IMU 和控制类日志,其余全部关掉。分析完需要的数据之后,再做一次全擦除,给存储区腾出完整空间。
6. 从源码到二次开发的高阶技巧:用版本管理做一个有章法的开发者
在 APM 源码上做二次开发,成熟的开发者一定会在动手前建一个自己的分支。我自己的习惯是创建一个独立的长期分支,专门存放针对某个项目的改动,上游稳定分支只做同步更新,绝不直接在上面写个人代码。这样才能在后续合并上游修复时不会把所有自定义改动都撞碎。
# 创建自己的长期开发分支,基于刚拉取的稳定版本 git checkout -b my-project/copter-4.3.7-custom从这里开始,你的所有改动就和上游切割开了,想对比改动范围随时用git diff查看。每隔一段时间,把远程稳定分支合并进自己的分支,解决冲突的过程虽然烦人,但比最后一次性合并所有历史改动轻松得多。不要尝试维护一个“零冲突”的本地仓库,那意味着你的改动和主干越来越远。
另一个容易忽略的验证习惯是:每次晚上或周末修改完代码,哪怕只是改了一个常量,也要留一个飞行日志存档。存档命名方式要能一眼看出大致改动内容,比如20240415-loiter-p-validation.bin。当你的调试遇到问题时,翻日志才知道是改动前还是改动后引入的异常,省掉从头排查的力气。
说到调参,一个在很多项目里都管用的边界做法是:不要在真实设备上调到完美状态,而应该刻意把参数调成“稍微保守但完全可控”的状态。原因在于,真机试飞的时间成本昂贵,气流环境也不可复制,过于激进的参数往往在换一个场地之后就失效。你会需要一套能应对多数情况的基础参数,而不是某个特定环境下的最优解。
直到今天,我依然保留着一个习惯:改完代码后先对自己的分支跑一次全量编译和仿真,再提交改动信息。这套流程看起来多花了十几分钟,却让我少处理了很多真机上的意外状况。源码级的飞控开发没有捷径,但一个清晰的、可反复执行的开发循环会帮你把踩坑变成经验,而不是教训。希望这些经验能帮你在 APM 源码的世界里,少走一段弯路。
本文还有配套的精品资源,点击获取