4. 机器人建模与URDF:URDF语法详解、link与joint配置、在Gazebo中显示自定义机器人

好,咱们今天来聊聊机器人建模。说白了,就是给你的机器人画一张「骨架图」。在ROS2和Gazebo的世界里,这张骨架图就是URDF文件。我刚开始接触URDF时,觉得不就是个XML嘛,写起来应该不难。结果第一次在Gazebo里加载,机器人直接散架了——轮子飞了,身体塌了,像个喝醉的积木。嗯,从那以后,我再也不敢小看这个文件了。

核心认知:URDF不是代码,是描述。它告诉系统:你的机器人长什么样,关节在哪,能怎么动。

4.1 URDF语法——其实就两个核心标签

URDF的语法结构非常清晰。你想想看,一个机器人无非就是「身体部件」和「连接方式」。对应到URDF里,就是link和joint。

  • link:描述一个刚体部件。比如底盘、轮子、摄像头支架。
  • joint:描述两个link之间的连接关系。比如轮子怎么装在底盘上,能转还是不能转。

我个人习惯把link想象成「骨头」,joint想象成「关节」。骨头不会动,关节才能动。这个比喻很直观,你记一下。

4.1.1 link的配置

一个典型的link长这样:

<link name="base_link">
  <visual>
    <geometry>
      <box size="0.4 0.3 0.1"/>
    </geometry>
    <material name="blue">
      <color rgba="0 0 1 1"/>
    </material>
  </visual>
  <collision>
    <geometry>
      <box size="0.4 0.3 0.1"/>
    </geometry>
  </collision>
  <inertial>
    <mass value="2.0"/>
    <inertia ixx="0.01" ixy="0" ixz="0"
              iyy="0.01" iyz="0"
              izz="0.01"/>
  </inertial>
</link>

这里有三块内容:

  • visual:视觉显示,Gazebo里你看到的样子。说白了就是「好看」用的。
  • collision:碰撞检测,物理引擎用它算碰撞。我踩过坑:有一次visual和collision尺寸不一致,机器人看着没撞到,但物理引擎认为撞了,直接弹飞。所以,collision尽量和visual保持一致。
  • inertial:惯性参数,质量、转动惯量。Gazebo仿真物理效果全靠它。如果你不写,Gazebo会报warning,而且物理行为会非常诡异。

我的经验:inertial里的转动惯量ixx、iyy、izz,对于简单形状可以用公式估算。长方体就是 (1/12)*m*(h²+w²) 这种。别偷懒,否则机器人会像在冰面上走路。

4.1.2 joint的配置

joint是URDF的灵魂。没有joint,你的机器人就是一坨散件。来看一个轮子的joint:

<joint name="left_wheel_joint" type="continuous">
  <parent link="base_link"/>
  <child link="left_wheel_link"/>
  <origin xyz="0.2 -0.25 0" rpy="0 0 0"/>
  <axis xyz="0 1 0"/>
</joint>

关键点:

  • type:关节类型。continuous是连续旋转(轮子),revolute是有限角度旋转(关节臂),fixed是固定连接(摄像头支架)。
  • parent/child:父子关系。父link是底盘,子link是轮子。这个关系决定了运动学树的结构。
  • origin:子link相对于父link的位置和姿态。xyz是平移,rpy是旋转(roll, pitch, yaw)。
  • axis:旋转轴。对于轮子,绕Y轴转就是向前滚动。

注意:我曾经犯过一个低级错误——把parent和child写反了。结果轮子成了底盘的父亲,整个运动学树乱套,Gazebo直接崩溃。记住:父link是靠近机器人基座的,子link是末端。

4.2 从零搭建一个自定义机器人

好,理论说完了,咱们动手。我带你搭一个两轮差速底盘机器人。结构很简单:一个底盘 + 两个轮子 + 一个万向轮。

先画个图,理清思路:

两轮差速机器人URDF结构图 base_link 底盘(父link) left_wheel right_wheel caster_wheel left_wheel_joint type: continuous right_wheel_joint type: continuous caster_wheel_joint type: fixed link joint

这个图很清晰吧?base_link是根节点,两个驱动轮通过continuous joint连接,万向轮通过fixed joint连接。这就是一个完整的运动学树。

4.2.1 完整URDF代码

把上面的结构写成URDF,大概长这样:

