ROS移动机器人仿真实战:从模块化Xacro到Gazebo多传感器集成

如果你刚开始接触ROS机器人仿真,可能会被一堆文件格式和配置搞得晕头转向。URDF、Xacro、Gazebo、RViz……这些名词听起来就让人头疼。但说实话,一旦你掌握了模块化设计的思路,构建一个带摄像头和雷达的移动机器人仿真模型,其实比想象中要简单得多。

我在带学生做机器人竞赛项目时发现,很多团队在仿真阶段就卡住了,不是因为技术有多难,而是因为没有找到正确的组织代码的方式。他们要么把所有东西塞进一个巨大的URDF文件里,改个轮子尺寸都得翻半天;要么就是传感器坐标对不上,在Gazebo里看到的和实际物理布局完全两码事。

这篇文章我想分享的,正是如何用Xacro的模块化设计思路,快速搭建一个可复用的移动机器人仿真平台。我们会从最基础的底盘开始,逐步添加驱动轮、摄像头和激光雷达,最后在Gazebo中让这个机器人真正“活”起来。更重要的是,我会重点讲解多传感器坐标对齐的那些坑——这些经验都是我在实际项目中踩出来的。

1. 理解Xacro模块化设计的核心优势

很多人把Xacro简单地看作“带变量的URDF”,这其实低估了它的价值。Xacro真正的威力在于模块化参数化,这让你能像搭积木一样构建机器人模型。

1.1 为什么不用纯URDF?

先看一个典型的纯URDF问题。假设你要定义两个完全相同的轮子:

<!-- 左轮 -->
<link name="left_wheel">
  <visual>
    <geometry>
      <cylinder radius="0.0325" length="0.015"/>
    </geometry>
    <origin xyz="0 0 0" rpy="1.5708 0 0"/>
  </visual>
  <collision>
    <geometry>
      <cylinder radius="0.0325" length="0.015"/>
    </geometry>
    <origin xyz="0 0 0" rpy="1.5708 0 0"/>
  </collision>
</link>

<!-- 右轮 -->
<link name="right_wheel">
  <visual>
    <geometry>
      <cylinder radius="0.0325" length="0.015"/>
    </geometry>
    <origin xyz="0 0 0" rpy="1.5708 0 0"/>
  </visual>
  <collision>
    <geometry>
      <cylinder radius="0.0325" length="0.015"/>
    </geometry>
    <origin xyz="0 0 0" rpy="1.5708 0 0"/>
  </collision>
</link>

发现问题了吗?完全重复的代码。如果我想把轮子半径从0.0325改成0.035,需要改两个地方。这还只是两个轮子,如果是六足机器人的18个关节呢?

1.2 Xacro的解决方案:宏定义

Xacro通过宏(macro)来解决这个问题:

<xacro:macro name="add_wheel" params="name flag">
  <link name="${name}_wheel">
    <visual>
      <geometry>
        <cylinder radius="${wheel_radius}" length="${wheel_length}"/>
      </geometry>
      <origin xyz="0 0 0" rpy="${PI/2} 0 0"/>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="${wheel_radius}" length="${wheel_length}"/>
      </geometry>
      <origin xyz="0 0 0" rpy="${PI/2} 0 0"/>
    </collision>
  </link>
</xacro:macro>

然后这样调用:

<xacro:add_wheel name="left" flag="1"/>
<xacro:add_wheel name="right" flag="-1"/>

现在要改轮子尺寸,只需要修改wheel_radiuswheel_length这两个属性。这种设计模式带来的好处远不止代码简洁:

  • 一致性保证:所有轮子参数绝对一致,不会出现“左轮半径0.0325,右轮半径0.0326”这种低级错误
  • 快速迭代:调整机器人尺寸时,改几个参数就能重新生成整个模型
  • 团队协作:不同成员负责不同模块,最后组合起来就行

1.3 模块化文件组织

一个良好的Xacro项目应该按功能模块拆分文件。我通常这样组织:

