引言
MoveIt 是机械臂运动规划的事实标准,它把逆解、碰撞检测、采样规划、时间参数化、轨迹执行串成一条可配置的管线。很多团队用 MoveGroupInterface 几行代码就跑通了 demo,却在上真机后发现「规划失败率高、规划耗时抖动大、偶发撞到物体」。这些问题的根因几乎都不在规划算法本身,而在规划场景的维护与碰撞检测的配置。
工程上的第一个难点是「规划场景与现实的一致性」。规划场景是 MoveIt 内部对世界的认知,它由机器人模型、静态环境、动态障碍、附着物体四部分构成。如果场景里没有及时加入当前抓着的物体,规划器就会认为机械臂是空的,从而规划出一条撞到物体的轨迹。场景维护是应用层的责任,不是 MoveIt 的。
第二个难点是碰撞检测的成本。规划器 90% 的时间花在碰撞检测上,而碰撞检测的配置(ACM、安全裕度、FCL 参数)直接决定规划速度与成功率。默认配置在很多场景下既慢又容易误判「自碰撞」。
第三个难点是任务级编排。抓取一件物体不是一次规划,而是「接近 → 抓 → 抬起 → 搬运 → 放置」多个阶段的状态机,涉及物体附着/脱离、多规划组协同、失败回退。MoveIt Task Constructor(MTC)正是为此设计。本文聚焦规划侧,机械臂的控制与执行管线(ros2_control、轨迹跟踪、装配力控)见机械臂控制与抓取规划 ,通用规划算法见运动规划与轨迹优化 。
目录
- MoveIt 规划体系总览
- 规划组、规划场景与监控场景
- 碰撞世界:添加、附着与更新
- ACM 与自碰撞检测配置
- FCL 与碰撞检测加速
- OMPL 规划器配置与规划时间
- 规划请求适配器与响应适配器
- 笛卡尔路径规划与步长
- MoveIt Task Constructor 抓取流水线
- 与真实控制器的轨迹交接
- 可视化、调试与性能剖析
- 仿真验证与回归测试
1. MoveIt 规划体系总览
MoveIt 的核心进程是 move_group,它把多个能力节点聚合在一个入口后面,外部只需与 move_group 交互。
move_group 聚合的能力:
规划管线 :规划请求适配器 → 规划器插件 → 响应适配器
逆解 :kinematics 插件(KDL / TRAC-IK / IKFast)
碰撞检测 :CollisionEnv(FCL/Bullet)+ ACM
规划场景 :PlanningSceneMonitor 维护世界状态
执行 :把轨迹交给 controller(通常经 ros2_control)
服务/动作 :/plan、/execute、/compute_ik、/apply_planning_scene ...
三类外部接口:
C++ MoveGroupInterface :生产代码首选,功能最全
Python moveit_py :脚本与实验
RViz MotionPlanning 插件 :可视化调试
理解 MoveIt 的关键是分清「配置」与「运行时」:SRDF、kinematics.yaml、ompl_planning.yaml 是静态配置,决定能力边界;规划场景与规划请求是运行时的输入,决定每次规划的具体问题。本文的重点是这两者如何配合。
2. 规划组、规划场景与监控场景
规划组(planning group)是 MoveIt 的基本操作单元,规划场景是它对世界的完整认知。
PlanningScene 的四个组成部分:
1. 机器人状态(RobotState):当前关节角、末端位姿
2. 机器人模型(RobotModel):URDF + SRDF 描述的结构
3. 世界几何(World Geometry):静态环境 + 动态障碍
4. 附着物体(Attached Bodies):机器人抓着的物体
PlanningSceneMonitor 的三种同步方式:
1. 订阅 /monitored_planning_scene 话题(MonitoringScene)
—— 来自外部(如感知节点)的场景更新
2. 订阅 /planning_scene 话题(完整场景替换)
3. 订阅 /collision_object 与 /attached_collision_object
—— 增量的碰撞体添加/移除,最常用
// 用 PlanningSceneInterface 增删碰撞体
#include <moveit/planning_scene_interface/planning_scene_interface.h>
moveit::planning_interface::PlanningSceneInterface psi;
moveit_msgs::msg::CollisionObject table;
table.header.frame_id = "world";
table.id = "table";
shape_msgs::msg::SolidPrimitive box;
box.type = shape_msgs::msg::SolidPrimitive::BOX;
box.dimensions = {1.2, 0.8, 0.05};
table.primitives.push_back(box);
geometry_msgs::msg::Pose pose;
pose.position.z = 0.4;
table.primitive_poses.push_back(pose);
table.operation = table.ADD;
psi.applyCollisionObjects({table}); // 加入世界
// psi.removeCollisionObjects({"table"}); // 移除
一个高频错误:规划场景的坐标系与世界坐标系不一致。碰撞体的 header.frame_id 必须是 TF 树里存在的坐标系,通常是 world 或 base_link。若写成 odom 而该坐标系不在 TF 树里,MoveIt 会静默丢弃该物体,规划器仍认为世界是空的。
3. 碰撞世界:添加、附着与更新
「附着物体」是抓取场景的关键:抓起来之后,物体必须从世界几何移到机器人上,跟随机械臂运动。
// 抓取成功后把物体附着到末端连杆
moveit_msgs::msg::AttachedCollisionObject aco;
aco.link_name = "panda_hand"; // 附着到的连杆
aco.object = box_object; // 被附着的物体
aco.touch_links = {"panda_hand", "panda_leftfinger",
"panda_rightfinger"}; // 允许接触的连杆
aco.object.operation = aco.object.ADD;
psi.applyAttachedCollisionObject(aco);
附着/脱离的状态机(必须成对且顺序正确):
抓取成功 → applyAttachedCollisionObject(ADD)
放置成功 → applyAttachedCollisionObject(REMOVE) 然后 applyCollisionObject(ADD)
(物体回到世界上,位置更新为放置点)
错误做法:
抓取后忘了附着 → 规划器认为臂是空的 → 抬起时撞到刚抓的物体
放置后忘了从世界移除再重新加 → 物体停留在被抓走时的位置
touch_links 是最容易被忽略的参数。附着的物体与夹爪之间必然接触,若不把夹爪连杆列入 touch_links,MoveIt 会认为「机器人碰到物体」而永远无法规划。同理,touch_links 不宜设得过宽(比如把整条臂都列进去),否则会漏检真实的碰撞。
4. ACM 与自碰撞检测配置
Allowed Collision Matrix(ACM)是碰撞检测的加速核心:它记录「哪些连杆对不需要检查碰撞」。
<!-- SRDF 中的 disable_collisions:ACM 的来源 -->
<disable_collisions link1="panda_link0" link2="panda_link1" reason="Adjacent"/>
<disable_collisions link1="panda_link0" link2="panda_link2" reason="Never"/>
<disable_collisions link1="panda_link1" link2="panda_link3" reason="Never"/>
ACM 的三类来源:
1. SRDF 的 disable_collisions(静态,来自 URDF 结构)
2. 运行时通过 PlanningScene 的 allowed_collision_matrix 动态设置
3. 附着物体的 touch_links(临时的允许接触对)
收益:
七轴臂若不配置,需检查 C(7,2)=21 对自碰撞
配置后通常只剩 3~5 对需要检查,规划提速 2~4 倍
漏配的代价:规划极慢(每步都做全量自碰撞检查)
// 运行时动态放宽/收紧 ACM(例如允许某两个连杆接触)
collision_detection::AllowedCollisionMatrix acm =
planning_scene->getAllowedCollisionMatrix();
acm.setEntry("panda_link7", "panda_hand", true); // true = 允许碰撞
planning_scene->setAllowedCollisionMatrix(acm);
reason="Adjacent" 与 reason="Never" 的区别很重要。Adjacent 表示两连杆通过关节直接相连,永远不该检查碰撞;Never 表示几何上永远不可能相交。MoveIt Setup Assistant 能自动生成,但生成后必须人工核对 Never 类条目——一旦错标,真实的碰撞会被漏检,导致撞机。ACM 的正确性是安全底线,不是性能优化。
5. FCL 与碰撞检测加速
碰撞检测的底层库与参数决定单次查询的成本。
碰撞检测的三层加速:
层次 1:包围体层次(BVH)
FCL 默认用 OBB,对凸多面体效果好
典型:无 BVH 时 100 µs/次 → 有 BVH 时 2 µs/次
层次 2:连续碰撞检测(CCD)
离散采样(快,可能漏检)vs 扫掠体(准,慢)
工程折中:采样间隔 < 最薄障碍厚度的一半
层次 3:碰撞缓存
缓存「已知无碰撞的构型」,重复查询直接命中
MoveIt 的 CollisionEnv 支持缓存,固定工位命中率高
# move_group 的碰撞检测参数
move_group:
ros__parameters:
# 连续碰撞检测的最大步长(米),越小越安全越慢
# 机械臂建议 0.01~0.05
# 由 planning_pipeline 的 collision_checking 配置
default_planning_pipeline: ompl
planning_pipelines:
pipeline_names: ["ompl", "pilz_industrial_motion_planner"]
ompl:
ros__parameters:
planning_plugin: ompl_interface/OMPLPlanner
request_adapters: >-
default_planner_request_adapters/AddTimeOptimalParameterization
default_planner_request_adapters/FixWorkspaceBounds
default_planner_request_adapters/FixStartStateBounds
default_planner_request_adapters/FixStartStateCollision
安全裕度(padding) 是另一个关键旋钮。给机器人或障碍加几毫米的 padding 能让轨迹与障碍保持安全距离,但过大(如 5 cm)会让狭窄空间无解。做法是按场景给不同物体设不同 padding:环境障碍设 1~2 cm,需要贴近操作的物体设 0。
6. OMPL 规划器配置与规划时间
OMPL 是 MoveIt 默认的采样规划器集合,配置的核心是「选哪个规划器 + 给多少时间」。
ompl:
ros__parameters:
planner_configs:
RRTConnect:
type: geometric::RRTConnect
range: 0.0 # 0 表示用默认步长
RRTstar:
type: geometric::RRTstar
range: 0.15
goal_bias: 0.05
BITstar:
type: geometric::BITstar
PRM:
type: geometric::PRM
panda_arm:
default_planner_config: RRTConnect
planner_configs: ["RRTConnect", "RRTstar", "BITstar", "PRM"]
projection_evaluator: joints(panda_joint1,panda_joint2)
longest_valid_segment_fraction: 0.005 # 碰撞检测的离散化步长
规划器选型与规划时间:
RRTConnect :双向扩展,首次解最快,工业首选(默认)
RRTstar :渐近最优,路径更短但要更长时间
BITstar :综合最优性与速度,近年推荐
PRM :固定环境多次查询,需预构建路标图
EST/KPIECE :特定场景,一般不用
规划时间的设置(关键工程经验):
规划时间不是「越长越好」,而是「覆盖 p99 即可」
机械臂典型:0.5~2.0 s,配合 5 次重试
p50 通常 20~100 ms,p99 可能 1~2 s,设置要看 p99
盲目设 10 s 只会让失败场景等更久,不提高成功率
longest_valid_segment_fraction 是常被忽视但影响巨大的参数:它决定路径上每隔多远做一次碰撞检测。值太小(0.001)会让碰撞检测次数暴增、规划变慢;值太大(0.05)可能漏检细障碍。机械臂关节空间的推荐值是 0.005 左右,即「单个关节最多变化 0.5% 的行程就检查一次」。
7. 规划请求适配器与响应适配器
适配器(adapter)是插在规划前后的可插拔处理步骤,是 MoveIt 灵活性的体现。
请求适配器(规划前,修正问题):
FixStartStateBounds :把起始状态夹到关节限位内
FixStartStateCollision :起始状态在碰撞中时,微调到最近的自由构型
FixStartStatePathConstraints:修正起始状态违反的路径约束
FixWorkspaceBounds :给未定义的工作空间边界补默认值
ValidateWorkspaceBounds :校验工作空间边界合理性
CheckStartStateBounds :只检查不修改(会报错而非静默修正)
响应适配器(规划后,优化轨迹):
AddTimeParameterization :给几何路径加时间(旧)
AddTimeOptimalParameterization:TOTG,推荐
ResolveConstraintFrames :解析约束中的坐标系
适配器顺序很重要(默认顺序即推荐顺序):
1. 先 Fix(修正)再 Validate(校验)
2. 时间参数化必须放在最后(前面都是几何层面)
3. 关闭 FixStartStateCollision 会让「起始状态碰撞」直接失败,
保留它能让系统更鲁棒,但会掩盖真实的场景错误
FixStartStateCollision 是双刃剑。它让系统在起始状态轻微碰撞时仍能规划,提高了鲁棒性;但如果它频繁生效,说明规划场景与真实状态已经偏离(比如附着物体没更新),是隐藏 bug 的信号。建议在生产环境中监控这个适配器的触发次数,把它当作场景一致性的健康指标。
8. 笛卡尔路径规划与步长
笛卡尔路径要求末端走直线或指定形状,用于接近、插拔、涂胶等场景。
// 笛卡尔直线规划:逐点逆解,fraction 是关键指标
std::vector<geometry_msgs::msg::Pose> waypoints = {start, mid, end};
moveit_msgs::msg::RobotTrajectory trajectory;
const double eef_step = 0.01; // 末端步长 1 cm
const double jump_threshold = 1.5; // 关节跳变阈值(0 表示禁用,不推荐)
double fraction = arm.computeCartesianPath(
waypoints, eef_step, jump_threshold, trajectory);
if (fraction < 0.99) {
RCLCPP_WARN(node->get_logger(),
"cartesian path only %.1f%% complete", fraction * 100);
return false; // 必须检查,不能直接执行
}
eef_step 的取舍:
0.005 m :最平滑,逆解次数最多,慢
0.01 m :推荐起点,平衡
0.05 m :快,但中间可能跳过奇异位形导致跟踪偏差
jump_threshold 的作用:
检测相邻路径点之间关节角是否发生「非物理的跳变」
逆解在多解间跳变时,关节角会突然变化,执行时是灾难
设为 0 会禁用检测,奇异附近可能产生瞬间大幅跳变
笛卡尔路径失败的常见原因是「经过奇异位形」。当末端接近工作空间边界或腕部对齐时,逆解退化,fraction 骤降。对策是「把长直线拆成多段,绕开奇异区」,或在接近奇异时切换到关节空间运动。
9. MoveIt Task Constructor 抓取流水线
MTC 把抓取任务表达成一棵阶段(Stage)树,是任务级编排的标准工具。
MTC 的阶段类型:
CurrentState :捕获当前状态作为起点
GenerateGrasps :生成候选抓取位姿
ComputeIK :对每个候选求逆解
Connect :连接两个阶段(可带碰撞检测)
MoveTo :移动到命名目标
ModifyPlanningScene :临时增删场景物体(如打开容器盖)
AllowCollision :临时允许某些碰撞(如插入时贴边)
流水线骨架:
CurrentState → MoveTo(ready) → Connect(→ 预抓取) → GenerateGrasps
→ ComputeIK → AllowCollision(接近时) → MoveTo(抓取)
→ ModifyPlanningScene(附着物体) → MoveTo(抬起)
→ Connect(→ 放置上方) → MoveTo(放置) → ModifyPlanningScene(脱离)
// 用 MTC 构建一个最小抓取任务
auto node = std::make_shared<rclcpp::Node>("mtc");
auto task = std::make_unique<moveit::task_constructor::Task>();
task->stages()->setName("pick");
task->loadRobotModel(node);
task->setProperty("group", "panda_arm");
auto current = std::make_unique<stages::CurrentState>("current");
task->add(std::move(current));
auto move_to_pick = std::make_unique<stages::MoveTo>("approach", solver);
move_to_pick->setGoal("ready");
task->add(std::move(move_to_pick));
// 计划:MTC 会展开所有阶段的解空间,选择整体代价最小的组合
if (task->plan(5) == moveit::core::MoveItErrorCode::SUCCESS) {
task->execute(*task->solutions().front());
}
MTC 的价值在于「全局搜索」:它不是逐阶段贪心,而是展开各阶段的候选解(多个抓取位姿 × 多条连接路径),选整体代价最小的组合。这解决了「贪心选了一个抓取位姿,结果后面接不上」的问题。代价是求解时间更长(数秒),因此适合离线规划或允许等待的场景。
10. 与真实控制器的轨迹交接
规划出的轨迹要交给真实控制器执行,这一交接是「规划成功但执行失败」的高发区。
交接的三个检查点:
1. 起始点匹配
轨迹第一个点的关节角必须与当前 /joint_states 在容差内
否则控制器报 "start point not within tolerance"
对策:规划前先更新 RobotState,或设置合理的起始容差
2. 时间戳
轨迹点的时间戳必须单调递增且从当前时刻开始
规划耗时(如 1 s)会让「从 0 开始」的时间戳过期
对策:执行前重新做时间参数化,或让控制器接受相对时间
3. 速度/加速度缩放
max_velocity_scaling_factor 只缩放速度,不缩放加速度
时间被拉长后,加速度需求反而可能增加
对策:同时设置 max_acceleration_scaling_factor
// 规划与执行的缩放因子策略
arm.setMaxVelocityScalingFactor(0.3); // 调试 0.1~0.2,生产 0.5~0.8
arm.setMaxAccelerationScalingFactor(0.2); // 通常 ≤ 速度缩放
arm.setPlanningTime(1.0);
arm.setNumPlanningAttempts(5);
// 执行前确认状态已同步(异步执行时尤其重要)
arm.asyncExecute(plan); // 或 execute(plan) 阻塞等待
规划与执行必须用同一份 RobotState。如果规划时用的是 100 ms 前的关节状态,而执行时机器人已经动了一点,起始点不匹配会导致执行失败或产生微小跳变。异步场景下,规划完成后应再次拉取最新状态并校验,必要时重新规划。
11. 可视化、调试与性能剖析
MoveIt 的问题大多能在 RViz 里看出来,善用可视化能省下大量真机调试时间。
RViz MotionPlanning 插件的关键显示项:
Planning Scene :世界几何、附着物体(先确认场景对不对)
Planned Path :规划出的路径(看是否绕远、是否贴障)
Start State :规划时的起始状态(看是否与真机一致)
Goal State :目标状态
Collision Contacts :碰撞点(诊断「为什么判定碰撞」)
Trajectory Trail :执行轨迹(对比规划与实际)
# 常用诊断命令
ros2 topic echo /monitored_planning_scene --once # 场景内容
ros2 service call /compute_ik moveit_msgs/srv/GetPositionIK "{...}"
ros2 topic hz /joint_states # 状态频率
ros2 run moveit_ros_benchmarks moveit_benchmark # 规划器基准测试
ros2 param get /move_group robot_description_kinematics.panda_arm.kinematics_solver
关键监控指标有四个:规划耗时(p50/p99)、规划失败率、逆解失败率、场景一致性告警(FixStartStateCollision 触发次数)。规划失败率超过 5% 就应排查配置,配置良好的系统应在 1% 以下。规划耗时是重尾分布,必须看 p99 而非均值。
12. 仿真验证与回归测试
MoveIt 的配置改动必须在仿真里回归,因为很多问题只在特定位形下出现。
回归测试的最小用例集:
1. 从多个起始位形规划到多个目标位形(覆盖工作空间)
2. 带障碍物的场景(验证碰撞规避)
3. 抓取与放置全流程(验证附着物体的增删)
4. 逆解边界位形(验证 IK 插件成功率)
5. 长时间连续规划(验证场景不累积垃圾物体)
指标:成功率、p50/p99 规划耗时、路径长度、逆解调用次数
# moveit_ros_benchmarks 可对多个规划器做定量对比
ros2 launch moveit_ros_benchmarks panda_benchmark.launch.py \
planner_configs:=RRTConnect BITstar
# 输出成功率、耗时分布、路径长度,用于选型决策
仿真验证的载体见机器人仿真一篇,抓取位姿如何从感知得到并进入 MTC 流水线见机器人视觉与抓取 。逆解插件(KDL/TRAC-IK/IKFast)的取舍与执行侧的控制器配置见机械臂控制与抓取规划一篇,本文不再重复。
权衡取舍
| 决策 | 选 A | 选 B |
|---|---|---|
| 规划器 | RRTConnect:首次解最快 | RRTstar/BITstar:路径更优、更慢 |
| 规划时间 | 短(0.5s)+ 多次重试:快 | 长(2s):成功率略高但失败更慢 |
| 碰撞安全裕度 | 大:安全、窄空间无解 | 小:能过窄处、靠传感器兜底 |
| 场景同步 | 增量话题:省带宽 | 完整场景:一致性强 |
| 笛卡尔步长 | 小(0.005):平滑、慢 | 大(0.05):快、可能跳过奇异 |
| 任务编排 | 手工状态机:可控 | MTC:全局最优、求解慢 |
| 逆解插件 | KDL:零配置 | TRAC-IK:成功率高 |
核心原则:先保证规划场景正确,再谈规划器调优。九成的「规划失败」根因是场景不一致(附着物体没更新、碰撞体坐标系错误、ACM 漏配),而不是规划器参数不好。
常见坑清单
- 抓取后忘记附着物体,规划器认为臂是空的,抬起时撞到刚抓的物体——抓取成功必须
applyAttachedCollisionObject(ADD)。 touch_links未包含夹爪连杆,附着物体被判为碰撞,永远规划失败——把必然接触的连杆列入 touch_links。- 碰撞体
header.frame_id用了 TF 树里不存在的坐标系,物体被静默丢弃——坐标系必须存在于 TF 树。 - ACM 的
Never条目错标,真实碰撞被漏检导致撞机——Setup Assistant 生成后必须人工核对。 - 规划时间设成 10 s,失败场景等待过久——按 p99 设定,配合有限次重试。
longest_valid_segment_fraction太大,路径离散化漏检细障碍——机械臂取 0.005 量级。- 笛卡尔路径
fraction小于 1.0 仍直接执行,轨迹不完整——必须检查 fraction 接近 100%。 jump_threshold设为 0,奇异附近关节瞬间大幅跳变——设为合理值而非禁用。- 规划与执行用不同时刻的 RobotState,起始点不匹配导致执行失败——执行前校验并同步状态。
- 只缩放速度不缩放加速度,时间拉长后加速度需求反增——同时设置加速度缩放因子。
小结
MoveIt 的成败在配置而不在算法:规划场景是否与现实一致、ACM 与碰撞检测是否正确、规划器与规划时间是否匹配场景、任务编排是否覆盖多阶段。把这几件事做对,默认的 RRTConnect 就能满足绝大多数需求。
下一步建议阅读机械臂控制与抓取规划一篇,看规划出的轨迹如何经 ros2_control 与 joint_trajectory_controller 落地执行,以及逆解插件与装配力控的细节;机器人视觉与抓取一篇讲清抓取位姿如何从感知进入规划;运动学正逆解 是理解逆解插件与奇异问题的基础。
最后一句经验:每次「规划失败」先打印规划场景,确认世界是对的。场景正确之后,剩下的才是规划器参数问题。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。