ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从URDF到SDF:并联机构闭环建模与Gazebo仿真实践

从URDF到SDF:并联机构闭环建模与Gazebo仿真实践 先说一个我在机器人社区里被问过很多次的问题一套并联夹爪或者一个Stewart平台用SolidWorks导出URDF之后扔进Gazebo加载时报错日志里写着某个link被两个joint同时当作child引用。大部分人的第一反应是检查导出设置、检查版本兼容折腾半天发现根本不是操作问题——URDF这个格式从根上就不允许表达“闭环”。URDF描述的是树形串联结构每个非根link有且只能有一个父关节而并联结构的输出件天生被多条支链同时牵着这在URDF的XML语义里就是非法数据。如果你的仿真环境支持SDF格式这个问题有一个标准解法用SDF来表达并联结构让一个link被多个joint引用形成真正的闭环约束。这篇文章就围绕“为什么URDF不行、SDF为什么可以、具体怎么写、跑起来之后还有哪些坑”这条链路展开适合正在做并联机械臂、并联夹爪、Delta机器人或者Stewart平台仿真的朋友参考。1. URDF的树形约束究竟卡在哪里1.1 一个link只能有一个父亲先看URDF最基本的模型结构。一个URDF文件由link和joint组成joint通过parent和child把两个link连起来。整张结构图是一棵有根树每个link只能有一个父关节根link没有父关节。这个约束不是某一个解析器的偏好而是URDF XML Schema层面的硬性规定。用一段最简单的串联机械臂代码来说明robot namesimple_arm link namebase/ link namelink1/ link namelink2/ joint namej1 typerevolute parent linkbase/ child linklink1/ /joint joint namej2 typerevolute parent linklink1/ child linklink2/ /joint /robot这个结构里从base出发经过j1到link1再经过j2到link2是一条标准的串联链。现在假设我做一个并联机构输出件platform同时被两条支链驱动joint nameleg1_joint typerevolute parent linkleg1_base/ child linkplatform/ /joint joint nameleg2_joint typerevolute parent linkleg2_base/ child linkplatform/ /joint第二段代码里platform已经不是第一次作为child出现。URDF解析器在执行check_urdf或者xacro加载时会直接报错大意是“link platform已经有一个父关节不能再次作为其他joint的child”。很多人把这个当作导出插件的问题其实错怪了插件——它只是把这个限制暴露了出来。1.2 并联结构在语法上的本质是什么从图论的角度看并联机构和串联机构的差别非常清晰。一个串联机械臂的关节-连杆图是一棵树任意两个link之间只有一条路径。而一个并联机构比如最常见的五杆机构、Delta并联机器人和Stewart平台本质上是多条开链共享同一个末端执行器。它的结构图是若干条支链在末端汇合的形态输出link的入度大于1也就是“同时有多个父亲”。这个差异看起来只是几个joint的写法问题实际影响深远。URDF的树形假设是整个ROS生态的底层前提TF管理器要靠这棵树来广播坐标系变换MoveIt的机器人运动学模型要基于这棵树构建ros_control的关节状态发布也要依赖这棵树的拓扑。一旦树变成图这些组件集体失效。这也是为什么你在网上几乎搜不到“并联机械臂URDF”现成文件的原因——能找到的要么是打断闭环之后的伪并联结构要么是直接把模型写成了SDF格式。2. 为什么并联结构的落点在SDF2.1 SDF是图不是树SDF和URDF最核心的区别在于结构表达能力。SDF里的joint同样有parent和child但SDF的规范并没有限制“一个link只能被一个joint当作child”。一个link可以同时被多个joint引用为child这意味着两套甚至多套运动链可以汇合到同一个输出件上。Gazebo拿到这些joint约束之后通过底层的约束求解器ODE、Bullet、DART、Simbody等把闭环方程一起解出来而不是像URDF那样只维护一棵树。SDF官方仓库里专门有一个测试模型叫closed_kinematic_chain.sdf用来验证闭环机构的支持情况。这个文件的存在本身就说明闭环不是SDF的边角功能而是被官方认可的建模方式。URDF和SDF在这个问题上的对比如下表能力URDFSDF结构表达树有向图允许闭环一个link多个父关节非法合法闭环物理求解不涉及约束求解器直接支持TF/MoveIt直接使用可以不行需要转回树模型惯量/碰撞/材质描述有限完整Pose坐标系语义关节origin逐级累乘显式Pose frame relative_to嵌套模型不支持支持这个表里最关键的一行就是“一个link多个父关节”。并联结构的所有可能性都建立在这一行上。你不需要再用“打断一条支链”这种自欺欺人的方式去建模也不需要在运动学上做大量近似SDF允许你把真实的拓扑写出来让物理引擎去处理闭环约束。2.2 标题里“仅适用于支持SDF格式的仿真环境”到底是什么意思这句话是在提醒选型。既然并联结构的最终表达载体是SDF那你的仿真目标环境必须能直接解析SDF。目前主流的环境里Gazebo Classic 11、GZ Sim也就是原来的IgnitionFortress/Garden/Harmonic等版本都是原生支持SDF的CoppeliaSim支持通过File-Import-SDF把模型导入导入后它会转成自己的场景结构如果你用的是某些只解析URDF的内部工具链那这个方案就不适用只能退而求其次用mimic关节做近似或者干脆只用正向运动学手算。这里有一个容易被忽略的点ROS生态里要求URDF的地方MoveIt、RViz、robot_state_publisher并不受影响它们不需要SDF。你完全可以让规划侧继续用URDF树模型让仿真侧用SDF图模型两个模型并存。这个“双模型策略”在第四节详细展开。但前提是你的仿真环境得认SDF否则这个策略的第一步就走不出去。3. 最小闭环示例平面五杆机构的SDF写法与验证3.1 结构设计为什么用五杆机构做演示五杆机构是平面并联机构里最简单的形态两个电机分别驱动两根曲柄两根曲柄再通过两根连杆汇合到同一个输出点。整个机构只有6个活动构件、6个转动副结构足够小一屏之内可以把完整SDF看完但闭环的关键结构全都具备。这个模型就是SDF里“同一个link被两个joint引用”的最小教学案例。先说明一下各link的初始位置和姿态。为了在t0时刻满足闭环几何我算了一组对称的数值两个电机轴分别位于模型坐标系的(0, 0)和(1.0, 0)曲柄长度0.5m初始仰角分别为60度和120度连杆长度0.5m输出点TCP在(0.5, 0.866)各link的pose如下link名称模型坐标系下的posebase_link0 0 0 0 0 0crank10.125 0.2165 0 0 0 1.0472crank20.875 0.2165 0 0 0 2.0944coupler10.375 0.6495 0 0 0 1.0472coupler20.625 0.6495 0 0 0 2.0944tcp0.5 0.866 0 0 0 0所有转动副的轴线都沿Z轴所以这是一个纯平面机构。下面给出结构骨架SDFvisual和inertial我做了简化实际项目中替换成从CAD导出或手填的真实参数即可?xml version1.0 ? sdf version1.7 model nameplanar_five_bar !-- 演示模型关掉重力只验证运动学拓扑真实项目请把base_link用固定关节连到世界 -- gravity0/gravity link namebase_link pose0 0 0 0 0 0/pose inertial mass1000/mass inertia ixx10/ixxiyy10/iyyizz10/izz /inertia /inertial visual namevis_base geometryboxsize1.2 0.1 0.02/size/box/geometry /visual collision namecol_base geometryboxsize1.2 0.1 0.02/size/box/geometry /collision /link link namecrank1 pose0.125 0.2165 0 0 0 1.0472/pose inertial mass0.2/mass inertia ixx0.001/ixxiyy0.001/iyyizz0.004/izz /inertia /inertial visual namevis_crank1 geometryboxsize0.5 0.04 0.04/size/box/geometry /visual collision namecol_crank1 geometryboxsize0.5 0.04 0.04/size/box/geometry /collision /link link namecrank2 pose0.875 0.2165 0 0 0 2.0944/pose inertial mass0.2/mass inertia ixx0.001/ixxiyy0.001/iyyizz0.004/izz /inertia /inertial visual namevis_crank2 geometryboxsize0.5 0.04 0.04/size/box/geometry /visual collision namecol_crank2 geometryboxsize0.5 0.04 0.04/size/box/geometry /collision /link link namecoupler1 pose0.375 0.6495 0 0 0 1.0472/pose inertial mass0.2/mass inertia ixx0.001/ixxiyy0.001/iyyizz0.004/izz /inertia /inertial visual namevis_coupler1 geometryboxsize0.5 0.03 0.03/size/box/geometry /visual collision namecol_coupler1 geometryboxsize0.5 0.03 0.03/size/box/geometry /collision /link link namecoupler2 pose0.625 0.6495 0 0 0 2.0944/pose inertial mass0.2/mass inertia ixx0.001/ixxiyy0.001/iyyizz0.004/izz /inertia /inertial visual namevis_coupler2 geometryboxsize0.5 0.03 0.03/size/box/geometry /visual collision namecol_coupler2 geometryboxsize0.5 0.03 0.03/size/box/geometry /collision /link link nametcp pose0.5 0.866 0 0 0 0/pose inertial mass0.1/mass inertia ixx0.0001/ixxiyy0.0001/iyyizz0.0001/izz /inertia /inertial visual namevis_tcp geometryboxsize0.05 0.05 0.05/size/box/geometry /visual collision namecol_tcp geometryboxsize0.05 0.05 0.05/size/box/geometry /collision /link joint namebase_to_crank1 typerevolute parentbase_link/parent childcrank1/child pose0 0 0 0 0 0/pose axis xyz0 0 1/xyz limit lower-3.14/lowerupper3.14/upper effort100/effortvelocity10/velocity /limit /axis /joint joint namebase_to_crank2 typerevolute parentbase_link/parent childcrank2/child pose1.0 0 0 0 0 0/pose axis xyz0 0 1/xyz limit lower-3.14/lowerupper3.14/upper effort100/effortvelocity10/velocity /limit /axis /joint joint namecrank1_to_coupler1 typerevolute parentcrank1/parent childcoupler1/child pose0.25 0 0 0 0 0/pose axis xyz0 0 1/xyz limit lower-3.14/lowerupper3.14/upper effort1/effortvelocity10/velocity /limit /axis /joint joint namecrank2_to_coupler2 typerevolute parentcrank2/parent childcoupler2/child pose0.25 0 0 0 0 0/pose axis xyz0 0 1/xyz limit lower-3.14/lowerupper3.14/upper effort1/effortvelocity10/velocity /limit /axis /joint !-- 关键闭环coupler1和coupler2都连接到tcp -- joint namecoupler1_to_tcp typerevolute parentcoupler1/parent childtcp/child pose0.25 0 0 0 0 0/pose axis xyz0 0 1/xyz limit lower-3.14/lowerupper3.14/upper effort1/effortvelocity10/velocity /limit /axis /joint joint namecoupler2_to_tcp typerevolute parentcoupler2/parent childtcp/child pose0.25 0 0 0 0 0/pose axis xyz0 0 1/xyz limit lower-3.14/lowerupper3.14/upper effort1/effortvelocity10/velocity /limit /axis /joint /model /sdf注意最后两个jointcoupler1_to_tcp和coupler2_to_tcp它们的child都是tcp。这就是整个闭环的入口也是URDF无论如何写不出来的那一行。这两个关节的pose都是0.25 0 0 0 0 0指的是各自父连杆末端在父连杆局部坐标系下的位置换算到模型坐标系正好都落在(0.5, 0.866)这个TCP点上和tcp link的初始pose重合所以初始时刻闭环是严格闭合的。3.2 在Gazebo里加载并验证闭环是否生效把上面的模型保存为five_bar.sdf然后写一个世界文件加载它?xml version1.0 ? sdf version1.7 world namedefault include urimodel://planar_five_bar/uri /include /world /sdf也可以直接把model内容粘贴进world里。Gazebo Classic 11下运行gazebo five_bar.worldGZ SimIgnition下运行gz sim five_bar.world加载成功后用关节状态话题或者Gazebo的GUI检查6个关节的状态。一个很有效的验证方法是把其中一个电机关节用limit锁死在某个角度然后驱动另一个电机观察TCP的位置轨迹。五杆机构在锁死一个电机后变成一个标准的四杆机构TCP应该画出平滑的连杆曲线而不是乱飞。如果TCP飞出、连杆脱开、或者模型原地抖动说明闭环约束没有生效问题大概率出在求解器配置上第六节细说。3.3 从五杆扩展到Delta和Stewart五杆跑通之后扩展到真正的空间并联机器人就是加支链、改关节类型的问题。Delta机器人有三条支链每条支链由一个主动臂和一个从动杆组成从动杆两端通常用球铰、虎克铰或者“虎克铰转动副”的组合连接。Stewart平台有六条支链每条支链两端分别连基座和动平台输出平台天然是6个joint的公共child。在SDF里这些结构都只是把五杆的代码复制几遍、把tcp换成平台、把关节类型从revolute换成universal或ball而已。平台这个link会被三条或六条支链同时引用这在URDF里是错误在SDF里就是标准操作。不同的并联机构建议的关节类型组合如下机构类型支链关节组合说明平面五杆revolute revolute最简单的闭环验证Delta机器人revolute主动臂 universal revolute沿杆轴线避免直接用两个球铰Stewart平台universal基座端 spherical平台端经典6-6构型平行四边形机构四根杆组成两个闭环每个角点都是一个多父节点link关节类型的选择直接关系到数值稳定性这个坑在第六节里展开。4. 从URDF迁到SDF的桥接路线与双模型策略4.1 让Gazebo先把URDF翻译成SDF现实项目里很少有人从零手写SDF大部分情况是你已经从SolidWorks或者Fusion 360导出了一套URDF。并联机构导出的URDF虽然在Gazebo里加载会报错但你仍然可以利用Gazebo自身的转换能力拿到一个可供修改的SDF底子。操作路线是这样的先用xacro把参数化模型渲染成URDFxacro model.xacro model.urdf确保纯URDF没有宏残留。在Gazebo Classic里通过spawn_model节点加载这个URDF注意加载时Gazebo内部会做一次URDF到SDF的转换转换失败的错误信息里会指明是哪个link出了问题。如果只是少数几个link存在多父关节冲突最直接的办法是在导出前先把闭环打断——选一条支链把它和输出件之间的关节删掉让模型变成一个树。用Gazebo GUI选中模型右键选择保存为SDF拿到Gazebo转换后的中间产物。手工编辑这个SDF把刚才删掉的闭环关节补回来也就是给输出link在SDF里增加一个额外的joint引用。用gz sdf -p model.sdf做一次预处理检查include、变量和格式是否有残留问题再加载进仿真环境。这里有个非常实用的排查技巧URDF里那些“悬空”的link——也就是没有任何joint连接到它身上的link——往往就是导出插件处理并联结构时生成的废件。它们在URDF里不属于任何树分支加载后会在Gazebo里直接从初始位置坠落。把这些悬空link找出来反推它们的父关节应该接到哪里你就知道要在SDF里补哪几个闭环关节了。4.2 双模型策略规划用树仿真用图SDF解决了物理仿真问题但MoveIt、RViz、robot_state_publisher这些ROS组件还等着树形URDF。正确的架构不是二选一而是两套模型同时维护规划侧用一个“生成树”URDF这个树是闭环结构的生成树包含所有主动关节和主运动链仿真侧用一个完整的SDF图模型包含全部闭环关节交给物理引擎求解。两个模型的link名称、joint名称、初始位姿必须保持一致否则控制器拿到的关节状态和TF对不上。具体到五杆机构树模型取base - crank1 - coupler1 - tcp这条主链crank2和coupler2作为另一条独立分支挂在base下面coupler2到tcp的闭环关节在树模型里不出现。这样TF不会成环MoveIt可以规划而Gazebo用完整SDF做物理闭环。需要接受一个视觉上的妥协在RViz里树的第二条支链的末端是悬空的它不会和tcp连在一起。做可视化的时候可以把这条支链隐藏或者接受这个显示瑕疵。规划层面反正只关心主动关节被动支链的运动学不需要参与规划。4.3 SolidWorks等CAD工具导出的现实问题SolidWorks的URDF导出插件sw_urdf_exporter是为串联机构设计的这一点在它的源码和文档里写得很清楚。它对并联机构的处理方式比较粗暴要么导出失败要么把闭环打断之后生成一堆悬空link。用下来最常见的问题是惯性参数单位、坐标系对齐、以及上面说的悬空link。处理CAD导出的并联模型我的建议是导出时选择一条主支链作为树的主干其余支链的末端关节先删掉导出后不要直接使用先做一遍“找悬空link”的检查从CAD里拿到装配体各关节的真实位置和轴线方向在SDF里手工补完整闭环碰撞几何一定要简化。SolidWorks导出的大三角网格STL放进物理引擎会让约束求解器计算量爆炸用凸包或基本图元替代复杂碰撞体。顺带回答一个经常搜到的问题Simulink怎么生成SDF文件。Simscape Multibody的Link插件主要导出URDF没有官方的一键SDF导出按钮。如果你在Simulink里建好了一个并联机构模型想拿到Gazebo仿真通常的路线是导出URDF之后再走4.1节的转换流程。Simscape本身对闭环的支持很自然因为它用的是物理建模语言不是树形运动链所以很多并联机构设计验证直接在Simulink里做反而更省事。5. 闭环模型的ROS侧难点TF、joint_states和mimic5.1 TF不允许成环即便你在URDF里强行写出了闭环结构有的工具链绕过检查能生成robot_state_publisher在广播TF时也会直接报错。TF的内部实现是一个严格的分层树从根坐标系开始逐级展开它的查找算法根本不接受环路。实际情况中你会看到类似“Transform cycle detected”或者递归超限的错误。这就是为什么要维持双模型物理引擎可以处理环TF不能处理环。给robot_state_publisher喂的永远是树模型给Gazebo喂的永远是完整SDF。两个模型的关节名保持一致TF树和物理模型之间通过同名关节的测量值对接。5.2 mimic并联的“低配替代”很多人在URDF里遇到并联结构时第一个搜到的方案是mimic。这个标签的语义很直观让一个被动关节去复制另一个主动关节的运动。典型用法joint namefinger2_joint typerevolute parent linkfinger2_base/ child linkfinger2/ mimic jointfinger1_joint multiplier-1/ /joint这段代码让finger2_joint跟随finger1_joint反向运动模拟一对手指夹爪的同步开合。优点是URDF和TF都合法可视化效果好适合夹爪、联动手腕这类“伪并联”机构。但mimic的物理本质是控制不是约束。在GZ Sim里mimic关节本质上是让被模仿关节用一个控制器去追踪目标角度它不是约束求解器解出来的硬约束因此不产生真实的约束反力也无法模拟机械卡死、受力分配和过约束情况。对于Delta、Stewart这种真正需要支链间载荷协调的机构mimic完全不够用。我见过有人用mimic做Stewart平台的近似平台一动就出现明显的漂移和部件互穿最后老老实实改回SDF。所以mimic的定位是“低配替代”适合运动学展示不适合精度仿真。5.3 joint_states和controller的过滤闭环SDF加载后Gazebo发布的/joint_states里会同时出现6个甚至更多关节包括那些被动关节。robot_state_publisher只处理URDF里声明过的关节所以多出来的闭环关节会被自动忽略不会引起TF问题。需要留意的是控制器。如果你用的是ros2_control或者gazebo_ros2_control控制器的joint列表里只应该写主动关节。把被动关节写进去硬件接口层会尝试对它们下发力矩指令而这些关节并没有真正的执行器轻则报错重则控制器状态机卡死。更合理的做法是只配置主动关节被动关节完全留给物理引擎处理。如果某个下游节点需要读取被动关节的角度从/joint_states里按名称过滤即可不需要额外写过滤节点——除非你订阅的是经过robot_state_publisher重映射之后的话题那种情况下才需要考虑把闭环关节从发布的URDF里摘除。6. 把闭环仿真跑稳求解器、步长与关节类型的选择6.1 闭环最容易出现的三种“灵异现象”闭环结构写进SDF只是第一步真正折磨人的是让它在物理引擎里稳定跑起来。我在实际项目里遇到的、以及帮别人排查过的问题绝大多数可以归为三类抖动模型静止状态下关节和link像果冻一样高频颤动最常见原因是约束求解器迭代次数不足或者仿真步长太大。漂移机构整体缓慢地“塌”下去或者闭环节点渐进分离通常和约束误差参数cfm/erp设置不当有关。穿透两个本不该相交的link互相穿过往往是被动关节的初始位形不满足闭环几何或者求解器直接放弃了某个约束。排查顺序有讲究。先检查初始位形是否满足闭环几何——很多抖动问题的根源是模型一开始就处于“悖论状态”求解器每帧都在努力把不可能的位置拉回来。然后看步长最后调求解器参数。我常用的一组调试起点参数如下参数Gazebo里的位置起始值出现抖动时的处理max_step_sizephysics ode0.001降到0.0005itersphysics ode solver50提高到100到200cfmphysics ode constraints1e-4降到1e-6穿透严重则升到1e-3erpphysics ode constraints0.2越大约束越硬越小越软solver_typephysics ode solverquick精度优先换world但明显变慢这些数值只是出发点不是标准答案。不同机构和不同求解器对参数的敏感程度差别很大最好的做法是一次只改一个参数观察现象再决定下一步。6.2 关节类型的选择比想象的更重要很多人在SDF里写Delta机器人时图省事把所有被动关节都写成joint typeball也就是球铰。球铰在Spring和物理引擎里的实现往往只约束位置不约束角向自由度这在闭环里容易引发两个问题一是绕杆轴线的自旋自由度没有被合理约束二是数值噪声通过角向自由度反复放大。用球铰写的闭环跑起来经常出现莫名其妙的抖动和漂移。更稳妥的组合是杆的一端用universal虎克铰另外用一个小型revolute沿着杆的轴线方向释放自旋自由度平台端再用一个单独的球铰或者直接固定。总之原则是“用约束最少但完整”的关节组合能不用球铰就不用球铰。另外给被动关节加一点阻尼非常有效dynamics damping0.05/damping friction0.01/friction /dynamics这个阻尼不是为了模拟真实摩擦力而是为了吸收约束求解器在每个仿真步里产生的数值噪声。数值噪声在闭环里会因为约束耦合被反复放大一点点阻尼就能让系统稳定下来。6.3 一次实际调试经历说一个我自己的案例。之前做三支链并联云台三条支链分别通过两个球铰连接动平台Gazebo里平台每隔几秒就会突然“跳”一下关节状态话题里某个被动关节的速度瞬间冲到极大值。一开始以为是步长问题把max_step_size从0.001降到0.0005抖动减轻了但还在跳。然后把ODE求解器迭代次数从50提到200跳变消失。后来又发现一个球铰在装配时轴线方向和CAD里的实际铰链轴线不一致把它的绕杆自旋自由度用revolute替代后仿真速度直接翻倍。那次调试我得到的经验是先关重力验证拓扑再开重力调参数。关掉重力跑一遍确认所有关节运动学上自洽、闭环不散架再打开重力处理动力学问题。这两件事混在一起排查你会分不清当前的抖动到底是拓扑错误还是求解器参数问题。最后再分享一个小实践并联结构在SDF里其实不神秘核心就一句话允许一个link被多条支链挂住。但这只是第一步真正的工作量在物理引擎的调试和ROS侧的树模型维护上。我现在的做法是把SDF图模型和树形URDF放在同一个Git仓库写一个脚本从同一份参数文件生成两份模型——树模型用于MoveIt规划和TFSDF模型用于Gazebo物理仿真闭环关节只在SDF里补齐。这样两套模型永远不会因为手工修改而漂移改一个尺寸参数两边同步更新。如果你的项目要长期维护建议一开始就把这条自动化的路铺好比每次手工同步省心得多。
返回列表