urdf/
├── common/                 # 通用定义
│   ├── materials.xacro    # 材料颜色定义
│   ├── inertial.xacro     # 惯性矩阵计算宏
│   └── constants.xacro    # 常量定义(如PI值)
├── chassis/               # 底盘模块
│   ├── base.xacro        # 底盘主体
│   └── wheels.xacro      # 轮子定义
├── sensors/               # 传感器模块
│   ├── camera.xacro      # 摄像头
│   ├── lidar.xacro       # 激光雷达
│   └── imu.xacro         # IMU(可选)
├── robot.xacro            # 主文件,组合所有模块
└── gazebo/                # Gazebo专用配置
    ├── plugins.xacro     # Gazebo插件
    └── materials.xacro   # Gazebo材质

这种结构在复杂机器人项目中特别有用。比如我们团队去年做的仓储机器人,有底盘、机械臂、传送带、视觉系统四个大模块,每个模块由不同的人开发。如果没有这种模块化设计,合并代码时会是一场灾难。

注意:Xacro文件中的${PI/2}这种表达式,实际计算时要注意精度。我曾经遇到过因为PI值精度不够(用了3.14而不是3.1415926),导致轮子有微小倾斜,在Gazebo中产生奇怪抖动的案例。

2. 底盘与驱动系统:构建移动基础

底盘是机器人的基础,设计时要同时考虑运动性能和仿真效率。

2.1 底盘结构设计

一个典型的差分驱动底盘包含以下部分:

  • base_footprint:虚拟链接,作为整个机器人的根坐标系
  • base_link:实际底盘主体
  • 驱动轮:两个,差分控制
  • 支撑轮:一个或多个,保持平衡

在Xacro中,我们可以这样定义底盘参数:

<!-- 底盘参数 -->
<xacro:property name="base_radius" value="0.15"/>      <!-- 底盘半径 -->
<xacro:property name="base_height" value="0.08"/>      <!-- 底盘高度 -->
<xacro:property name="base_mass" value="2.0"/>         <!-- 底盘质量 -->
<xacro:property name="wheel_radius" value="0.065"/>    <!-- 轮子半径 -->
<xacro:property name="wheel_width" value="0.03"/>      <!-- 轮子宽度 -->
<xacro:property name="wheel_mass" value="0.2"/>        <!-- 轮子质量 -->

2.2 差分驱动轮实现

差分驱动的核心是两个独立控制的轮子。在Gazebo中,我们需要为每个驱动轮配置传输(transmission)和Gazebo插件:

<!-- 驱动轮宏定义 -->
<xacro:macro name="drive_wheel" params="prefix x_offset">
  <link name="${prefix}_wheel">
    <!-- 视觉和碰撞几何体 -->
    <visual>
      <geometry>
        <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
      </geometry>
      <origin xyz="0 0 0" rpy="${PI/2} 0 0"/>
      <material name="black"/>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
      </geometry>
      <origin xyz="0 0 0" rpy="${PI/2} 0 0"/>
    </collision>
    
    <!-- 惯性属性(必须为Gazebo仿真提供) -->
    <inertial>
      <mass value="${wheel_mass}"/>
      <inertia ixx="${wheel_mass*(3*wheel_radius*wheel_radius+wheel_width*wheel_width)/12}" 
               ixy="0" ixz="0"
               iyy="${wheel_mass*(3*wheel_radius*wheel_radius+wheel_width*wheel_width)/12}" 
               iyz="0"
               izz="${wheel_mass*wheel_radius*wheel_radius/2}"/>
    </inertial>
  </link>
  
  <!-- 关节定义 -->
  <joint name="${prefix}_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="${prefix}_wheel"/>
    <origin xyz="${x_offset} 0 ${-wheel_radius}"/>
    <axis xyz="0 1 0"/>
  </joint>
  
  <!-- Gazebo传输配置 -->
  <transmission name="${prefix}_wheel_trans">
    <type>transmission_interface/SimpleTransmission</type>
    <joint name="${prefix}_wheel_joint">
      <hardwareInterface>VelocityJointInterface</hardwareInterface>
    </joint>
    <actuator name="${prefix}_wheel_motor">
      <mechanicalReduction>1</mechanicalReduction>
    </actuator>
  </transmission>
  
  <!-- Gazebo插件配置 -->
  <gazebo reference="${prefix}_wheel">
    <material>Gazebo/Black</material>
  </gazebo>