<?xml version="1.0"?>
<robot name="my_robot">
  
  <!-- 底盘 -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="0.4 0.3 0.1"/>
      </geometry>
      <material name="blue"/>
    </visual>
    <collision>
      <geometry>
        <box size="0.4 0.3 0.1"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="2.0"/>
      <inertia ixx="0.01" ixy="0" ixz="0"
                iyy="0.01" iyz="0"
                izz="0.01"/>
    </inertial>
  </link>
  
  <!-- 左轮 -->
  <link name="left_wheel_link">
    <visual>
      <geometry>
        <cylinder radius="0.05" length="0.04"/>
      </geometry>
      <material name="black"/>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.05" length="0.04"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.2"/>
      <inertia ixx="0.0001" ixy="0" ixz="0"
                iyy="0.0001" iyz="0"
                izz="0.0001"/>
    </inertial>
  </link>
  
  <!-- 左轮关节 -->
  <joint name="left_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="left_wheel_link"/>
    <origin xyz="-0.15 -0.2 0" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
  </joint>
  
  <!-- 右轮 -->
  <link name="right_wheel_link">
    <visual>
      <geometry>
        <cylinder radius="0.05" length="0.04"/>
      </geometry>
      <material name="black"/>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.05" length="0.04"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.2"/>
      <inertia ixx="0.0001" ixy="0" ixz="0"
                iyy="0.0001" iyz="0"
                izz="0.0001"/>
    </inertial>
  </link>
  
  <!-- 右轮关节 -->
  <joint name="right_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="right_wheel_link"/>
    <origin xyz="-0.15 0.2 0" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
  </joint>
  
  <!-- 万向轮 -->
  <link name="caster_wheel_link">
    <visual>
      <geometry>
        <sphere radius="0.03"/>
      </geometry>
      <material name="gray"/>
    </visual>
    <collision>
      <geometry>
        <sphere radius="0.03"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.1"/>
      <inertia ixx="0.00001" ixy="0" ixz="0"
                iyy="0.00001" iyz="0"
                izz="0.00001"/>
    </inertial>
  </link>
  
  <!-- 万向轮关节(固定) -->
  <joint name="caster_wheel_joint" type="fixed">
    <parent link="base_link"/>
    <child link="caster_wheel_link"/>
    <origin xyz="0.15 0 -0.05" rpy="0 0 0"/>
  </joint>
  
  <!-- 颜色定义 -->
  <material name="blue">
    <color rgba="0.2 0.4 0.8 1.0"/>
  </material>
  <material name="black">
    <color rgba="0.1 0.1 0.1 1.0"/>
  </material>
  <material name="gray">
    <color rgba="0.5 0.5 0.5 1.0"/>
  </material>
  
</robot>

避坑指南:我曾经把万向轮的joint类型写成continuous,结果Gazebo里万向轮疯狂自转,机器人原地打转。万向轮应该是fixed,它只是被动滚动,不需要主动驱动。

4.3 在Gazebo中显示自定义机器人

URDF写好了,怎么让Gazebo看到它?这里有个关键步骤:你需要一个launch文件来加载URDF并启动Gazebo。

我个人习惯用robot_state_publisher来发布机器人状态,然后用spawn_entity.py把机器人放到Gazebo里。来看一个典型的launch文件:

import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node

def generate_launch_description():
    # URDF文件路径
    urdf_path = os.path.join(
        get_package_share_directory('my_robot_description'),
        'urdf',
        'my_robot.urdf'
    )
    
    # 读取URDF内容
    with open(urdf_path, 'r') as f:
        robot_desc = f.read()
    
    return LaunchDescription([
        # 启动Gazebo
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource([
                os.path.join(
                    get_package_share_directory('gazebo_ros'),
                    'launch',
                    'gazebo.launch.py'
                )
            ]),
        ),
        
        # 发布机器人状态
        Node(
            package='robot_state_publisher',
            executable='robot_state_publisher',
            output='screen',
            parameters=[{'robot_description': robot_desc}]
        ),
        
        # 将机器人放入Gazebo
        Node(
            package='gazebo_ros',
            executable='spawn_entity.py',
            arguments=['-topic', 'robot_description', '-entity', 'my_robot'],
            output='screen'
        )
    ])

运行这个launch文件,Gazebo就会启动,你的机器人就会出现在仿真世界里。如果一切顺利,你会看到:

  • 一个蓝色底盘
  • 两个黑色轮子
  • 一个灰色万向轮
  • 机器人稳稳地站在地面上

常见问题:如果Gazebo里机器人直接掉到地面以下,或者悬浮在半空,检查两个地方:

  1. 万向轮的origin坐标是否正确?它应该接触地面。
  2. 碰撞模型是否和视觉模型一致?不一致会导致物理引擎计算错误。

我遇到过最离谱的一次——机器人直接穿模到地心里,后来发现是collision的尺寸写成了0。嗯,0尺寸的碰撞体,物理引擎直接忽略它。

4.4 本章小结

URDF是机器人建模的基石。说白了,它就是一张「零件清单 + 装配图」。你只要把link和joint配置好,Gazebo就能帮你把机器人「组装」出来。

我个人觉得,URDF最难的不是语法,而是空间想象。你要在脑子里把xyz坐标、rpy旋转想清楚。刚开始可以画个草图,就像我上面那张SVG图一样。等熟练了,直接写代码也不怕。

下一章,我们会给这个机器人加上传感器,让它能「看」到世界。但那是后话了,先把今天的URDF练熟。

核心要点回顾:

  • link = 骨头(刚体部件),joint = 关节(连接关系)
  • 每个link必须有visual、collision、inertial三部分
  • joint的parent/child关系决定运动学树结构
  • Gazebo显示需要robot_state_publisher + spawn_entity

可可精品资料库