ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

机械臂轨迹规划:关节空间与笛卡尔空间怎么选?

2026/9/7 22:52:06 拓冰建站 浏览量
机械臂轨迹规划:关节空间与笛卡尔空间怎么选? 最近后台私信里被问得最多的问题就是机械臂轨迹规划到底该用关节空间还是笛卡尔空间。很多小伙伴在玩机械臂的时候从ROS里的MoveIt示例程序开始接触轨迹规划看到别人贴的代码里一会是setJointValueTarget一会是computeCartesianPath直接一脸懵。这俩货到底有什么区别什么时候该用哪个今天我就把这块内容掰开了揉碎了讲清楚最后还会直接上一段可以跑起来的硬核代码保证你看完能自己写规划代码。先说个简单的定义方便完全没接触过的小白有个基础印象。关节空间轨迹规划描述的是机械臂每个关节从角度A转到角度B的过程规划出来的是关节角度随时间变化的曲线。笛卡尔空间轨迹规划描述的是机械臂末端执行器在三维空间里走的路径规划出来的是末端位姿随时间变化的曲线比如要让机械臂沿着一条直线去抓取物体。这样听起来可能还不够直观。我换个说法关节空间规划像是在心里默念“肩膀抬30度、手肘弯45度”笛卡尔空间规划则像是在心里默念“手要沿着这条直线移动10厘米”。前者的思维对象是关节后者的思维对象是手。理解到这个层面我们才能往下聊更深的东西。1. 两个空间到底差在哪从几何直觉到数学本质1.1 用夹菜和画直线的例子理解空间差异我第一次跟朋友解释这两个概念用的是夹菜的例子。你想让机械臂帮人夹一道菜从餐盘的一端夹到另一端中间会经过汤碗的上方。如果你用关节空间规划你只需要告诉机械臂“起点是这个关节角度终点是那个关节角度”中间的路径它自己算。问题是它可能直接从汤碗正上方穿越过去甚至可能从汤碗里穿过去整条路径走得像喝醉了酒一样扭来扭去。这是因为关节空间规划不关心末端在三维空间里走了什么形状它只关心关节从哪到哪。如果你用笛卡尔空间规划你可以明确要求“手从点A沿着一条直线移动到点B”机械臂会努力保证末端走出一条漂亮的直线绕开汤碗正上方。换句话说关节空间规划管不到末端路径的形状笛卡尔空间规划则把末端路径的形状作为硬约束。这个区别在画直线的场景里更明显。你想让机械臂拿着笔在纸板上画一条笔直的线用关节空间规划出来的路径末端往往是弧线因为多个关节同时转动时叠加出来的末端运动轨迹本来就难以控制成直线。用笛卡尔空间规划末端就能老老实实地走出直线。所以凡是涉及到涂胶、焊接、写字、切划这类对末端路径形状有要求的任务基本都得用笛卡尔空间。1.2 从DH参数和正逆运动学看数学本质再往深一层说机械臂的数学建模通常用DH参数Denavit-Hartenberg参数。通过DH参数我们可以建立每个关节坐标系之间的变换关系然后用正运动学Forward Kinematics简称FK算出末端执行器在笛卡尔空间里的位姿。从关节空间到笛卡尔空间是一个从n维关节向量到6维位姿向量的映射[ T_{base}^{tool} f(\theta_1, \theta_2, ..., \theta_n) ]反过来从笛卡尔空间到关节空间就是要求解逆运动学Inverse Kinematics简称IK问题[ (\theta_1, \theta_2, ..., \theta_n) f^{-1}(T_{base}^{tool}) ]这个逆运动学方程有没有解析解、是不是唯一解取决于机械臂的结构。比如UR5这种球形手腕结构的机械臂有解析解计算快且稳定而一些7自由度机械臂是冗余的同一个末端位姿可能有无数个关节构型。这也是为什么笛卡尔空间规划在实时性上会有压力——它每一步都可能需要调用IK求解器如果IK解不出来整条路径就断掉了。关节空间规划就轻松得多它直接在关节空间里插值不需要反复求解IK。比如从当前关节角度到目标关节角度用五次多项式插值或者梯形速度曲线就能生成一条平滑的关节运动轨迹。这里的关键不是末端走了什么路线而是每个关节的角度变化是否平滑、是否满足速度加速度约束。1.3 为什么要同时在两个空间里思考实际开发中你会发现两个空间是互补的不是互斥的。完整的轨迹规划链条是先在前端做任务和路径规划生成末端要走的笛卡尔路径然后用逆运动学把路径点转换成关节角度最后在关节空间做插值和速度规划生成可以执行的运动指令。换句话说你上层的任务逻辑、避障逻辑要放在笛卡尔空间思考因为你需要知道末端在哪个位置、会不会撞到障碍物你下层的电机控制、平滑运动要放在关节空间思考因为电机的指令本质上是关节角度或关节速度。MoveIt里的OMPL规划器默认也是在关节空间做采样和规划的但它的规划目标可以是用笛卡尔空间位姿指定的。理解了这一层你就明白为什么不能盲目选空间规划。选哪个空间本质上是问你更关心末端行为还是更关心关节行为更关心末端行为就选笛卡尔空间更关心关节行为和规划效率就选关节空间。2. 从零开始环境准备与工具链梳理2.1 开发环境的实际选择ROS 2还是ROS 1聊代码之前先把开发环境捋清楚。现在网上搜机械臂轨迹规划的资料搜出来的结果一半是老掉牙的ROS 1教程一半是刚起步的ROS 2文档新手很容易陷入环境选择的纠结。我的建议很直接如果你用的是较新的Ubuntu版本比如22.04或24.04直接上ROS 2。ROS 1的Noetic是最后一个版本社区已经停止维护新项目再开在老版本上等于给自己挖坑。ROS 2的moveit2包已经非常成熟API设计摆脱了ROS 1时代很多奇怪的写法而且它对实时性、多机通信、参数服务的支持都更好。具体版本对应关系是Ubuntu 22.04配HumbleUbuntu 24.04配Jazzy。你别看到Jazzy版本号新就觉得是测试版ROS 2的发行版机制是同步发版、同步维护Jazzy就是24.04的官方标配。装完系统之后sudo apt install ros-jazzy-desktop sudo apt install ros-jazzy-moveit开发语言方面控制机械臂的规划代码主要是C和Python两种选择。Python在原型验证和教学场景里很受欢迎C更适合产品化落地。我下面的代码给C版本因为实际项目中C的使用率更高性能也更好。2.2 用仿真环境做验证Gazebo还是MuJoCo写规划代码之前一定要有个好用的仿真环境不然真机械臂上跑错代码轻则撞限位重则撞人撞设备。我调试轨迹规划最喜欢用的是Gazebo配合MoveIt因为MoveIt和Gazebo的集成非常成熟可以直接用ros2 launch moveit_resources_panda_moveit_config demo.launch.py拉起一个带Gazebo仿真的完整环境Panda机械臂在里面可以真实地执行规划出来的轨迹。最近MuJoCo也很火尤其是做强化学习的朋友特别喜欢因为它的物理引擎计算速度快Python接口友好。但坦白说MuJoCo和MoveIt的集成没有Gazebo那么顺滑需要自己写插件做桥接。如果你只是想学轨迹规划不想折腾底层集成用Gazebo就够了。如果你后续要做强化学习、要训练PPO策略来控制机械臂那MuJoCo更合适因为它的仿真速度快可以并行跑很多环境。我自己的开发习惯是先用MoveIt自带的RViz可视化调试算法逻辑确认轨迹合理了再丢进Gazebo做物理仿真验证最后才上真机。这个三级验证的流程帮我避免了很多次机器事故强烈推荐大家也这么干。2.3 控制真机之前必须搞懂的接口问题很多小伙伴问“我要控制这个机械臂怎么控制”这个问题如果没有仿真经验确实容易一头雾水。控制真机机械臂一般有三层接口第一层是驱动层直接和伺服电机通信接收位置指令、反馈编码器读数通常走EtherCAT、CAN总线或者串口。第二层是控制层把规划出的关节轨迹转换成各个电机的控制指令。第三层是规划层也就是MoveIt干的活负责生成轨迹。你自己写轨迹规划代码工作在第三层你要做的就是把规划好的JointTrajectory发出来由驱动层去执行。MoveIt提供了move_group节点作为中介规划代码通过ROS 2的action接口把目标发给move_groupmove_group规划好轨迹后再通过FollowJointTrajectoryaction把轨迹发给驱动节点。如果你是自己写驱动需要实现这个action server接收轨迹点控制电机执行。市面上很多机械臂都自带ROS 2驱动比如越疆、珞石甚至是开源方案的so-101机械臂都有对应的驱动包。买机械臂或者自己组装之前一定要先确认驱动支持ROS 2不然又要自己造轮子写驱动那工作量就大了。我这篇文章后面的代码跑通MoveIt仿真后只要驱动接口是标准的FollowJointTrajectory就能直接迁移到真机上。3. 核心实操对比两种轨迹规划的代码实现与效果差异3.1 关节空间规划5分钟上手的平滑运动我们先用MoveIt的C API写一个关节空间规划的简单例子。这个段代码做的事情很纯粹把机械臂从当前姿态移动到一个预设的关节角度位置。#include moveit/move_group_interface/move_group_interface.h #include moveit/planning_scene_interface/planning_scene_interface.h #include chrono int main(int argc, char** argv) { rclcpp::init(argc, argv); auto node std::make_sharedrclcpp::Node(joint_space_planner); auto logger rclcpp::get_logger(joint_space_planner); // 使用async参数让move_group_interface在后台独立spin moveit::planning_interface::MoveGroupInterface move_group(node, panda_arm); // 设置允许的最大速度和加速度缩放防止运动过猛 move_group.setMaxVelocityScalingFactor(0.1); move_group.setMaxAccelerationScalingFactor(0.1); // 目标关节角度rad以Panda机械臂7个关节为例 std::vectordouble joint_target {0.0, -0.785, 0.0, -2.356, 0.0, 1.571, 0.785}; move_group.setJointValueTarget(joint_target); moveit::planning_interface::MoveGroupInterface::Plan my_plan; bool success (move_group.plan(my_plan) moveit::planning_interface::MoveItErrorCode::SUCCESS); if (success) { move_group.execute(my_plan.trajectory_); } rclcpp::shutdown(); return 0; }这个代码看着很简单但里面有几个关键点值得注意。首先是setMaxVelocityScalingFactor和setMaxAccelerationScalingFactor这两行新手最容易忽略。默认情况下MoveIt会以100%的速度执行轨迹对于仿真无所谓但真机上第一次跑很容易吓人一跳甚至损坏机构。我第一次上真机跑代码就吃过这个亏轨迹没调速机械臂像弹簧一样弹出去还好限位开关挡住了。从那以后真机测试一律先调到0.1。然后是setJointValueTarget的参数顺序这个顺序必须和机械臂的关节定义顺序一致不然就是“张冠李戴”机械臂会做出完全错误的动作。Panda机械臂是7个关节从底座到末端依次是panda_joint1到panda_joint7。不同品牌的机械臂关节顺序和零点位置不同用之前一定要看官方URDF文件确认。关节空间规划的优点是计算快、轨迹平滑因为算法只关心关节角度曲线只要在每个关节的加速度、速度约束范围内插值轨迹一定平滑。缺点就是我在开头提到的末端路径的形状不受控。你看代码里从头到尾没有提到末端位置所以机械臂从A点运动到B点时末端走的是一条什么曲线代码作者根本管不了。3.2 笛卡尔空间规划保证末端走直线接下来是笛卡尔空间规划的代码。这个就稍微复杂一点因为要涉及位姿的描述。在MoveIt C API里用computeCartesianPath接口传入末端期望经过的路径点规划器会返回一条尽量贴近这些点的轨迹。#include moveit/move_group_interface/move_group_interface.h #include moveit/planning_scene_interface/planning_scene_interface.h #include geometry_msgs/msg/pose.hpp int main(int argc, char** argv) { rclcpp::init(argc, argv); auto node std::make_sharedrclcpp::Node(cartesian_planner); auto logger rclcpp::get_logger(cartesian_planner); moveit::planning_interface::MoveGroupInterface move_group(node, panda_arm); move_group.setMaxVelocityScalingFactor(0.1); move_group.setMaxAccelerationScalingFactor(0.1); // 设置末端执行器的参考坐标系通常用工具坐标系 move_group.setPoseReferenceFrame(panda_link0); // 从当前位姿出发构造一系列路径点 geometry_msgs::msg::Pose current_pose move_group.getCurrentPose().pose; std::vectorgeometry_msgs::msg::Pose waypoints; // 终点在X方向平移0.1米姿态保持不变 geometry_msgs::msg::Pose target_pose current_pose; target_pose.position.x 0.1; waypoints.push_back(target_pose); // 再加一个点从终点再往上抬0.05米形成一段L型路径 geometry_msgs::msg::Pose lift_pose target_pose; lift_pose.position.z 0.05; waypoints.push_back(lift_pose); // 关键参数eef_step是末端步长单位米jump_threshold是跳跃阈值 double eef_step 0.01; double jump_threshold 0.0; moveit_msgs::msg::RobotTrajectory trajectory; double fraction move_group.computeCartesianPath(waypoints, eef_step, jump_threshold, trajectory); RCLCPP_INFO(logger, 路径规划完成比例: %.2f, fraction); if (fraction 0.9) { moveit::planning_interface::MoveGroupInterface::Plan my_plan; my_plan.trajectory_ trajectory; move_group.execute(my_plan); } rclcpp::shutdown(); return 0; }这里有几个参数必须讲清楚。eef_step是末端在笛卡尔空间采样时的步长单位是米。这个值越小路径点越密集轨迹越精细但规划耗时也越长。我自己的经验是一般取0.01米就够用了对于高精度涂胶之类的要求可以调到0.005但再小意义就不大了因为机械臂本身的重复定位精度就在0.01毫米到0.1毫米级别。jump_threshold是跳跃阈值用来检测相邻路径点之间关节角度变化是否过大。默认MoveIt的注释建议设成0.0表示禁用检查但实际开发中我建议设一个值比如0.5这样当笛卡尔路径经过奇异点附近导致关节角突变时规划器能及时中断避免生成暴力运动。设成0.0的话它会一直尝试生成轨迹哪怕某个关节要瞬间转90度也会硬生成那段轨迹执行起来非常危险。笛卡尔空间规划的返回值fraction非常关键它表示路径规划完成的比例。1.0说明整条路径规划成功0.8说明只规划到了80%最后20%因为IK解不出来或者碰撞等原因中断了。很多新手拿到这个返回值不检查直接把轨迹发出去执行结果机械臂只走了一半就停了还以为是代码bug。实际操作中分数低于0.9就别执行了先调整路径点或者机械臂起始构型再说。3.3 执行效果对比看轨迹曲线和实际运动写完了两种规划的代码我们看看执行效果有什么差异。我这里用RViz里的轨迹可视化工具来看Display里选择Trajectory就能看到规划的轨迹线。关节空间规划出来的轨迹末端走的是一条空间曲线。如果起始点和终点比较远这条曲线会绕一个大弯看起来很“飘逸”但是每个关节的运动都很平稳没有突变的转角。这个特性特别好用因为平稳意味着对机械臂的冲击小、能耗低所以只要任务对末端路径没有严格要求我基本都用关节空间规划。笛卡尔空间规划出来的轨迹末端走的是严格的直线段或者你指定的路径形状。这个特性在做涂胶、写字、码垛这类精确路径任务时是必须的。但代价是为了保证末端走直线各个关节的运动可能不平滑尤其是在路径接近奇异点或者经过工作空间边缘时某个关节可能要快速翻转冲击很大。实际执行的时候你还会发现一个关节空间规划没有的问题笛卡尔空间规划的执行时间更长。因为轨迹里包含了很多密集的路径点每个点都要执行控制周期也短。同样是移动末端100毫米关节空间规划可能1秒完成笛卡尔空间规划可能需要2到3秒。这很好理解走直线要求的约束多需要各个关节协调运动自然花的时间更多。4. 决策指南不同场景到底该选哪个4.1 按任务需求选的三个硬性标准我在实际项目里选择空间的标准总结下来就是三条跟纠结算法精不精没关系先按这三条去筛选就行。第一条末端路径形状是否是硬约束。如果任务要求末端必须走直线、走圆弧、走特定轮廓比如涂胶、焊接、切割、写字、打磨那直接选笛卡尔空间。反之如果只是从A点到B点搬运东西中间没有严格的路径形状要求选关节空间省事又高效。第二条是否有避障需求。如果你的工作环境里障碍物比较多需要在路径中间指定若干个途经点来绕障那你需要做的是在笛卡尔空间定义这些途经点然后分段用笛卡尔规划或者把途经点转换成关节空间约束让OMPL在关节空间搜索。纯笛卡尔空间规划遇到障碍物会很尴尬因为它只会沿着你给的路径点走不会自动避障路径上有碰撞就卡住了。纯关节空间规划虽然能避障但你很难精确控制末端绕障的路径。第三条是否需要轨迹可预测、可重复。做对接、装配这类任务末端姿态在接近目标位置时通常有严格要求。这时候即使关节空间能规划出一条好看的轨迹我也倾向于用笛卡尔空间在末端接近阶段做一段直线逼近保证姿态可控、路径可预测。装配件的结构固定你总不希望机械臂每次接近的路径都不一样。4.2 混合策略才是实战常态很多刚接触轨迹规划的朋友会陷入一个误区觉得一个任务从头到尾只能用一种规划方式。实际上我在项目里几乎从来不用单一规划方式都是混合着来。举一个码垛任务的例子。机械臂要从传送带上抓取箱子放到托盘上。这个任务可以拆成三段第一段从待机位到抓取点上方这一段空间开阔用关节空间规划快速移动节省节拍第二段从抓取点上方垂直下降到抓取点这一段末端路径必须严格垂直因为箱子在传送带上有固定位置稍微偏一点就抓不准所以用笛卡尔空间做直线下降第三段抓取箱子后抬升也是笛卡尔空间直线抬升保证箱子不会碰撞到周围物体第四段从抬升位置移动到托盘上方这一段空间相对开阔继续用关节空间第五段从托盘上方垂直下降到放置点继续用笛卡尔空间直线下降。你看一个完整的码垛任务关节空间和笛卡尔空间交替使用各自负责自己擅长的路段。这样做的好处很实际既保证了关键路段的路径精度又最大化地节省了非关键路段的运动时间。节拍就是产能省下来的每一秒都是钱。4.3 遇到奇异点和工作空间边界怎么办这里必须单拎出来讲因为这是我踩过最多坑的地方。奇异点是机械臂在某个特定构型下自由度退化、IK求解困难的点。最典型的例子是手腕中心与肩部中心重合时机械臂末端无法在这个方向上继续运动表现为某几个关节需要瞬间转到极限位置才能维持末端的运动。在笛卡尔空间规划中如果路径经过奇异点规划出来的轨迹在这些点附近关节速度会爆炸执行起来极其危险。我自己遇到过的真实案例是让机械臂画一个大圆在圆的某个角度附近机械臂突然发出很大的噪音运动也出现抖动。后来查了一下那个位置刚好靠近腕部奇异点。解决办法很简单要么调整任务路径让路径绕过奇异点要么改变机械臂起始构型让奇异点不在路径上要么把奇异点附近的路径切换成关节空间规划牺牲路径形状换取关节运动的平滑。工作空间边界的问题类似。机械臂的工作空间是有限的末端越靠近边界IK解越不稳定笛卡尔空间规划的失败率越高。如果规划失败的比例高检查一下是不是目标路径已经超出了机械臂的物理可达范围。实际项目里我通常会在规划前先做一个可达性检查用IK快速求解器验证路径点是否可达提前拦截无效请求。5. 常见问题与排查技巧实录5.1 规划的轨迹乱飞、走出一条离谱的曲线这个问题99%出在目标位姿设置上。最常见的是把四元数姿态给错了导致规划器在关节空间找到一个看起来“路径奇怪”但姿态正确的解。比如你期望末端竖直向下但给的四元数末端姿态其实是倒着的规划器为了满足这个姿态会让各个关节绕一大圈。排查办法很简单在RViz里把目标位姿的坐标系显示出来看看姿态对不对。同时要检查你给的位姿是在哪个坐标系下描述的。MoveIt默认使用panda_link0基座坐标系或者panda_hand末端坐标系不同坐标系下同一个数值天差地别。有一次同事跟我抱怨规划路径古怪我过去一看他把在末端坐标系下测的位姿数值直接当成基座坐标系下的数值传给规划器了不出问题才怪。5.2 computeCartesianPath返回的fraction不为1fraction不为1说明路径中间某一段规划失败了。排查顺序是这样的第一步检查路径点是否在机械臂工作空间内可以用RVIZ的Marker把路径点画出来看看肉眼可见第二步检查路径是否经过奇异点把失败点附近的关节角打印出来看有没有突变第三步检查路径是否和障碍物碰撞打开MoveIt的Collision Objects显示排除环境障碍物的影响第四步检查起始构型是否合理有些构型虽然IK有解但起始点的关节角离路径上的其他点太远导致中间步骤无解。最后还有一个屡试不爽的土办法把eef_step调大一点比如从0.01调到0.02。路径点越稀疏规划器越容易找到可行解。当然这会牺牲一些路径精度但至少能先让任务跑起来精度问题再慢慢优化。5.3 笛卡尔空间轨迹在真机上抖动、震动真机执行笛卡尔轨迹时抖动最常见的原因是控制频率跟不上。规划出来的轨迹点很多每个点的执行间隔很短如果你的驱动层控制频率不够高机械臂就会“跳着”走完轨迹看起来就像抖动。解决办法有三个方向第一调大数据eef_step减少路径点数量第二在驱动层对轨迹点做插值在每个控制周期内对相邻路径点做线性插值或者三次样条插值平滑运动第三检查驱动层的轨迹缓存看看是不是缓存不够导致路径点被丢弃。另外我之前也遇到过一种隐蔽的原因MoveIt规划出的关节轨迹用的是位置控制模式而你的电机驱动默认跑的是速度模式两者不匹配轨迹执行时就会出现不断过冲、回调的振荡。这种情况需要检查电机驱动器的控制模式设置确认和规划器的输出一致。5.4 常见问题速查表为了大家方便我把平时被问得最多的几个问题整理成了一张表遇到问题先对着表查一遍现象可能原因排查方向解决建议轨迹乱飞、走大弧线目标位姿坐标系设置错误检查目标位姿参考系确认是在基座坐标系还是末端坐标系下描述笛卡尔路径只运行了一部分路径规划fraction1打印fraction值检查调整路径点、eef_step或改变起始构型轨迹经过奇异点导致关节剧烈运动路径经过奇异区域检查关节角突变点路径绕开奇异点或切换关节空间规划真机执行时抖动明显控制频率不足或路径点过密检查驱动控制周期调大eef_step或在驱动层做轨迹插值末端姿态到达目标时偏差较大四元数差值或工具坐标系标定问题检查TCP标定重新标定工具坐标系确认四元数方向笛卡尔路径在起点就失败起始构型与路径不兼容检查起始关节角调整机械臂起始位姿写在最后的实操体会我在实际项目中用的最多的是关节空间规划因为大量搬运、分拣场景根本不需要精确控制末端路径。但做涂胶、焊接、装配这类精度要求高的任务笛卡尔空间又是绕不开的工具。我现在的工作习惯是先用仿真把路径跑一遍确认两种规划方式下的轨迹和行为都符合预期再上真机。上真机的第一件事永远是限定速度和加速度先把轨迹跑慢十倍看着没问题再逐步加回来。这行做久了会发现轨迹规划的核心难点往往不在算法本身而在于对机械臂物理特性的理解知道它的工作空间哪里是“坑”知道哪些构型是“雷区”知道路径精度要和电机性能匹配。把这些经验积累下来比死磕一套高级算法有用得多。最后再分享一个小技巧调试阶段把日志级别调到DEBUG让MoveIt把每个路径点的位姿、关节角、IK求解状态都打印出来配合RViz的可视化你会对自己编写的轨迹有非常直观的理解。这套“日志可视化”的组合陪我排查过无数个诡异问题比任何调试器都靠谱。