</xacro:macro>

这里有几个关键点需要注意:

  1. 惯性矩阵计算:Gazebo需要准确的惯性数据来进行物理仿真。圆柱体的惯性矩阵公式是固定的,但很多人会抄错。
  2. 关节类型:驱动轮用continuous类型,表示可以无限旋转。
  3. 硬件接口VelocityJointInterface表示我们通过速度控制轮子,也可以使用EffortJointInterface(力矩控制)。

2.3 支撑轮设计

支撑轮(万向轮)通常使用球体或小圆柱体,主要作用是保持机器人平衡:

<xacro:macro name="caster_wheel" params="name x y">
  <link name="${name}_caster">
    <visual>
      <geometry>
        <sphere radius="0.015"/>
      </geometry>
      <material name="gray"/>
    </visual>
    <collision>
      <geometry>
        <sphere radius="0.015"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.05"/>
      <inertia ixx="${2*0.05*0.015*0.015/5}" ixy="0" ixz="0"
               iyy="${2*0.05*0.015*0.015/5}" iyz="0"
               izz="${2*0.05*0.015*0.015/5}"/>
    </inertial>
  </link>
  
  <joint name="${name}_caster_joint" type="fixed">
    <parent link="base_link"/>
    <child link="${name}_caster"/>
    <origin xyz="${x} ${y} ${-base_height/2 - 0.015}"/>
  </joint>
</xacro:macro>

2.4 完整的底盘组装

把各个部分组合起来:

<!-- 底盘主体 -->
<link name="base_link">
  <visual>
    <geometry>
      <cylinder radius="${base_radius}" length="${base_height}"/>
    </geometry>
    <material name="blue"/>
  </visual>
  <collision>
    <geometry>
      <cylinder radius="${base_radius}" length="${base_height}"/>
    </geometry>
  </collision>
  <inertial>
    <mass value="${base_mass}"/>
    <origin xyz="0 0 0"/>
    <inertia ixx="${base_mass*(3*base_radius*base_radius+base_height*base_height)/12}" 
             ixy="0" ixz="0"
             iyy="${base_mass*(3*base_radius*base_radius+base_height*base_height)/12}" 
             iyz="0"
             izz="${base_mass*base_radius*base_radius/2}"/>
  </inertial>
</link>

<!-- 添加驱动轮 -->
<xacro:drive_wheel prefix="left" x_offset="${base_radius}"/>
<xacro:drive_wheel prefix="right" x_offset="${-base_radius}"/>

<!-- 添加支撑轮 -->
<xacro:caster_wheel name="front" x="0" y="${base_radius*0.7}"/>
<xacro:caster_wheel name="back" x="0" y="${-base_radius*0.7}"/>

3. 多传感器集成:摄像头与激光雷达

传感器集成是机器人仿真的难点,不仅要正确放置,还要配置Gazebo插件以生成仿真数据。

3.1 摄像头模块设计

摄像头在机器人中通常用于视觉导航、目标识别等任务。在Gazebo中仿真摄像头需要配置相机传感器插件。

<!-- 摄像头参数 -->
<xacro:property name="camera_width" value="0.05"/>      <!-- 摄像头宽度 -->
<xacro:property name="camera_height" value="0.03"/>     <!-- 摄像头高度 -->
<xacro:property name="camera_depth" value="0.04"/>      <!-- 摄像头深度 -->
<xacro:property name="camera_mass" value="0.1"/>        <!-- 摄像头质量 -->

