3、创建机器人模型:使用URDF/SDF描述机器人、添加差速驱动插件、添加激光雷达传感器

好,我们进入第三章。这一章,我们要亲手搭建一个能在Gazebo里跑起来的机器人模型。

说白了,就是给机器人画个骨架,装上轮子和眼睛。嗯,这里要注意,模型的质量直接决定了后续导航调试的体验。我在项目中见过太多人因为模型参数写错,折腾了一整天。

3.1 用URDF还是SDF?我的选择

ROS2里描述机器人,主要有两种格式:URDF和SDF。很多人纠结选哪个。

我个人习惯:在Nav2项目里用URDF。为什么?因为URDF和ROS2的生态结合得更紧密。你想想看,robot_state_publisher、joint_state_publisher这些工具,原生就支持URDF。SDF虽然功能更强,但配置起来稍显繁琐。

当然,如果你用Gazebo Classic,SDF是原生格式。但我们现在讲的是Gazebo Ignition(现在叫Gazebo Garden/Harmony),它俩都支持。我建议你从URDF入手,等熟练了再尝试SDF。

核心结论: 本课程统一使用URDF + xacro宏文件。xacro可以帮我们减少重复代码,让模型更清晰。

3.2 搭建机器人骨架

我们先画一个两轮差速机器人。结构很简单:一个底盘、两个驱动轮、一个万向轮,再加一个激光雷达支架。

来看代码。我习惯把每个部件拆成独立的xacro文件,最后用include组合起来。

<!-- robot_core.xacro -->
<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="my_nav2_robot">

  <!-- 底盘 -->
  <link name="base_footprint">
    <visual>
      <geometry>
        <box size="0.01 0.01 0.01"/>
      </geometry>
    </visual>
  </link>

  <joint name="base_joint" type="fixed">
    <parent link="base_footprint"/>
    <child link="base_link"/>
    <origin xyz="0 0 0.05"/>
  </joint>

  <link name="base_link">
    <visual>
      <geometry>
        <cylinder length="0.15" radius="0.20"/>
      </geometry>
      <material name="blue">
        <color rgba="0.2 0.2 0.8 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder length="0.15" radius="0.20"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="2.0"/>
      <inertia ixx="0.02" ixy="0.0" ixz="0.0"
               iyy="0.02" iyz="0.0"
               izz="0.03"/>
    </inertial>
  </link>

  <!-- 左轮 -->
  <link name="left_wheel_link">
    <visual>
      <geometry>
        <cylinder length="0.04" radius="0.05"/>
      </geometry>
      <material name="black"/>
    </visual>
    <collision>
      <geometry>
        <cylinder length="0.04" radius="0.05"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.2"/>
      <inertia ixx="0.0001" ixy="0.0" ixz="0.0"
               iyy="0.0001" iyz="0.0"
               izz="0.0002"/>
    </inertial>
  </link>

  <joint name="left_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="left_wheel_link"/>
    <origin xyz="0.0 0.18 -0.05" rpy="1.5708 0 0"/>
    <axis xyz="0 0 1"/>
  </joint>

  <!-- 右轮(对称) -->
  <link name="right_wheel_link">
    <!-- 内容与左轮类似,位置在 xyz="0.0 -0.18 -0.05" -->
  </link>

  <joint name="right_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="right_wheel_link"/>
    <origin xyz="0.0 -0.18 -0.05" rpy="1.5708 0 0"/>
    <axis xyz="0 0 1"/>
  </joint>

  <!-- 万向轮 -->
  <link name="caster_wheel_link">
    <visual>
      <geometry>
        <sphere radius="0.03"/>
      </geometry>
    </visual>
  </link>

  <joint name="caster_wheel_joint" type="fixed">
    <parent link="base_link"/>
    <child link="caster_wheel_link"/>
    <origin xyz="-0.15 0.0 -0.08"/>
  </joint>

</robot>

这段代码里,我特别强调一下惯性参数。很多人图省事随便填,结果仿真里机器人像纸片一样飘。我曾经踩过这个坑,仿真跑得好好的,一换真实机器人就抖得不行。后来发现是惯性矩阵算错了。

小技巧: 惯性参数可以用在线计算器算。圆柱体、长方体都有公式。别偷懒,这直接影响控制效果。

3.3 添加差速驱动插件

模型画好了,怎么让它动起来?我们需要给Gazebo一个驱动插件。差速驱动是最常见的底盘形式。

在URDF里添加插件,用<gazebo>标签。看下面这个例子:

