
做机械臂轨迹规划这一年多我被MoveIt 2默认的OMPL采样规划折磨过很多次。最典型的一次是给精密装配任务调轨迹机械臂末端明明要走一条直线但关节在运动过程中出现几次明显顿挫示教出来的轨迹看起来都不像人写的。后来接触MPC模型预测控制把轨迹生成换成滚动优化问题来处理轨迹质量有了质的改变。这篇博文完整复盘我在ROS2 MoveIt 2环境下配置MPC轨迹规划器、并逐步做参数调优的整个实战过程内容涵盖插件加载机制、配置文件写法、预测时域与权重矩阵的调试经验以及仿真迁移到实机时踩过的坑。适合已经会用MoveIt 2基础功能、准备进一步做高质量轨迹规划的开发者参考。1. 从OMPL到MPC同样是规划为什么轨迹质量差距这么大1.1 采样规划在工程任务上的隐性短板OMPL是MoveIt 2默认集成的采样规划库原理是在配置空间C-space随机采样、构建搜索树或概率路线图找出一条从起始位姿到目标位姿的几何路径再通过时间参数化模块给路径加上速度和加速度信息。这个流程本身没有错但工程上大家都有体会采样规划的路径往往偏保守绕远、折线、有回退而且时间参数化阶段只考虑运动学限制完全不考虑机械臂的动力学模型比如关节力矩极限、重力补偿后的残余力矩、动力学耦合。于是很多看起来合理的轨迹一旦上实际机器人就会暴露问题关节速度曲线不平滑加速度存在大量高频跳变导致末端抖动。接近目标点时为了避开一个微小的碰撞检测误差规划器突然规划出一条几乎反转的路径。遇到动态障碍物或目标点移动时需要重新规划而重新规划耗时往往在几十到几百毫秒无法满足实时响应。我在UR5e上跑OMPLRRTConnect做过一次统计规划一个末端位移200mm的点到点运动平均耗时大约120ms路径经过时间参数化后关节2的加速度峰值接近12 rad/s²但驱动器实际能平滑跟踪的加速度上限只有8 rad/s²。这意味着每次高速运动时驱动器都在挣扎。低速任务里还能忍节拍一提上来就非常危险。装配、涂胶、焊接这类要求轨迹平滑、机械臂持续贴着路径走的场景前两个问题几乎不可接受。1.2 MPC滚动优化为什么能解决这个痛点MPC的全称是Model Predictive Control模型预测控制。它不再是一次性找一条路径而是把轨迹生成看作一个连续滚动的最优控制问题。在每个控制周期内基于机械臂的动力学模型预测未来一段时间内系统的状态演变在线求解一个带约束的最优控制问题得到最优控制序列只执行第一个控制量下一周期重新滚动求解。这就像一个在陌生路段开车的人不会一次性把整条路线的每一步都规划死而是不断向前看几秒钟的路况根据前方来车、路口信号、路面情况实时调整方向盘和油门刹车。虽然每次只看一小段路但因为始终有预测和反馈校正实际效果往往比一口气跑完全程好很多。机械臂轨迹规划引入MPC核心收益有三点。第一可以把关节力矩限制、速度限制、加速度限制、末端笛卡尔空间路径约束同时放进优化问题里求出来的轨迹天然满足动力学约束。第二每一拍都在滚动求解对于动态变化的障碍物或目标点随时可以把最新环境信息塞进优化问题响应实时性远高于重新采样规划。第三优化目标函数里可以显式写轨迹平滑度惩罚项从源头抑制抖动。1.3 哪些项目值得上MPC哪些不值得先说结论MPC不是万金油它有两高——模型要求高、算力要求高。如果机械臂只需要做点到点搬运没有强动力学限制周围环境完全静态那OMPL加时间参数化完全够用没必要引入MPC。但项目中只要有下面任一特征MPC就值得认真考虑动态避障需求环境中有移动的人或设备机械臂需要实时调整轨迹。高节拍生产线要求在保证关节冲击不超过极限的情况下跑尽量快的轨迹。精密装配或力控打磨末端路径误差和时间精度要求都很高。具备相对准确的机械臂动力学模型包括连杆质量、惯量、摩擦力辨识结果。维度OMPL采样规划TRAJOPT/STOMPMPC轨迹规划模型依赖仅运动学运动学可选动力学为主约束类型碰撞、关节限位碰撞、平滑碰撞、关节、力矩、路径实时性百毫秒级重规划百毫秒到秒级毫秒到十毫秒级轨迹平滑度依赖后处理较好最优最适合场景静态环境避障离线轨迹优化动态环境、精密作业2. 环境组合选型与MPC规划插件的前期搭建2.1 版本选择背后的避坑思路我写这套方案时使用的组合是Ubuntu 22.04 LTS系统ROS2 Humble发行版MoveIt 2对应Humble分支机械臂模型用UR5e的URDF/XacroMPC求解器基于OSQP二次规划求解配合自研的机械臂动力学模型包装层。为什么不选更新的Jazzy或Rolling做机器人软件有一条原则稳定优先。Humble和Ubuntu 22.04都是LTSMoveIt 2官方二进制包维护力度最大网上资料也最多。Jazzy虽新但不少第三方插件仓库还停留在Humble的API兼容状态一旦遇到接口变动整个项目都要跟着改。这里还建议一条能用apt直接安装ros-humble-moveit就不要自己去源码编译MoveIt 2全家桶否则编译依赖链能把一上午耗光而且自己编译的版本与pluginlib、moveit_core的ABI可能对不上。2.2 MoveIt 2里MPC规划插件的安装与编译MoveIt 2本身没有内置一个叫MPC的默认规划器它是通过pluginlib机制加载各种规划plugin的。我使用的MPC插件是一个开源仓库基于moveit_core提供的PlannerManager接口实现内部用OSQP求解有限时域二次规划问题。具体安装步骤mkdir -p ~/mpc_ws/src cd ~/mpc_ws colcon build安装基础依赖sudo apt install ros-humble-moveit ros-humble-ros2-control ros-humble-ros2-controllers sudo apt install libosqp-dev libosqp-eigen-dev把MPC插件包克隆到src目录并编译cd ~/mpc_ws/src git clone https://github.com/your-example/moveit_mpc_planner.git cd ~/mpc_ws colcon build --symlink-install --packages-select moveit_mpc_planner source install/setup.bash编译报错大多集中在这几类。一是找不到moveit_core头文件原因通常是没装ros-humble-moveit-msgs、ros-humble-moveit-core二进制包或者当前shell没有source ROS2环境。二是找不到OSQP头文件libosqp-dev安装后头文件在/usr/local/include/osqp/目录如果插件仓库按老路径找头文件需要在CMakeLists.txt里手动加include_directories。三是与Eigen版本冲突插件里用了Eigen::Geometry和Eigen::MatrixXd如果ROS2自带的eigen3版本和编译时用的版本不一致链接阶段会报一堆符号找不到。踩过这个坑后我总结出一条经验在工作空间编译插件一定要先执行source /opt/ros/humble/setup.bash再用colcon build。这看似是废话但很多人直接在干净的终端里编译ROS2环境变量没有进入编译器搜索路径报错千奇百怪。2.3 机械臂描述文件和动力学参数的准备MPC规划器和MoveIt 2其他规划器一样需要一份机械臂URDF和SRDF来描述运动学结构额外还需要一个简化版本的动力学模型参数。我的做法是URDF里写入连杆惯量mass、inertia再写一个YAML文件把每个关节的摩擦系数、阻尼系数补上作为MPC优化模型的一部分。摩擦系数如果没有做参数辨识先给一个很小的初值比如0.01到0.05 N·m/(rad/s)后面实机调优时再手动修正。这个阶段容易漏掉的是MoveIt 2配置助手生成的config目录里默认只有OMPL的planning.yaml不会自动生成MPC插件的配置。需要自己补一个mpc_planning.yaml并在后续的启动文件里显式声明这条planning pipeline。3. MoveIt 2里MPC插件从注册到跑通第一个规划请求3.1 先理清MoveIt 2的planning pipeline结构MoveIt 2中move_group节点收到规划请求后会交给moveit_planning_pipeline处理分为两层。第一层是Planner plugin真正执行路径搜索或轨迹优化OMPL是这类插件MPC规划器同样是。第二层是planning request adapter一组可插拔的预处理和后处理模块比如FixStartStateCollision在起始位姿本身处于碰撞时尝试修正FixStartStateBounds把关节位置恢复到限位内AddTimeParameterization给路径添加时间信息。每个adapter按顺序执行构成一条链。在MPC方案里AddTimeParameterization这个adapter建议去掉因为MPC规划器内部已经输出带时间戳的轨迹保留它反而会把MPC生成的速度信息二次加工导致速度曲线变形。3.2 插件描述文件与package.xml的注册pluginlib依赖XML文件发现插件。我使用的MPC插件包结构如下moveit_mpc_planner/ ├── package.xml ├── CMakeLists.txt ├── include/moveit_mpc_planner/mpc_planner_manager.h ├── src/mpc_planner_manager.cpp ├── config/mpc_planner_plugin.xml └── config/ur5e_mpc_planning.yamlpackage.xml中需要添加export moveit_core plugin${prefix}/config/mpc_planner_plugin.xml/ /exportmpc_planner_plugin.xml里描述插件的类名和库路径library pathlib/libmoveit_mpc_planner class namemoveit_mpc_planner/MPCPlannerManager typemoveit_mpc_planner::MPCPlannerManager base_class_typeplanning_interface::PlannerManager descriptionMPC trajectory planner plugin for MoveIt 2/description /class /librarytype必须是插件类在C代码里的全限定名base_class_type必须是planning_interface::PlannerManager。很多新手在这里写错类名或路径MoveIt 2启动时就会提示PluginlibFactory检索不到插件。这块排查不难但是第一次接触pluginlib机制时很容易卡住。3.3 在move_group启动链路里接入MPC规划管线我用的是在move_group.launch.py里增加planning pipeline配置的方式。如果是由MoveIt Setup Assistant生成的启动文件通常在config目录下有一个move_group.launch.py里面通过参数给入planning_pipelines列表declare_parameter( planning_pipelines, [ompl, mpc], descriptionList of planning pipelines to use, )启动时传入参数ros2 launch my_robot_moveit_config move_group.launch.py planning_pipelines:[ompl,mpc]同时要在config目录下准备好mpc_planning.yaml定义pipeline名称和adapter链planning_pipelines: - pipeline_name: mpc planners: [] planning_adapters: - default_planner_request_adapters.FixStartStateBounds - default_planner_request_adapters.FixWorkspaceBounds - default_planner_request_adapters.FixStartStateCollision - default_planner_request_adapters.FixStartStatePathConstraints注意这里没有写AddTimeParameterization理由前文已经说过。3.4 用MoveIt 2 API发起第一次MPC规划配置文件写完并重新启动move_group如果插件正确加载日志里会看到类似[pluginlib_loader]: Successfully loaded plugin moveit_mpc_planner/MPCPlannerManager然后我用一个C测试程序来发规划请求moveit::planning_interface::MoveGroupInterface move_group(planning_plugin_name); move_group.setPlannerId(mpc); move_group.setStartStateToCurrentState(); move_group.setPoseTarget(target_pose); auto plan move_group.plan();如果plan.error_code不是SUCCESS多半是MPC内部求解失败涉及后面要讲的参数问题。这一步先确保插件链路是通的能进入MPC的求解入口。3.5 一个容易被忽略的坑配置重新生成导致MPC配置丢失如果你用MoveIt Setup Assistant重新生成config它会默认用初始的ompl_planning.yaml覆盖掉原文件。即使之前配置好了mpc_planning.yaml重新生成后也要手动加回来。我在项目改版时犯过一次启动move_group后MPC pipeline直接消失排查半天才发现config被覆盖。这个细节提醒大家任何基于MoveIt Setup Assistant重新生成配置的操作做完之后都要核对一下planning_pipelines列表确认自定义管线还在。4. 逐参数调优预测时域、权重矩阵和控制周期怎么定才不玄学4.1 预测时域N视野长短与求解负担的博弈预测时域N决定MPC每次向前看多少步。N越小优化问题规模越小、求解越快但规划视野短对前方约束如障碍物、关节限位的反应不足N越大越接近全局最优但求解耗时显著增长还容易出现数值病态。我在UR5e上做了一组对比实验执行同一个末端直线运动固定其他参数只改N预测时域N单次求解耗时末端最大路径偏差关节速度跳变次数备注59ms3.2mm7轨迹短视明显偏离直线1014ms1.5mm3可接受但仍有抖动2021ms0.7mm0综合效果最好3038ms0.6mm0耗时翻倍收益很小5079ms0.5mm0不适合实时控制结论很直接这个场景下N20是甜点区N30以后实际轨迹质量没有本质提升但求解消耗暴增。调优建议是先从N20起步打印每个控制周期的求解耗时如果平均耗时小于控制周期的50%可以尝试增大N看轨迹质量变化如果耗时逼近控制周期果断减小N并考虑使用更快的求解器或简化动力学模型。4.2 权重矩阵Q和R优化目标背后的物理含义MPC的代价函数一般写成J sum_{k1}^{N} (e_k^T Q e_k u_k^T R u_k) x_N^T Q_N x_N其中e_k是状态误差比如末端位姿误差和关节速度u_k是控制增量或关节力矩Q是状态权重R是控制权重Q_N是终端权重。如果Q里末端位置误差权重远大于关节速度权重MPC会优先保证末端到达目标位置但可能在末端到位瞬间产生较大关节速度变化反过来如果关节速度权重过大机械臂会变得很慢、很软末端定位精度下降。我在调UR5e时使用的初始权重和后调整权重如下状态或控制变量初始权重调优后权重调优理由末端位置误差x/y/z200800装配任务对末端精度要求高末端姿态误差RPY50100姿态不需要过于激进关节速度105允许关节适当运动关节加速度2030抑制冲击控制量力矩2015避免力矩过大导致驱动器饱和调参中一个关键点权重量纲要匹配。末端位置误差以米为单位关节速度以弧度每秒为单位如果直接放在同一个二次型里中间差好几个数量级。调参前最好先对状态变量做归一化把位置误差除以最大允许位置误差速度误差除以最大允许速度统一变成无量纲量权重才有实际意义。我踩过的坑就是一开始没做归一化直接给末端位置权重1000、关节速度权重10结果MPC完全忽略关节速度。末端接近目标时猛冲过去关节速度瞬间冲高控制器为了不超限又急刹形成非常剧烈的抖动。4.3 控制周期dt与实时性预算控制周期dt是MPC每次求解的时间步长MoveIt 2在对规划器输出轨迹时通常按固定dt输出位姿点。dt越小轨迹分辨率越高但预测时域对应的物理时间越短。比如N30但dt10ms只能预测未来300msdt大求解变快但轨迹粗糙。推荐做法是让MPC单次求解耗时不超过dt的30%到50%。比如dt20ms求解耗时尽量控制在10ms以内。如果超了优先减少N其次是缩小状态维度比如只对前三个主要关节做MPC其余关节用逆运动学解算最后才是换求解器。4.4 约束离散化与碰撞检测步长MPC的约束处理是一个大坑。如果采样点之间的步长太大机械臂可能从障碍物一侧跳到另一侧而无碰撞检测发现。我的经验是障碍物检测步长要明显小于机械臂连杆中最小宽度的一半一般取10到20mm一个检测点。我一开始用30mm步长末端连杆半径约25mm在快速运动中直接穿透了一个细柱形障碍物MPC完全没有反馈直到实机动作结束才发现碰撞检测根本没生效。另外要把关节位置极限设置成软约束加硬约束两层。硬约束由OSQP直接处理软约束放在目标函数的罚函数项里。这样即使某个极限无法满足MPC也不会直接求解失败而是尽量逼近极限至少在工况极端时能给出一个可用轨迹。4.5 可直接抄作业的参数基线以下是我在UR5e上经过三轮调优后最终采用的参数可以作为起点再针对自己的机械臂调整mpc_controller: dt: 0.02 # 控制周期20ms horizon: 20 # 预测时域 max_iterations: 50 # 求解器最大迭代 solver_tolerance: 1e-4 q_pose: [800.0, 800.0, 800.0] # 末端x/y/z位置误差权重 q_orientation: [100.0, 100.0, 100.0] q_velocity: [5.0, 5.0, 5.0, 5.0, 5.0, 5.0] # 关节速度 q_acceleration: [30.0, 30.0, 30.0, 30.0, 30.0, 30.0] # 关节加速度 r_input: [15.0, 15.0, 15.0, 15.0, 15.0, 15.0] # 控制输入 state_normalize: true collision_step_size: 0.02 # 碰撞检测步长20mm collision_inflation: 0.02 # 障碍物膨胀2cm这个配置在我测试的UR5e装配和涂胶任务上单次规划平均耗时21ms轨迹平滑末端最大路径偏差0.7mm满足大多数中等精度作业需求。5. 实机调试日志我踩过的六个故障和完整排查链路5.1 故障一MoveIt 2启动即崩溃MPC插件加载失败现象启动move_group.launch.py后日志提示无法加载planner mpc。排查链路第一步查看pluginlib相关完整日志确认是不是XML找不到。第二步用ros2 pkg prefix moveit_mpc_planner确认功能包已经安装到当前工作空间。第三步查看build目录里是否生成了libmoveit_mpc_planner.so。第四步直接检查XML文件路径和package.xml里的${prefix}路径是否一致。根因是我一开始把config目录放在src里但没有在CMakeLists.txt里加install指令导致colcon build后config/mpc_planner_plugin.xml没有被复制到install目录pluginlib自然找不到。解决办法是在CMakeLists.txt中添加install(DIRECTORY config DESTINATION share/${PROJECT_NAME} )5.2 故障二规划请求超时MPC求解器一直在迭代现象发送规划请求后move_group长时间处于busy状态直到请求超时被取消。排查链路先在MPC插件的求解入口打印每个周期的迭代次数和残差发现残差在前30次迭代内下降缓慢怀疑问题出在初始解或问题病态性。然后检查权重矩阵发现末端位置误差权重已经调到2000且未归一化。末端误差变化1mm会让代价函数变化很大而关节速度误差1rad/s引起的代价变化相对很小两者差距过大导致Q矩阵接近病态。修复方案分两步。第一步对状态变量做归一化把权重降到合理范围。第二步初始解改进。最初用全零向量作为初始解改成用上一时刻的MPC解做热启动或者在启动前先用当前关节速度匀速外推一小段作为初始轨迹收敛速度明显提升。实测中热启动能让单次求解耗时从35ms降到18ms效果立竿见影。5.3 故障三仿真中轨迹正常实机末端抖动剧烈现象在Gazebo或RViz显示中轨迹平滑但实机跑起来末端高速抖动伴随异响。排查链路先用ros2 topic hz /joint_states检查控制器实际运行频率发现只有40Hz而MPC输出是50Hz两者不一致导致插值器不断调整目标值。把MPC输出频率降到40Hz后抖动有所缓解但没根治。接着检查速度前馈是否启用发现位置环加速度环的增益偏高外部一个很小的速度波动就被放大。最后给关节速度指令加了一个15Hz的一阶低通滤波器抖动明显改善。经验是仿真基本不暴露频率不匹配问题实机一跑就露馅。实机调试前务必逐个模块核对发布频率建一个简单台账列清楚每个topic的目标频率和实际频率。5.4 故障四MPC规划的轨迹仍然与障碍物发生碰撞现象RViz里路径看起来避开了障碍物但实际运行中机械臂还是和障碍物剐蹭。排查链路一开始检查分段数N20、dt20ms对应物理时间只有400ms在快速运动中末端每20ms走大约5mm按常规理解步长不大。真正的问题出在碰撞检测模型上之前用球体包络连杆被近似成几个大球球与球之间存在缝隙碰撞检测忽略了这些缝隙细长障碍物正好从缝隙穿过。解决办法是把碰撞模型改成贴合紧密的凸包形式或者把胶囊体碰撞体在配置时细分。同时把碰撞检测步长采样密度提升到20mm。这个故障非常隐蔽从轨迹可视化上看不出来只有实机运行才能发现。后来我又在MPC碰撞约束里加了安全膨胀量所有障碍物向外膨胀3cm才算彻底根治。5.5 故障五MoveIt 2在多次规划后内存持续增长现象长时间批量规划后move_group内存占用从500MB一路涨到2GB最终触发OOM。排查链路用gdb attach到move_group进程看到大量空闲的OSQP工作区对象。根因是MPC插件每次求解都new了一个OSQPWorkspace对象结束时没有调用osqp_cleanup释放。修复方法很直接把OSQP solver实例定义为插件类成员变量整个生命周期只初始化一次每次求解只更新数据并调用osqp_solve。修复后内存稳定在600MB左右。这也说明一个问题插件虽然跑在move_group进程里但内存管理完全由自己负责MoveIt 2不会帮你兜底。写这类长期运行的插件凡是涉及动态内存分配的地方都要格外小心。5.6 故障六轨迹连接点速度跳变现象分段执行任务时第一段轨迹结束时刻的速度与第二段开始时刻的速度相差很大机械臂在切换瞬间出现明显冲击。原因分析MoveIt 2中多次规划是相互独立的默认方案没有把上一段轨迹的终点速度传给下一次MPC求解的初始条件。解决办法是自定义一个planning request adapter在MPC求解前从move_group的当前状态中读取关节速度把它作为MPC初始状态的一部分。adapter链最前面插入planning_adapters: - custom_trajectory_adapters.SetCurrentVelocityAsInitial - default_planner_request_adapters.FixStartStateBounds ...加上以后第二段轨迹的起始速度与第一段终点速度对齐切换瞬间的加速度冲击基本消失。个人体会是MPC轨迹规划这件事很像调PID七分靠模型三分靠参数。模型不准、约束建模粗糙再好的权重矩阵也救不回来。而一旦把动力学模型、归一化权重、碰撞检测这些底层细节打牢MPC在轨迹质量上的提升非常明显。我现在的习惯是接到一个新机械臂先用阶跃辨识摸清每个关节的时间常数和摩擦系数再进入MPC参数调优。这套流程走顺以后新机型从零到上线运行大概能缩短到一周以内。希望这篇实战记录能帮你省去一些试错时间。