<xacro:macro name="camera_sensor" params="name parent_link x y z roll pitch yaw">
  <!-- 摄像头链接 -->
  <link name="${name}_link">
    <visual>
      <geometry>
        <box size="${camera_depth} ${camera_width} ${camera_height}"/>
      </geometry>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <material name="black"/>
    </visual>
    <collision>
      <geometry>
        <box size="${camera_depth} ${camera_width} ${camera_height}"/>
      </geometry>
      <origin xyz="0 0 0" rpy="0 0 0"/>
    </collision>
    <inertial>
      <mass value="${camera_mass}"/>
      <inertia ixx="${camera_mass*(camera_height*camera_height + camera_depth*camera_depth)/12}" 
               ixy="0" ixz="0"
               iyy="${camera_mass*(camera_width*camera_width + camera_depth*camera_depth)/12}" 
               iyz="0"
               izz="${camera_mass*(camera_width*camera_width + camera_height*camera_height)/12}"/>
    </inertial>
  </link>
  
  <!-- 摄像头关节 -->
  <joint name="${name}_joint" type="fixed">
    <parent link="${parent_link}"/>
    <child link="${name}_link"/>
    <origin xyz="${x} ${y} ${z}" rpy="${roll} ${pitch} ${yaw}"/>
  </joint>
  
  <!-- Gazebo摄像头插件 -->
  <gazebo reference="${name}_link">
    <sensor type="camera" name="${name}_sensor">
      <update_rate>30.0</update_rate>
      <camera name="${name}">
        <horizontal_fov>1.3962634</horizontal_fov>  <!-- 80度视角 -->
        <image>
          <width>640</width>
          <height>480</height>
          <format>R8G8B8</format>
        </image>
        <clip>
          <near>0.02</near>
          <far>300</far>
        </clip>
        <noise>
          <type>gaussian</type>
          <mean>0.0</mean>
          <stddev>0.007</stddev>
        </noise>
      </camera>
      <plugin name="${name}_controller" filename="libgazebo_ros_camera.so">
        <alwaysOn>true</alwaysOn>
        <updateRate>0.0</updateRate>
        <cameraName>${name}</cameraName>
        <imageTopicName>image_raw</imageTopicName>
        <cameraInfoTopicName>camera_info</cameraInfoTopicName>
        <frameName>${name}_link</frameName>
        <hackBaseline>0.07</hackBaseline>
      </plugin>
    </sensor>
  </gazebo>
</xacro:macro>

这里有几个重要参数需要根据实际需求调整:

参数典型值说明
update_rate30.0摄像头帧率,单位Hz
horizontal_fov1.396水平视场角,80度对应的弧度值
image/width640图像宽度像素
image/height480图像高度像素
clip/near0.02最近可视距离(米)
clip/far300最远可视距离(米)
noise/stddev0.007图像噪声标准差

3.2 激光雷达模块设计

激光雷达(LiDAR)是移动机器人最常用的传感器之一,用于建图和避障。Gazebo提供了多种激光传感器插件。

<!-- 激光雷达参数 -->
<xacro:property name="lidar_radius" value="0.05"/>      <!-- 雷达半径 -->
<xacro:property name="lidar_height" value="0.06"/>      <!-- 雷达高度 -->
<xacro:property name="lidar_mass" value="0.3"/>         <!-- 雷达质量 -->

