1. 先明确这套组合的定位:Ubuntu 20.04 + FastPlanner 到底是什么
如果你在做四旋翼、小型无人机的自主飞行,或者正在读研做运动规划方向的项目,大概率会在某个时间点撞上 FastPlanner 这个名字。它是一套面向四旋翼平台的开源轨迹规划框架,核心由几层组成:基于 ESDF(欧氏符号距离场)的局部地图、动力学约束下的前端路径搜索、B 样条后端轨迹优化,外加一层偏航角规划。简单说,它解决的是“给定当前位姿和一个目标点,在一堆障碍物中间生成一条既避障、又不超出无人机加速度和速度极限的光滑轨迹”这件事。我把这套东西在 Ubuntu 20.04 上从零到跑通、再到接实机,前后折腾了不短时间,中间踩的坑足够写一篇完整的记录。
这篇内容面向三类人:刚接触运动规划、想把 FastPlanner 跑起来看看效果的初学者;已经在仿真里跑通、但卡在参数调不出来或者准备上实机的中间层;以及需要在 Ubuntu 20.04 这个特定系统版本上复现环境的人。我会把选型逻辑、环境搭建、编译报错、仿真跑通、参数调优、故障排查、实机迁移这几个环节完整拆开讲。不堆概念,重点是“为什么这么做”和“这一步实际会出什么岔子”。
先给一个整体判断:Ubuntu 20.04 配 FastPlanner 是当前比较省心的一套组合,但这个“省心”是相对的。它的省心来自 ROS Noetic 和 Ubuntu 20.04 的强绑定,避免了跨版本凑依赖的痛苦;它的不省心来自 FastPlanner 本身对 PCL、Eigen、Ceres 这类数学和点云库的版本敏感,装错一个小版本就可能编译报出一堆看起来完全不相关的错误。所以整篇的叙述顺序是:先讲清楚为什么这么选,再讲环境怎么搭,然后讲编译和运行,最后讲调参和排查。你可以顺着读,也可以直接跳到卡住的那一节。
2. 为什么是这套组合:版本绑定关系与依赖链拆解
2.1 ROS 版本和系统版本之间的强绑定关系
这一条是所有踩坑的源头,必须先讲清楚。ROS 的发行版和 Ubuntu 版本是绑定的,Noetic 只跑在 20.04 上,Melodic 只跑在 18.04 上,再往前 Kinetic 是 16.04。FastPlanner 这类项目大量使用了roscpp、tf、pcl_ros、rviz、visualization_msgs这些包,pcl_ros又和系统里的 PCL 版本绑定,PCL 版本又跟 Ubuntu 发行版绑定。所以“用 Ubuntu 20.04”这个决定的真实含义是:我把 PCL 锁在 1.10 这个系列,把 Eigen 锁在 3.3.7,把 Python 锁在 3.8,然后把整套依赖链一次性定死。
如果你图省事在 22.04 上装,会立刻遇到第一个硬墙:Noetic 没有 22.04 的官方二进制包。有人会想着自己源码编译 ROS,我不推荐这条路,因为 FastPlanner 本身不是“编译过就完事”的项目,后面还要反复装包、跑rosdep、拉别人的包做对比,一个非标准的 ROS 环境会让你在后期的每一次求助里都变成孤岛。也有人退回到 18.04 + Melodic,理论上可行,但 Python 2 和 Python 3 混用的问题会持续折磨你,尤其是你要写脚本做数据处理的时候。
我这里还遇到过一类更隐蔽的坑:机器上先装了 Anaconda,conda的 Python 排在PATH前面,结果catkin_make的时候 CMake 找到的是 conda 里的 Python 3.9 和一堆 conda 自带的库,编译期直接报找不到PythonLibs,或者更诡异地在链接阶段报undefined reference。这个问题我在两台机器上重复遇到,处理方式在后文的排查表里会写。
2.2 FastPlanner 的依赖链条:哪些是显性的,哪些是隐性的
FastPlanner 的 README 上通常会列三样:Eigen、PCL、ROS。看起来很少,实际上这只是“显性依赖”。真正编译的时候,还会牵扯到cmake_modules(用来找 Eigen 和 PCL 的 CMake 脚本)、uav_utils(工具函数)、armadillo(部分分支用来做矩阵运算)、NLopt或OSQP(不同分支的优化求解器选型不一样)。不同人 fork 的版本依赖会有差异,所以最靠谱的做法不是背依赖清单,而是先看仓库的CMakeLists.txt和package.xml,把find_package和<depend>扫一遍,再决定装什么。
我个人的经验是:先把 ROS 装好,再按package.xml里列的顺序装系统依赖,然后用rosdep install做一次“补漏”,最后再编译。这个顺序的理由是,rosdep会读package.xml里的依赖名去查对应的 apt 包,如果系统里 ROS 环境没配好,rosdep的 key 就解析不出来,等于白跑。
| 依赖 | 20.04 上的典型版本 | 作用 | 装错的典型症状 |
|---|---|---|---|
| ROS Noetic | 1.15.x | 通信、可视化、构建系统 | catkin_make找不到catkin |
| Eigen3 | 3.3.7 | 矩阵、四元数、几何运算 | 模板报错刷屏,指向Eigen/src/Core |
| PCL | 1.10.x | 点云、ESDF 相关数据结构 | pcl_ros链接失败或段错误 |
| Ceres / NLopt / OSQP | 视分支而定 | 后端优化求解 | 链接期undefined reference |
| Armadillo | 9.x/10.x | 部分矩阵运算 | 头文件缺失或 ABI 不匹配 |
2.3 硬件层面的现实考量:别只看能不能编译
FastPlanner 的前端搜索和 ESDF 更新是计算密集型的,尤其是地图分辨率调细之后,单帧建图和搜索的耗时会上得很明显。我在一台老笔记本上试过,i5 双核、8G 内存,跑小尺寸地图勉强能动,地图一放大就开始出现规划超时,RViz 里轨迹断断续续。换到 6 核以上、16G 内存的机器上,同样的参数就顺畅很多。
这里有两个容易忽略的点。第一是显卡,RViz 依赖 OpenGL,虚拟机里没有 3D 加速的话,光是点云显示就能把 CPU 吃满,看起来像是“规划很慢”,实际上是渲染拖累。第二是 CPU 的主频比核心数更影响单次规划的延迟,因为搜索和优化里大量是串行逻辑,核心再多也并行不上去。所以如果你打算在机载计算机上跑,选型时优先看单核性能和散热,不要只盯着核心数量。
注意:整篇文章里的参数值是“典型量级”,具体数值请以你手上那份仓库的 launch 和 yaml 文件为准。不同 fork 的参数名和默认值差异不小,照抄别人的数值是最常见的错误来源之一。
3. 从裸系统到最小可用环境:环境搭建的实操细节
3.1 系统安装阶段的几处关键设置
装系统这一步看着没技术含量,但有两处会直接影响后续。第一是分区,/给足,至少 60G。ROS 加上各种依赖、编译中间产物、录的 bag 包,很快就吃掉几十个 G。我见过有人给/只分 30G,编译到一半磁盘满,catkin_make报的错和磁盘一点关系都没有,查了半天。第二是 swap,内存 16G 以下建议留 8G 以上的 swap,因为catkin_make -j并行编译时峰值内存会冲得很高,尤其是编译带模板的优化库,OOM killer 会在没有任何提示的情况下直接杀掉编译进程,你只会看到一个莫名其妙的c++: fatal error: Killed signal terminated program cc1plus。
安装完成后第一件事是换软件源。默认源在国内网络下经常超时,apt update卡住会让后面每一步都很难受。换源之后跑一次完整升级,然后装基础工具:
sudo apt update && sudo apt upgrade -y sudo apt install -y build-essential cmake git wget curl \ libeigen3-dev libpcl-dev libarmadillo-dev \ libgoogle-glog-dev libgflags-dev libatlas-base-dev \ libsuitesparse-dev这几个包里,libpcl-dev会把 PCL 的开发和运行时一起带上,libeigen3-dev装完是 3.3.7,正好是 20.04 官方源里的版本。libsuitesparse-dev和libatlas-base-dev是为 Ceres 准备的,如果你的分支不用 Ceres 可以先跳过,但装上没坏处。
3.2 ROS Noetic 的安装与 rosdep 初始化
ROS 安装网上教程很多,我只强调几个容易漏的点。第一,sources.list里要加上universe仓库,rosdep依赖它来解析很多包。第二,装完要做rosdep init和rosdep update,update这一步在国内网络下失败率不低,失败之后rosdep install会静默地什么都装不上,你以为装好了,编译的时候才发现缺一堆东西。第三,source /opt/ros/noetic/setup.bash这行要写进~/.bashrc,否则每开一个新终端都得手动 source 一次。
rosdep update如果反复超时,可以直接跳过它,改成手动装依赖——因为 FastPlanner 的依赖数量有限,手动装反而更可控。我现在的习惯就是:先看package.xml,把缺的包用 apt 一个个装上,比跟rosdep的索引较劲快得多。
3.3 创建工作空间与源码落地
工作空间我建议单独建,不要和别的项目混在一起,因为 FastPlanner 会往环境里塞不少自定义消息,混在一起很容易出现消息类型冲突:
mkdir -p ~/fast_ws/src cd ~/fast_ws/src git clone <你的仓库地址> fast_planner cd .. catkin_make -j4-j4这个参数值得说一下。默认的catkin_make会用满所有核心,看起来最快,实际上在小内存机器上极容易 OOM。我的经验公式是:内存 GB 数除以 2,再和核心数取小值。16G 内存 8 核,用-j4到-j6之间比较稳。如果你的机器编译到最后一个包才挂,八成就是这个原因。
编译前还要确认一件事:catkin_make用的是哪套 Python 和哪套 CMake。跑一句which python3和cmake --version,确认没有指向 conda 或自建的路径。这一步花十秒钟,能省掉后面一小时的排查。
4. 编译 FastPlanner:报错结构分析与逐个解决
4.1 源码结构与构建顺序理解
FastPlanner 的包结构一般分成几块:地图与 ESDF 管理、路径搜索、B 样条优化、轨迹管理、以及若干工具包。catkin_make会按依赖关系自动排序,但如果某个包声明的依赖不全,就会出现“先编译的包找不到后编译的包的头文件”这种顺序问题。遇到这类报错,别急着改代码,先去看CMakeLists.txt里的find_package(catkin REQUIRED COMPONENTS ...)和package.xml里的<depend>是否一致。这两处不一致是 catkin 世界最经典的坑。
我遇到过一种情况:package.xml里写了某个依赖,但CMakeLists.txt里的find_package没列,结果编译单个包没问题,整体工作空间编译的时候头文件路径没被加进去,报fatal error: xxx.h: No such file or directory。解决方式就是在find_package的COMPONENTS里补上,然后catkin_make前先rm -rf build devel清一遍,因为 catkin 的缓存有时候不会自己刷新。
4.2 高频编译错误对照表
| 报错关键字 | 大概率原因 | 处理方式 |
|---|---|---|
Could NOT find Eigen3 | cmake_modules没被 source 或版本不匹配 | 确认find_package(Eigen3 REQUIRED),检查Eigen3_DIR |
undefined reference to pcl::... | PCL 版本与pcl_ros不一致 | 统一用系统源里的 PCL,别自己编一份 |
fatal error: Eigen/Core: No such file | 头文件路径没带/eigen3 | 用find_package(Eigen3)提供的变量,别硬写路径 |
Killed signal terminated program cc1plus | 内存不足被 OOM | 降-j,加 swap |
undefined reference to ceres::... | 优化库没链接或版本不兼容 | 检查target_link_libraries,确认库路径 |
ImportError: No module named ... | Python 环境串了 | which python3,清理PYTHONPATH |
这张表是我自己踩出来的,前四条出现频率最高。特别是第二条,很多人图新鲜自己编一份 PCL 装到/usr/local,结果系统里同时存在两份 PCL,pcl_ros用的是/usr那份,自己代码用的是/usr/local那份,编译能过,运行时段错误,而且报错位置完全不相关。这条我强烈建议:20.04 上就用apt装的 PCL,别自己编。
4.3 编译参数与产物确认
编译成功后,devel/lib下应该能看到对应的可执行文件,devel/include下能看到生成的消息头文件。如果devel/lib里是空的,说明某个包虽然“编译通过”但没产出可执行目标,常见原因是CMakeLists.txt里的add_executable被注释掉了,或者catkin_package声明有问题。这种情况别急着怀疑环境,先回去读 CMake 文件。
实操心得:每次大改环境之前,先
cp -r ~/fast_ws ~/fast_ws_bak。这套依赖链调起来很容易把原来能跑的环境改坏,有个备份能救你一次。
5. 跑通仿真:从 launch 文件到 RViz 可视化
5.1 仿真 launch 的组成与参数含义
FastPlanner 的仿真一般通过roslaunch拉起,典型的 launch 会做几件事:加载参数文件、启动地图与规划节点、启动一个模拟的位姿发布源(有的是简单的位姿模拟器,有的接的是更完整的仿真环境)、启动 RViz 并加载预设的配置文件。参数文件里通常包含这几组:
- 地图组:
map_size_x/y/z、resolution、obstacle_inflation - 前端组:
max_vel、max_acc、search_time、init_max_tau - 后端组:
dist0、time_weight、control_points_distance - 可视化组:发布频率、轨迹采样密度
resolution是影响最大的一个。0.1 和 0.2 看着只差一倍,实际内存占用差四倍(三维是立方关系),ESDF 更新耗时也差四倍左右。我一开始图精度设成 0.05,结果地图稍微大一点就直接把内存吃满。后来改成 0.15 起步,跑顺了再往下调,这个顺序比一上来就追求极致精度要合理得多。
5.2 启动流程与话题确认
启动之后不要立刻盯着 RViz 看,先开一个终端跑rostopic list,确认几个关键话题都在:地图或 ESDF 相关的话题、规划器状态话题、轨迹话题、位姿话题。话题缺了,RViz 里当然什么都没有。然后用rostopic hz看位姿话题的频率,正常情况下应该在几十赫兹,如果只有几赫兹或者干脆不动,说明位姿源没起来,规划器收不到位姿就不会触发规划。
RViz 里需要确认的显示项通常包括:地图点云或占据栅格、ESDF 的可视化、当前轨迹、目标点、无人机模型。如果 RViz 一片黑,先检查Fixed Frame设的是不是和代码里发布坐标系一致,world和map混用是最常见的第一个坑。
5.3 触发一次规划并观察结果
大部分实现里,规划是通过发布一个目标点触发的,有的还要求先给一个初始位姿。触发后观察三件事:轨迹有没有从当前位置连到目标点、轨迹是否绕开了障碍、轨迹曲率是否平滑。如果轨迹直接穿过障碍,问题一般出在 ESDF 没建起来或者膨胀半径太小;如果轨迹剧烈抖动、来回折,问题一般出在后端权重或者控制点密度。
这里分享一个观察技巧:把轨迹的采样密度参数调高,让 RViz 里显示出一条密集的点线,然后放大看它在障碍物附近的走向。稀疏采样会把很多细节吃掉,看起来“很平滑”,实际执行的时候会撞。
6. 参数调优:把“能跑”变成“好用”
6.1 前端搜索参数:决定“找不找得到路”
前端的核心是在动力学约束下做启发式搜索,几个参数的作用很直接。max_vel和max_acc决定了搜索树扩展时的状态边界,设得太大,搜索空间爆炸、耗时上升,设得太小,稍微远一点的目标就搜不到解。我的做法是先把这两个值设成实机能力的 1.2 倍左右,跑通之后再往下收。
搜索的“贪心程度”也很关键。偏向目标点的权重高,搜索快但容易陷在局部;权重低,能找到更绕但更可行的路,代价是时间长。地图复杂的时候我倾向于稍微降低贪心程度,因为规划失败比规划慢更让人难受。
6.2 后端优化权重:决定“好不好看、执不执行得了”
后端做的是把前端那条粗糙的路径拟合成 B 样条并做优化,几个权重项的直觉是:
| 参数类型 | 调大的效果 | 调大的代价 |
|---|---|---|
| 平滑项权重 | 轨迹更顺,加速度连续 | 贴障碍更紧,安全裕度下降 |
| 碰撞项权重 | 更远离障碍 | 轨迹变形,可能绕远甚至无解 |
| 时间项权重 | 整体飞行时间缩短 | 速度冲击大,实机跟随吃力 |
| 控制点密度 | 轨迹表达能力更强 | 优化变量多,单次求解变慢 |
我的调参顺序固定是:先把碰撞项权重拉高,保证绝对不撞;再调平滑项,看轨迹是否可执行;最后才动时间项,追求速度。反过来调的人很多,结果就是轨迹好看但撞,或者速度上去了但实机跟不住。
6.3 安全距离与膨胀策略
obstacle_inflation这个参数容易被当成一个“随便填个 0.2”的东西,实际上它跟地图分辨率是耦合的。分辨率 0.15 的地图,膨胀半径设 0.2,只相当于一个多格,实际效果比想象中薄。我的经验是膨胀半径至少要有 2 到 3 个格子,再叠加实机机身半径。另外要注意,膨胀是“把障碍变大”,不是“把安全距离算进轨迹约束”,两者结合才是完整的安全裕度。
7. 常见问题与排查实录
7.1 编译与启动阶段的问题
一类高频问题是“昨天能编译,今天不能了”。这种情况九成是环境变量的问题,比如新开的终端没有 source,或者装了别的东西改了CMAKE_PREFIX_PATH。排查方式很简单:env | grep -i ros和env | grep -i cmake,对比能编译时的状态。我现在的做法是在工作空间里放一个setup.sh,里面固定写死 source 路径,每次开终端先跑它,把环境不确定性消掉。
第二类是“启动就退”。节点启动后立刻退出,日志里通常能看到参数读取失败的痕迹。FastPlanner 的参数大量通过nh.param读取,某个参数名写错、类型不匹配,读不到默认值,节点就会在初始化阶段崩掉。这种情况直接把日志里的第一行报错往前翻,通常能看到缺了哪个参数。
7.2 运行阶段的问题与速查
| 现象 | 排查方向 | 处理建议 |
|---|---|---|
| RViz 里没有轨迹 | 话题名不匹配、坐标系不一致 | rostopic list对照,检查Fixed Frame |
| 规划一触发就失败 | 起点或终点在障碍内 | 查 ESDF 值,必要时临时关掉碰撞检查定位 |
| 轨迹穿过障碍 | 膨胀太小、地图未更新 | 提高膨胀半径,确认地图输入话题有数据 |
| 规划延迟大、卡顿 | 分辨率过细、地图过大 | 先降分辨率到 0.2 验证瓶颈 |
| 程序段错误 | 库版本冲突、指针越界 | 检查是否装了多份 PCL,用ldd看链接 |
| 轨迹抖动明显 | 后端权重失衡 | 提高平滑项,降低时间项 |
这张表里我最想强调的是“段错误”那一行。段错误在 C++ 项目里几乎等于“没有信息”,但在这类项目里有很明确的常见原因:多份库共存导致 ABI 不匹配。判断方法是ldd看一下可执行文件到底链接了哪个路径的libpcl,如果出现/usr/local/lib而系统里本来有/usr/lib的版本,那就是它。处理方式是卸掉自己编的那份,或者用LD_LIBRARY_PATH强制指到系统路径。
7.3 性能层面的问题
性能问题最容易被误判成“算法慢”。我遇到过一次,规划单次耗时忽高忽低,从 20ms 跳到 300ms。查了半天算法,最后发现是 RViz 在同时渲染一大片点云,把 CPU 的一个核吃满了。把 RViz 的点云降采样打开,抖动立刻消失。所以排查性能问题的第一步应该是:关掉所有可视化,再测一次。这个顺序能帮你排除掉一大半假象。
第二个常见原因是日志。有些分支默认打开了很详细的调试输出,每帧刷屏,IO 开销不小。把日志等级调到 info 或 warn,实测能省下可观的耗时。
8. 从仿真到实机:迁移时必须提前想清楚的事
8.1 坐标系、位姿与里程计的一致性
仿真里位姿是“送”给你的,实机上要自己估。这里最常见的问题是坐标系层级没理清:world、map、odom、base_link之间的关系如果和代码里假设的不一致,规划出来的轨迹在 RViz 里看着对,飞机飞起来是斜的。我的做法是先在一个静态场景里,把飞机放在原点,检查各个坐标系的方向和单位,确认没有旋转或者缩放被悄悄引入。
另一个是时间同步。实机的位姿、点云、IMU 来自不同传感器,时间戳偏差大了,建图和规划都会出问题。仿真是理想时间,这个问题根本不会暴露,所以迁移前一定要单独测一次时间戳对齐情况。
8.2 计算平台与实时性
仿真里慢一点你能等,实机上慢一点飞机就飘出去了。迁移到机载计算机时,先测三个数:ESDF 单次更新耗时、前端搜索耗时、后端优化耗时。三个加起来要留出足够的余量给控制和通信。如果超了,优先降地图分辨率,其次缩小地图范围,最后才考虑换硬件。
还有一个隐蔽的坑:机载计算机的 CPU 会因为温度升高而降频,实验室里跑得好好的参数,飞了几分钟之后规划开始变慢。这个只能靠实测,跑长航时看日志里的耗时曲线。
8.3 安全边界与降级策略
实机上必须有降级方案。规划失败怎么办、规划超时怎么办、位姿突然跳变怎么办,这些都要在代码里写死,而不是指望“应该不会发生”。我的做法是设一个超时阈值,超过就悬停并上报状态,而不是继续用旧轨迹飞。另外,第一次实飞一定要在有空旷区域的场地,把速度限制调到很低,确认轨迹跟踪正常之后再逐步放开。
我个人在实际操作中的体会是,从仿真到实机这一段,真正花时间的不是算法本身,而是各种“仿真里不存在”的细节:通信延迟、传感器噪声、坐标系约定、电源管理。这些没有捷径,只能一项项验证。我现在的习惯是列一张迁移清单,每验证一项打一个勾,全部打完再飞,比凭感觉“差不多了”要靠谱得多。
最后分享一个我觉得很有用的小习惯:每次调参之前,先把当前能跑通的参数文件完整复制一份,命名带上日期和一句说明。FastPlanner 这类参数多的系统,改到后面很容易忘记哪个组合是最优的,而那一份“上周三那个能跑的配置”往往就是救命的东西。