1. 为什么值得花时间啃下编译这块硬骨头
很多刚接触开源飞控的朋友,拿到源码之后第一反应就是找现成的固件刷进去,能飞就行。但只要你打算改一行控制参数、加一个自定义传感器、或者把飞控移植到一块非官方推荐的板子上,编译和构建这道坎就绕不过去。Ardupilot 作为目前覆盖面最广的开源自动驾驶仪固件之一,支持的载具类型从多旋翼、固定翼到水下机器人、地面车辆,代码库庞大,构建系统也经历过好几轮演进。你如果只是照着某个教程敲两行命令,遇到报错就抓瞎,那后面改代码、调参数、做二次开发基本寸步难行。
这篇内容面向的是已经决定深入 Ardupilot 二次开发、但被编译环境卡住的开发者。我会把整个构建流程拆开讲清楚:为什么官方推荐这套工具链、waf 构建系统到底在背后做了什么、不同载具的编译目标怎么选、交叉编译工具链怎么配、遇到最常见的几类报错怎么排查。看完之后你应该能做到:在一台干净的机器上,从零把 Ardupilot 源码编译成可以烧录的固件,并且知道每一步在干什么,出了问题知道往哪个方向查。
需要先说明一点,Ardupilot 的构建体系这几年变化不小,早期用 make,后来全面转向 waf,目录结构、命令写法、依赖管理都跟老教程对不上。网上很多帖子还是几年前的写法,直接照抄大概率失败。所以我会以当前主流的 waf 构建流程为主线,把那些容易踩坑的地方标出来。另外,编译这件事跟你的操作系统、Python 版本、工具链版本强相关,我会给出经过验证的组合,而不是泛泛地说“装个编译器就行”。
2. 构建系统整体设计与选型思路
2.1 为什么 Ardupilot 最终选择了 waf
要理解编译流程,先得理解构建系统为什么长这样。Ardupilot 代码库里有上千个源文件,涉及不同的处理器架构(STM32 系列为主,也有 Linux 平台的原生编译),还要针对几十种不同的飞控板生成不同的固件。早期用 Makefile 的时候,每加一块新板子就要写一堆重复的规则,维护成本极高,而且增量编译慢得让人抓狂。
waf 是一个基于 Python 的构建框架,它的核心优势在于:配置和构建分离、依赖关系自动推导、支持并行编译、脚本用 Python 写所以可读性和可扩展性都好。Ardupilot 把板子定义、工具链路径、编译选项全部抽成了 Python 配置,新增一块板子往往只需要加一个配置文件,不用动核心构建逻辑。这就是为什么现在官方文档里清一色是waf命令,而不是make。
从使用者的角度看,waf 的工作分两个阶段:configure(配置)和build(构建)。配置阶段会检测你的工具链、Python 环境、目标板子,把结果写进一个缓存;构建阶段根据配置去编译。理解这个两阶段模型很关键,因为很多报错其实发生在配置阶段,而不是编译阶段,你得知道去哪看日志。
2.2 源码目录结构里藏着的信息
拿到源码之后别急着敲命令,先花五分钟看看目录结构,能省下后面很多困惑。顶层大致有这么几个关键目录:
libraries/:所有核心库,比如姿态控制、传感器驱动、通信协议,这是你二次开发最常改的地方。ArduCopter/、ArduPlane/、ArduRover/、ArduSub/:各个载具的主程序入口,每个目录下有自己独立的参数定义和主循环。modules/:第三方子模块,比如 mavlink 协议库、DroneCAN 协议栈,这些是通过 git submodule 管理的。Tools/:构建脚本、工具链定义、各种辅助脚本都在这里,Tools/scripts/下有很多实用工具。build/:编译产物目录,配置完成后才会生成,里面按板子名分文件夹。
这里有个新手最容易忽略的点:modules/目录下的内容是子模块,如果你 clone 源码的时候没加--recursive,这些目录是空的,编译必然失败。而且子模块的版本必须和主仓库匹配,手动去更新反而容易出问题。所以第一步永远是确认子模块完整。
2.3 工具链选型的逻辑
Ardupilot 跑在 STM32 这类 ARM Cortex-M 芯片上,你的开发机大概率是 x86 的 Linux 或 Windows,所以需要交叉编译工具链——也就是在 x86 上生成 ARM 能执行的机器码。官方推荐的是arm-none-eabi-gcc这一套,eabi 表示嵌入式应用二进制接口。
为什么版本这么重要?因为不同版本的 GCC 对 C++ 标准的支持、优化行为、甚至某些内联汇编的处理都不一样。Ardupilot 代码里用了不少 C++11/14 的特性,太老的 GCC 编不过;而太新的 GCC 有时候会因为更严格的检查报出一堆警告甚至错误。官方在Tools/scripts/里其实有安装脚本,会拉取经过验证的版本,这是最省心的做法。我强烈建议不要用系统包管理器自带的gcc-arm-none-eabi,版本往往对不上,后面会浪费大量时间。
3. 核心环境搭建与实操要点
3.1 操作系统与基础依赖
我实测下来最顺的组合是 Ubuntu 20.04 或 22.04 的 LTS 版本,Python 用 3.8 到 3.10 之间。太新的发行版有时候 Python 版本过高,某些依赖包还没跟上,反而添乱。Windows 用户建议用 WSL2,原生 Windows 下编译虽然官方也支持,但路径、换行符、权限这些问题会时不时冒出来,WSL2 里跑 Linux 流程省心得多。
基础依赖这块,官方提供了一个一键脚本Tools/environment_install/install-prereqs-ubuntu.sh,它会装好编译器、Python 包、必要的库。但我要提醒的是,这个脚本会往你的 shell 配置文件里写环境变量,装完之后要重新登录或者手动 source 一下,否则PATH里没有工具链,waf configure会报找不到编译器。
手动装的话,核心依赖包括:git、python3、python3-pip、python3-dev、gcc-arm-none-eabi(但建议用官方脚本装的版本)、g++、make、ccache。其中ccache是个宝贝,它缓存编译结果,第二次编译同样的文件几乎是瞬间完成,改代码后增量编译能快好几倍,强烈建议装上。
3.2 拉取源码的正确姿势
克隆命令本身不复杂,但细节决定成败:
git clone --recursive https://github.com/ArduPilot/ardupilot.git cd ardupilot git submodule update --init --recursive--recursive是关键,它会把子模块一起拉下来。如果你已经 clone 了但忘了加这个参数,第二条命令能补救。拉完之后进modules/看看,mavlink、DroneCAN这些目录里应该有内容,空的就说明子模块没拉全。
这里有个坑:子模块拉取经常因为网络问题中断,尤其是mavlink这种大仓库。如果中断了,重新执行git submodule update --init --recursive就行,git 会接着没下完的继续。实在拉不下来,可以配置一下 git 的代理设置,或者换个时间段重试。拉完之后用git submodule status检查一下,每个子模块前面应该是一个 commit hash,如果前面有个减号,说明还没初始化。
3.3 工具链安装与验证
用官方脚本装工具链是最稳的:
Tools/environment_install/install-prereqs-ubuntu.sh -y装完之后,重新打开终端,验证一下:
arm-none-eabi-gcc --version应该能看到版本号。如果提示 command not found,检查一下~/.bashrc或者~/.profile里有没有被写入工具链路径,手动 source 一下。我遇到过脚本写入了路径但当前终端没生效的情况,白白排查了半天。
注意:不要同时装系统自带的
gcc-arm-none-eabi和官方脚本装的版本,两者路径冲突会导致编译时用到错误的编译器,报出莫名其妙的链接错误。如果之前装过系统的,先卸载干净。
3.4 Python 环境隔离的必要性
Ardupilot 的构建脚本依赖一堆 Python 包,比如pymavlink、future、lxml等。直接装在系统 Python 里不是不行,但一旦你机器上还有别的项目依赖不同版本的包,就会打架。我的习惯是用venv建一个独立环境:
python3 -m venv ardupilot-env source ardupilot-env/bin/activate pip install --upgrade pip pip install pymavlink future lxml激活环境后再执行 waf 命令,这样依赖隔离得干干净净。官方脚本其实也会装这些包,但装到系统里,用 venv 的话需要自己补一下。这一步不是必须,但能避免很多“昨天还能编今天就不行了”的诡异问题。
4. 编译流程完整实操
4.1 配置阶段:告诉 waf 你要编什么
配置是编译的前置步骤,命令格式是:
./waf configure --board <板子名><板子名>是目标飞控板的标识,比如Pixhawk1、CubeOrange、MatekF405等等。完整的板子列表在Tools/scripts/或者libraries/AP_HAL_ChibiOS/hwdef/目录下,每个板子一个文件夹,文件夹名就是板子名。你可以直接ls libraries/AP_HAL_ChibiOS/hwdef/看看支持哪些。
配置阶段 waf 会做几件事:检测工具链、读取板子定义(包括芯片型号、内存布局、外设配置)、生成构建缓存。如果这一步报错,通常是工具链没找到或者板子名拼错了。配置成功后会在build/<板子名>/下生成一堆文件,包括最终的链接脚本。
有个实用技巧:如果你只是想验证代码能不能编过,不针对特定硬件,可以用--board linux做原生编译,速度快很多,适合改纯算法逻辑时快速验证。但涉及硬件相关的代码,还是得交叉编译。
4.2 构建阶段:并行编译加速
配置好之后就是构建:
./waf copter这里的copter是载具类型,对应多旋翼。其他还有plane(固定翼)、rover(地面车)、sub(水下)、heli(直升机)等。waf 会自动并行编译,默认用满你的 CPU 核心数。如果你的机器内存不大,并行太多可能爆内存,可以用-j参数限制,比如./waf copter -j4。
第一次编译会比较久,视机器性能从十几分钟到半小时不等。这时候ccache就体现出价值了,第二次编译同样的目标可能只要一两分钟。编译过程中如果某个文件报错,waf 会停下来并打印错误信息,你需要根据错误定位到具体源文件。
编译成功后,固件文件在build/<板子名>/bin/目录下,通常是一个.apj文件(用于地面站烧录)和一个.hex或.bin文件(用于底层烧录工具)。.apj是 Ardupilot 自己的格式,里面包含了固件和元数据,用官方地面站刷写最方便。
4.3 增量编译与清理
改完代码后重新编译,waf 会自动检测哪些文件变了,只重编受影响的部分,这就是增量编译。但有时候依赖关系没检测对,或者你切换了板子,就需要清理:
./waf clean这个命令清掉当前板子的构建产物。如果想彻底清掉所有板子的产物,用./waf distclean。我一般切换板子或者升级了工具链之后会 clean 一次,避免缓存导致的诡异问题。
提示:不要手动去删
build/目录,虽然效果类似,但 waf 的缓存状态可能不一致,用官方命令更稳妥。
4.4 编译产物的验证
编完之后别急着刷,先确认产物合理。.apj文件大小通常在 1MB 到 2MB 之间,太小说明没编全,太大可能是调试符号没剥。可以用arm-none-eabi-size看看各个段的大小:
arm-none-eabi-size build/<板子名>/bin/arducopter输出会显示 text、data、bss 段的大小。如果 text 段接近芯片 Flash 容量,说明固件快装不下了,得考虑裁剪功能。这个检查在给资源紧张的板子做定制时特别有用。
5. 常见报错与排查技巧实录
5.1 子模块缺失导致的编译失败
最常见的报错之一,编译到某个文件时提示找不到头文件,比如mavlink.h找不到。九成是子模块没拉全。解决方法是回到仓库根目录执行git submodule update --init --recursive,然后重新配置和编译。如果某个子模块死活拉不下来,检查网络,或者看看.gitmodules里配置的地址是不是需要调整。
5.2 工具链版本不匹配
报错形式多样,可能是编译某个文件时语法错误,也可能是链接阶段找不到某个符号。根源往往是 GCC 版本不对。判断方法:arm-none-eabi-gcc --version看版本,跟官方文档要求的对比。解决就是卸载重装官方脚本提供的版本。我踩过一次坑,系统自带的 GCC 10 编某个内联汇编报错,换成官方推荐的版本立马通过。
5.3 Python 依赖问题
waf 本身是 Python 脚本,依赖缺失会直接导致命令跑不起来,报ModuleNotFoundError。常见的是pymavlink没装或者版本太老。用 pip 装上最新版即可。如果是在 venv 里,确认 venv 已激活。还有一种情况是 pip 装到了用户目录但当前 shell 的PATH没包含,导致找不到,用python3 -m pip install而不是直接pip install能避免这个问题。
5.4 内存溢出与链接错误
编译到链接阶段报region RAM overflowed或者section .text will not fit,说明固件超出了芯片的 Flash 或 RAM 容量。这在给 F405 这类资源紧张的板子加功能时很常见。解决办法:关掉不需要的功能模块,比如用--enable-math-check-indexes之类的选项反而会增加体积,应该去掉;或者在hwdef.dat里精简外设配置。这个属于进阶话题,但知道这个方向能帮你快速定位。
5.5 常见问题速查表
| 报错现象 | 可能原因 | 排查方向 |
|---|---|---|
| 找不到头文件 | 子模块缺失 | 执行 submodule update |
| 语法错误/链接符号缺失 | 工具链版本不对 | 核对 GCC 版本 |
| ModuleNotFoundError | Python 依赖缺失 | pip 安装对应包 |
| region overflowed | 固件超容量 | 裁剪功能模块 |
| 配置阶段找不到编译器 | PATH 未生效 | source 环境变量 |
| 编译中途卡死 | 内存不足 | 降低并行数 -j |
5.6 几个独家避坑心得
第一,养成看日志的习惯。waf 的报错信息其实挺详细,但很多人只看最后一行。往上翻,找到第一个 error,那才是根因,后面的往往是连锁反应。
第二,切换板子前先 clean。我有一次从 F405 切到 H743,没 clean 直接编,结果链接脚本还是旧的,报了一堆莫名其妙的地址错误,clean 之后一切正常。
第三,保留一份能编过的环境快照。用虚拟机或者容器把配好的环境存下来,以后换机器直接恢复,比重新配一遍省事得多。Ardupilot 的环境配置说复杂不算太复杂,但零零碎碎的依赖不少,重配一次少说半小时。
第四,编译时间长的项目,善用ccache和并行。ccache的缓存在~/.ccache,定期看看命中率,如果命中率很低,可能是编译选项里带了时间戳之类的变量,导致缓存失效。
6. 从编译到烧录的衔接
编译只是第一步,产物怎么进飞控也得说清楚。.apj文件用官方地面站(比如 Mission Planner 或 QGroundControl)刷写最方便,连上飞控,选择“自定义固件”,指向你的.apj文件即可。如果飞控变砖了,需要用 DFU 模式配合dfu-util或者 STM32CubeProgrammer 刷.hex或.bin。
这里有个细节:不同板子的烧录方式不一样,有的支持 DFU,有的要用串口 bootloader,有的要用调试器。编译出来的产物格式要跟烧录方式匹配。.apj是给地面站用的,.hex是给 STM32CubeProgrammer 用的,.bin是给某些 bootloader 用的。搞混了会刷不进去。
另外,刷完固件后第一次上电,建议先不接动力电,只接 USB,用地面站看看能不能正常识别、参数能不能读出来。确认没问题再上动力电做传感器校准。这个习惯能避免很多“刷完就炸”的悲剧。
7. 关于构建效率的一些个人体会
编译这件事,配好一次之后其实很少再折腾,但前期投入的时间绝对值得。我见过太多人卡在环境上,最后放弃二次开发,挺可惜的。其实核心就三件事:工具链版本对、子模块拉全、Python 依赖齐。这三样搞定,剩下的就是敲命令等结果。
如果你打算长期做 Ardupilot 开发,建议把整个构建流程脚本化,写个 shell 脚本一键完成拉代码、配环境、编译。这样换机器或者重装系统的时候,几分钟就能恢复工作环境。我自己维护了一个这样的脚本,里面还加了错误检查和日志记录,出问题直接看日志,比在终端里翻历史命令高效得多。
最后说个容易被忽略的点:编译产物的版本管理。每次编译出来的固件最好带上 git commit hash 和编译时间,方便回溯。Ardupilot 的构建系统其实会在固件里嵌入版本信息,地面站能读出来,但你自己在文件命名上也可以加上标识,比如arducopter-v4.5.0-abc1234.apj,这样一堆固件放一起也不会搞混。