<xacro:macro name="lidar_sensor" params="name parent_link x y z roll pitch yaw">
  <!-- 雷达支架(可选) -->
  <link name="${name}_mount">
    <visual>
      <geometry>
        <cylinder radius="0.01" length="0.15"/>
      </geometry>
      <material name="silver"/>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.01" length="0.15"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.05"/>
      <inertia ixx="${0.05*(3*0.01*0.01+0.15*0.15)/12}" ixy="0" ixz="0"
               iyy="${0.05*(3*0.01*0.01+0.15*0.15)/12}" iyz="0"
               izz="${0.05*0.01*0.01/2}"/>
    </inertial>
  </link>
  
  <joint name="${name}_mount_joint" type="fixed">
    <parent link="${parent_link}"/>
    <child link="${name}_mount"/>
    <origin xyz="${x} ${y} ${z}" rpy="${roll} ${pitch} ${yaw}"/>
  </joint>
  
  <!-- 雷达主体 -->
  <link name="${name}_link">
    <visual>
      <geometry>
        <cylinder radius="${lidar_radius}" length="${lidar_height}"/>
      </geometry>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <material name="black"/>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="${lidar_radius}" length="${lidar_height}"/>
      </geometry>
      <origin xyz="0 0 0" rpy="0 0 0"/>
    </collision>
    <inertial>
      <mass value="${lidar_mass}"/>
      <inertia ixx="${lidar_mass*(3*lidar_radius*lidar_radius+lidar_height*lidar_height)/12}" 
               ixy="0" ixz="0"
               iyy="${lidar_mass*(3*lidar_radius*lidar_radius+lidar_height*lidar_height)/12}" 
               iyz="0"
               izz="${lidar_mass*lidar_radius*lidar_radius/2}"/>
    </inertial>
  </link>
  
  <joint name="${name}_joint" type="fixed">
    <parent link="${name}_mount"/>
    <child link="${name}_link"/>
    <origin xyz="0 0 0.075" rpy="0 0 0"/>  <!-- 安装在支架顶部 -->
  </joint>
  
  <!-- Gazebo激光雷达插件 -->
  <gazebo reference="${name}_link">
    <sensor type="ray" name="${name}_sensor">
      <pose>0 0 0 0 0 0</pose>
      <visualize>false</visualize>
      <update_rate>40</update_rate>
      <ray>
        <scan>
          <horizontal>
            <samples>720</samples>          <!-- 360度扫描,每0.5度一个点 -->
            <resolution>1.0</resolution>
            <min_angle>-3.14159</min_angle> <!-- -180度 -->
            <max_angle>3.14159</max_angle>  <!-- +180度 -->
          </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>             <!-- 1cm标准差噪声 -->
        </noise>
      </ray>
      <plugin name="${name}_controller" filename="libgazebo_ros_laser.so">
        <topicName>scan</topicName>
        <frameName>${name}_link</frameName>
      </plugin>
    </sensor>
  </gazebo>
</xacro:macro>

激光雷达的关键配置参数:

参数说明
samples720水平方向采样点数,对应360度扫描
resolution1.0水平分辨率系数
min_angle-3.14159最小扫描角度(-π)
max_angle3.14159最大扫描角度(+π)
range/min0.10最小有效测距(米)
range/max12.0最大有效测距(米)
update_rate40扫描频率(Hz)

3.3 传感器坐标对齐技巧

这是多传感器集成中最容易出错的部分。每个传感器都有自己的坐标系,如果对齐不正确,后续的传感器融合和导航算法都会出问题。

3.3.1 坐标系定义规则

在ROS中,坐标系遵循右手规则:

  • X轴:向前
  • Y轴:向左
  • Z轴:向上

对于摄像头:

  • 光轴通常沿Z轴负方向(Gazebo默认)或X轴正方向
  • 图像坐标系:X向右,Y向下,Z向前

对于激光雷达:

  • 通常水平安装,0度方向沿X轴正方向
  • 扫描平面为XY平面
3.3.2 实际安装示例

假设我们的机器人设计如下:

  • 摄像头安装在底盘前部中央,朝前
  • 激光雷达安装在底盘中央上方,水平扫描
<!-- 安装摄像头:在base_link前方0.1m,高度0.08m,俯仰角向下10度 -->
<xacro:camera_sensor name="front_camera" parent_link="base_link"
                     x="0.1" y="0" z="0.08"
                     roll="0" pitch="-0.1745" yaw="0"/>  <!-- -10度俯角 -->

<!-- 安装激光雷达:在base_link正上方0.2m -->
<xacro:lidar_sensor name="lidar" parent_link="base_link"
                    x="0" y="0" z="0.05"
                    roll="0" pitch="0" yaw="0"/>
3.3.3 验证坐标系

在Gazebo中启动机器人后,可以通过以下命令验证坐标系:

# 查看所有坐标系
rosrun tf view_frames

# 查看特定坐标系关系
rosrun tf tf_echo base_link front_camera_link

# 在RViz中可视化
rosrun rviz rviz

在RViz中添加:

  1. TF显示,查看所有坐标系
  2. LaserScan显示,查看激光雷达数据
  3. Camera显示,查看摄像头图像