<gazebo>
  <plugin name="diff_drive_controller" filename="libgazebo_ros_diff_drive.so">
    <ros>
      <namespace>/</namespace>
      <argument>diff_drive_controller</argument>
    </ros>
    <left_joint>left_wheel_joint</left_joint>
    <right_joint>right_wheel_joint</right_joint>
    <wheel_separation>0.36</wheel_separation>
    <wheel_diameter>0.10</wheel_diameter>
    <max_wheel_torque>20</max_wheel_torque>
    <max_wheel_acceleration>10.0</max_wheel_acceleration>
    <command_topic>cmd_vel</command_topic>
    <odometry_topic>odom</odometry_topic>
    <odometry_frame>odom</odometry_frame>
    <robot_base_frame>base_footprint</robot_base_frame>
    <publish_odom>true</publish_odom>
    <publish_odom_tf>true</publish_odom_tf>
    <update_rate>50</update_rate>
  </plugin>
</gazebo>

这里有几个参数要特别留意:

参数 说明 我的建议
wheel_separation 左右轮中心距 精确测量,误差超过1cm转弯半径就不对
wheel_diameter 轮子直径 和模型保持一致,别写错
max_wheel_torque 最大扭矩 设大一点没关系,仿真里不会烧电机
update_rate 里程计发布频率 50Hz够用,太高了CPU扛不住
注意: odom_frame和robot_base_frame必须和后续Nav2配置里的坐标系一致。我曾经因为这里写成了"base_link"而不是"base_footprint",导致整个导航系统坐标系错乱,排查了整整一个下午。

3.4 添加激光雷达传感器

导航需要感知环境。激光雷达是最常用的传感器。我们给机器人头顶装一个2D激光雷达。

先定义雷达的link和joint:

<link name="laser_link">
  <visual>
    <geometry>
      <cylinder length="0.05" radius="0.03"/>
    </geometry>
    <material name="red"/>
  </visual>
</link>

<joint name="laser_joint" type="fixed">
  <parent link="base_link"/>
  <child link="laser_link"/>
  <origin xyz="0.10 0.0 0.10"/>
</joint>

然后添加Gazebo的雷达插件:

<gazebo reference="laser_link">
  <sensor type="ray" name="laser_sensor">
    <pose>0 0 0 0 0 0</pose>
    <visualize>true</visualize>
    <update_rate>10</update_rate>
    <ray>
      <scan>
        <horizontal>
          <samples>360</samples>
          <resolution>1</resolution>
          <min_angle>-3.14159</min_angle>
          <max_angle>3.14159</max_angle>
        </horizontal>
      </scan>
      <range>
        <min>0.10</min>
        <max>12.0</max>
        <resolution>0.01</resolution>
      </range>
      <noise>
        <type>gaussian</type>
        <mean>0.0</mean>
        <stddev>0.01</stddev>
      </noise>
    </ray>
    <plugin name="laser_controller" filename="libgazebo_ros_ray_sensor.so">
      <ros>
        <namespace>/</namespace>
        <argument>scan</argument>
      </ros>
      <output_type>sensor_msgs/LaserScan</output_type>
      <frame_name>laser_link</frame_name>
    </plugin>
  </sensor>
</gazebo>

这里我加了噪声模型。真实雷达是有误差的,仿真里加一点高斯噪声,能让你的导航算法更鲁棒。我在项目里测试过,不加噪声的仿真跑得飞起,一上真车就撞墙。加了噪声后,算法在仿真里就能提前适应真实环境。

3.5 本章知识体系

下面这张图,帮你理清本章的核心逻辑:

第三章:创建机器人模型 URDF/SDF描述 差速驱动插件 激光雷达传感器 底盘 轮子 惯性参数 cmd_vel 里程计 坐标系 扫描参数 噪声模型 话题发布 最终产出:可在Gazebo中运动的完整机器人模型

3.6 验证模型

写完了怎么验证?我一般三步走:

  1. 检查URDF语法:check_urdf my_robot.urdf,看有没有报错
  2. 可视化查看:urdf_to_graphiz my_robot.urdf,生成结构图,确认父子关系正确
  3. 在Rviz里预览:启动robot_state_publisher,用Rviz看模型姿态
我的习惯: 每改一个参数,就在Rviz里看一眼。别等全部写完再调试,那样问题会堆在一起,很难排查。

嗯,这一章的内容就到这里。模型是导航的基础,花时间把它做扎实,后面会省很多事。下一章我们把它放进Gazebo里,看看能不能动起来。

可可精品资料库