确保激光雷达的扫描平面与地面平行,摄像头的视野覆盖预期区域。

4. Gazebo仿真环境配置与启动

有了机器人模型,接下来需要配置Gazebo仿真环境和启动文件。

4.1 创建仿真世界

Gazebo世界文件定义了仿真环境。我们可以创建一个简单的室内环境:

<!-- box_house.world -->
<sdf version="1.6">
  <world name="default">
    <!-- 环境光照 -->
    <include>
      <uri>model://sun</uri>
    </include>
    
    <!-- 地面 -->
    <include>
      <uri>model://ground_plane</uri>
    </include>
    
    <!-- 简单室内环境 -->
    <model name="room">
      <pose>0 0 0 0 0 0</pose>
      <static>true</static>
      
      <!-- 地板 -->
      <link name="floor">
        <collision name="floor_collision">
          <geometry>
            <box>
              <size>10 10 0.1</size>
            </box>
          </geometry>
        </collision>
        <visual name="floor_visual">
          <geometry>
            <box>
              <size>10 10 0.1</size>
            </box>
          </geometry>
          <material>
            <script>
              <uri>file://media/materials/scripts/gazebo.material</uri>
              <name>Gazebo/Grey</name>
            </script>
          </material>
        </visual>
      </link>
      
      <!-- 墙壁 -->
      <link name="wall_north">
        <pose>0 5 1 0 0 0</pose>
        <collision>
          <geometry>
            <box>
              <size>10 0.1 2</size>
            </box>
          </geometry>
        </collision>
        <visual>
          <geometry>
            <box>
              <size>10 0.1 2</size>
            </box>
          </geometry>
          <material>
            <script>
              <uri>file://media/materials/scripts/gazebo.material</uri>
              <name>Gazebo/White</name>
            </script>
          </material>
        </visual>
      </link>
      
      <!-- 更多墙壁定义... -->
      
      <!-- 测试障碍物 -->
      <link name="obstacle1">
        <pose>2 0 0.5 0 0 0</pose>
        <collision>
          <geometry>
            <cylinder>
              <radius>0.3</radius>
              <length>1.0</length>
            </cylinder>
          </geometry>
        </collision>
        <visual>
          <geometry>
            <cylinder>
              <radius>0.3</radius>
              <length>1.0</length>
            </cylinder>
          </geometry>
          <material>
            <script>
              <uri>file://media/materials/scripts/gazebo.material</uri>
              <name>Gazebo/Red</name>
            </script>
          </material>
        </visual>
      </link>
    </model>
    
    <!-- 物理引擎参数 -->
    <physics name="default_physics" default="true" type="ode">
      <max_step_size>0.001</max_step_size>
      <real_time_factor>1.0</real_time_factor>
      <real_time_update_rate>1000</real_time_update_rate>
    </physics>
  </world>
</sdf>

4.2 创建Launch文件

Launch文件是ROS中启动多个节点的标准方式。我们需要一个launch文件来启动Gazebo、加载机器人模型和控制器。

<!-- robot_simulation.launch -->
<launch>
  <!-- 参数设置 -->
  <arg name="paused" default="false"/>
  <arg name="use_sim_time" default="true"/>
  <arg name="gui" default="true"/>
  <arg name="headless" default="false"/>
  <arg name="debug" default="false"/>
  <arg name="world_name" default="$(find robot_description)/worlds/box_house.world"/>
  
  <!-- 启动Gazebo -->
  <include file="$(find gazebo_ros)/launch/empty_world.launch">
    <arg name="world_name" value="$(arg world_name)"/>
    <arg name="debug" value="$(arg debug)"/>
    <arg name="gui" value="$(arg gui)"/>
    <arg name="paused" value="$(arg paused)"/>
    <arg name="use_sim_time" value="$(arg use_sim_time)"/>
    <arg name="headless" value="$(arg headless)"/>
  </include>
  
  <!-- 加载机器人描述 -->
  <param name="robot_description" 
         command="$(find xacro)/xacro '$(find robot_description)/urdf/robot.xacro'"/>
  
  <!-- 在Gazebo中生成机器人 -->
  <node name="urdf_spawner" pkg="gazebo_ros" type="spawn_model"
        args="-urdf -model mobile_robot -param robot_description -x 0 -y 0 -z 0.1"
        respawn="false" output="screen"/>
  
  <!-- 加载关节状态控制器 -->
  <rosparam file="$(find robot_description)/config/joint_state_controller.yaml" command="load"/>
  
  <!-- 加载差分驱动控制器 -->
  <rosparam file="$(find robot_description)/config/diff_drive_controller.yaml" command="load"/>
  
  <!-- 启动控制器管理器 -->
  <node name="controller_spawner" pkg="controller_manager" type="spawner"
        args="joint_state_controller mobile_base_controller"/>
  
  <!-- 机器人状态发布 -->
  <node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher"/>
  
  <!-- RViz可视化 -->
  <node name="rviz" pkg="rviz" type="rviz" 
        args="-d $(find robot_description)/config/robot_simulation.rviz"/>
</launch>

4.3 控制器配置

控制器配置文件定义了如何控制机器人。对于差分驱动机器人,我们需要两个控制器:

joint_state_controller.yaml

joint_state_controller:
  type: joint_state_controller/JointStateController
  publish_rate: 50

diff_drive_controller.yaml

mobile_base_controller:
  type: diff_drive_controller/DiffDriveController
  left_wheel: ['left_wheel_joint']
  right_wheel: ['right_wheel_joint']
  
  wheel_separation: 0.3          # 左右轮间距
  wheel_radius: 0.065            # 轮子半径
  
  pose_covariance_diagonal: [0.001, 0.001, 0.001, 0.001, 0.001, 0.01]
  twist_covariance_diagonal: [0.001, 0.001, 0.001, 0.001, 0.001, 0.01]
  
  # 里程计设置
  publish_rate: 50.0
  base_frame_id: base_footprint
  odom_frame_id: odom
  
  # 速度限制
  linear:
    x:
      has_velocity_limits: true
      max_velocity: 1.0          # 最大线速度 1 m/s
      min_velocity: -1.0         # 最小线速度(后退)
      has_acceleration_limits: true
      max_acceleration: 0.5      # 最大加速度 0.5 m/s²
  angular:
    z:
      has_velocity_limits: true
      max_velocity: 2.0          # 最大角速度 2 rad/s
      has_acceleration_limits: true
      max_acceleration: 1.0      # 最大角加速度 1 rad/s²

4.4 测试仿真系统

现在可以测试完整的仿真系统了:

# 编译工作空间
cd ~/catkin_ws
catkin_make
source devel/setup.bash

# 启动仿真
roslaunch robot_description robot_simulation.launch

如果一切正常,你应该看到:

  1. Gazebo窗口打开,显示室内环境和机器人
  2. RViz窗口打开,显示机器人模型和传感器数据
  3. 机器人站立在地面上

测试机器人运动:

# 在新的终端中发送速度命令
rostopic pub /mobile_base_controller/cmd_vel geometry_msgs/Twist "linear:
  x: 0.2
  y: 0.0
  z: 0.0
angular:
  x: 0.0
  y: 0.0
  z: 0.1" -r 10

机器人应该以0.2 m/s的速度前进,同时以0.1 rad/s的角速度左转。

5. 高级技巧与故障排除

在实际项目中,你可能会遇到各种问题。这里分享一些常见问题的解决方案。

5.1 Gazebo中机器人抖动或翻转

这是最常见的问题之一,通常由以下原因引起:

原因1:惯性矩阵计算错误

<!-- 错误的惯性矩阵 -->
<inertial>
  <mass value="0.5"/>
  <inertia ixx="0.1" ixy="0" ixz="0"
           iyy="0.1" iyz="0"
           izz="0.1"/>  <!-- 所有值相同,不符合物理规律 -->
</inertial>

<!-- 正确的惯性矩阵(圆柱体) -->
<inertial>
  <mass value="0.5"/>
  <inertia ixx="${0.5*(3*0.1*0.1+0.2*0.2)/12}" ixy="0" ixz="0"
           iyy="${0.5*(3*0.1*0.1+0.2*0.2)/12}" iyz="0"
           izz="${0.5*0.1*0.1/2}"/>
</inertial>

原因2:质量设置不合理

  • 底盘质量太轻:至少1.0 kg
  • 轮子质量太重:不超过底盘质量的1/10
  • 重心太高:尽量降低传感器安装高度

原因3:关节限制过松

<!-- 添加关节限制 -->
<joint name="wheel_joint" type="continuous">
  <limit effort="100" velocity="10"/>
  <dynamics damping="0.1" friction="0.01"/>
</joint>

5.2 传感器数据异常

激光雷达数据不稳定

<!-- 增加扫描频率和采样点 -->
<update_rate>40</update_rate>
<samples>720</samples>
<resolution>1.0</resolution>

<!-- 添加噪声模型 -->
<noise>
  <type>gaussian</type>
  <mean>0.0</mean>
  <stddev>0.01</stddev>
</noise>

摄像头图像延迟

<!-- 降低图像分辨率和帧率 -->
<image>
  <width>320</width>
  <height>240</height>
</image>
<update_rate>15.0</update_rate>

5.3 性能优化建议

当机器人模型复杂时,Gazebo仿真可能会变慢。以下优化方法很有效:

  1. 简化碰撞模型:使用简单的几何体代替复杂网格

    <!-- 使用简化碰撞模型 -->
    <collision>
      <geometry>
        <box size="0.1 0.1 0.05"/>  <!-- 简单盒子代替复杂网格 -->
      </geometry>
    </collision>
    
  2. 减少不必要的传感器更新

    <update_rate>10.0</update_rate>  <!-- 降低非关键传感器频率 -->
    
  3. 使用Gazebo的性能分析工具

    # 查看Gazebo性能统计
    gz stats
    
    # 记录性能数据
    gz log -d 1 -o performance.log
    

5.4 模块化设计的扩展性

本文展示的双轮差分机器人只是起点。同样的模块化设计可以扩展到更复杂的机器人:

四轮驱动机器人

<!-- 定义四轮驱动宏 -->
<xacro:macro name="four_wheel_drive" params="front_x rear_x left_y right_y">
  <xacro:drive_wheel prefix="front_left" x="${front_x}" y="${left_y}"/>
  <xacro:drive_wheel prefix="front_right" x="${front_x}" y="${right_y}"/>
  <xacro:drive_wheel prefix="rear_left" x="${rear_x}" y="${left_y}"/>
  <xacro:drive_wheel prefix="rear_right" x="${rear_x}" y="${right_y}"/>
</xacro:macro>

机械臂集成

<!-- 在主机器人文件中包含机械臂 -->
<xacro:include filename="$(find robot_description)/urdf/arm/arm.xacro"/>

<!-- 安装机械臂 -->
<xacro:arm_6dof parent="base_link" x="0" y="0" z="0.1"/>

多传感器融合

<!-- 添加IMU -->
<xacro:include filename="$(find robot_description)/urdf/sensors/imu.xacro"/>
<xacro:imu_sensor name="imu" parent="base_link" x="0" y="0" z="0.05"/>

<!-- 添加深度相机 -->
<xacro:include filename="$(find robot_description)/urdf/sensors/depth_camera.xacro"/>
<xacro:depth_camera name="depth_cam" parent="base_link" x="0.1" y="0" z="0.08"/>

这种模块化设计最大的好处是,当你需要修改或升级机器人时,只需要修改对应的模块文件,不会影响其他部分。比如要把摄像头从单目升级到双目,只需要替换camera.xacro文件,底盘和激光雷达完全不用动。

我在实际项目中最深的一点体会是:好的模块化设计不仅能提高开发效率,还能大大降低维护成本。我们团队维护的一个仓储机器人项目,三年间硬件迭代了四次,每次都是只更新对应的模块文件,整个仿真框架基本没变。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