注意:如下教程基于安装了ros-humble-desktop的情况

1. 创建包

ros2 pkg create --build-type ament_cmake car_sim_bringup

其中目录结构如下
什么目录没有就直接mkdir创建就行

car_sim_bringup/
├── CMakeLists.txt
├── config
│   ├── custom_nav2_params.yaml
│   └── diff_drive_controller.yaml
├── include
│   └── car_sim_bringup
├── launch
│   └── sim.launch.py
├── maps
│   ├── room.pgm
│   └── room.yaml
├── package.xml
├── src
├── urdf
│   └── robot.urdf.xacro
└── worlds
    └── indoor.world

1.1创建机器人描述文件

在urdf目录下新建robot.urdf.xacro
向其中写入如下内容
注意导致小车无法旋转的摩擦力方向
其中还加入了激光雷达,imu,和四轮差速控制器插件

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

  <!-- 参数定义 -->
  <xacro:property name="chassis_length" value="0.4"/>
  <xacro:property name="chassis_width" value="0.3"/>
  <xacro:property name="chassis_height" value="0.1"/>
  <xacro:property name="wheel_radius" value="0.05"/>
  <xacro:property name="wheel_width" value="0.04"/>
  <xacro:property name="wheel_x_offset" value="0.12"/>
  <xacro:property name="wheel_y_offset" value="0.18"/>

  <!-- 激光雷达参数 -->
  <xacro:property name="lidar_radius" value="0.04"/>
  <xacro:property name="lidar_length" value="0.04"/>

  <!-- ==================== 1. 底盘与基准坐标系 ==================== -->
  <link name="base_footprint"/>
  <joint name="base_footprint_joint" type="fixed">
    <parent link="base_footprint"/>
    <child link="base_link"/>
    <origin xyz="0 0 ${wheel_radius}" rpy="0 0 0"/>
  </joint>

  <link name="base_link">
    <visual>
      <origin xyz="0 0 ${chassis_height/2}"/>
      <geometry>
        <box size="${chassis_length} ${chassis_width} ${chassis_height}"/>
      </geometry>
      <material name="blue">
        <color rgba="0.2 0.2 0.8 1.0"/>
      </material>
    </visual>
    <collision>
      <origin xyz="0 0 ${chassis_height/2}"/>
      <geometry>
        <box size="${chassis_length} ${chassis_width} ${chassis_height}"/>
      </geometry>
    </collision>
    <inertial>
      <origin xyz="0 0 ${chassis_height/2}"/>
      <mass value="5.0"/>
      <inertia ixx="0.041" ixy="0" ixz="0" iyy="0.071" iyz="0" izz="0.104"/>
    </inertial>
  </link>

  <!-- ==================== 2. 车轮宏定义与实例化 ==================== -->
  <xacro:macro name="wheel" params="prefix x_reflect y_reflect">
    <link name="${prefix}_wheel">
      <visual>
        <geometry>
          <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
        </geometry>
        <material name="black">
          <color rgba="0.1 0.1 0.1 1.0"/>
        </material>
      </visual>
      <collision>
        <geometry>
          <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
        </geometry>
      </collision>
      <inertial>
        <mass value="0.5"/>
        <inertia ixx="0.00038" ixy="0" ixz="0" iyy="0.00038" iyz="0" izz="0.000625"/>
      </inertial>
    </link>

    <joint name="${prefix}_wheel_joint" type="continuous">
      <parent link="base_link"/>
      <child link="${prefix}_wheel"/>
      <origin xyz="${x_reflect * wheel_x_offset} ${y_reflect * wheel_y_offset} 0" rpy="-1.5708 0 0"/>
      <axis xyz="0 0 1"/>
      <limit effort="50.0" velocity="10.0"/>
    </joint>

    <gazebo reference="${prefix}_wheel">
      <mu1>1.0</mu1>
      <mu2>0.2</mu2>
      <!-- 这个不加会导致横向和纵向摩擦系数一致,从而导致小车无法滑动转圈 -->
      <fdir1>1 0 0</fdir1>      <!-- 指定轮子滚动切向为局部坐标系 X 轴 -->
      <kp>10000000.0</kp>
      <kd>1.0</kd>
      <material>Gazebo/DarkGrey</material>
    </gazebo>
  </xacro:macro>

  <xacro:wheel prefix="front_left" x_reflect="1" y_reflect="1" />
  <xacro:wheel prefix="front_right" x_reflect="1" y_reflect="-1" />
  <xacro:wheel prefix="rear_left" x_reflect="-1" y_reflect="1" />
  <xacro:wheel prefix="rear_right" x_reflect="-1" y_reflect="-1" />

  <!-- ==================== 3. 2D 激光雷达传感器 ==================== -->
  <link name="laser_link">
    <visual>
      <geometry>
        <cylinder radius="${lidar_radius}" length="${lidar_length}"/>
      </geometry>
      <material name="red">
        <color rgba="0.8 0.1 0.1 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="${lidar_radius}" length="${lidar_length}"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.125"/>
      <inertia ixx="0.0001" ixy="0" ixz="0" iyy="0.0001" iyz="0" izz="0.0001"/>
    </inertial>
  </link>

  <!-- 雷达安装在车顶前方 0.08m 处,抬高半个雷达高度 -->
  <joint name="laser_joint" type="fixed">
    <parent link="base_link"/>
    <child link="laser_link"/>
    <origin xyz="0.08 0 ${chassis_height + lidar_length/2}" rpy="0 0 0"/>
  </joint>

  <!-- Gazebo 2D 激光雷达插件 (ray sensor) -->
  <gazebo reference="laser_link">
    <material>Gazebo/Red</material>
    <sensor name="lidar_sensor" type="ray">
      <always_on>true</always_on>
      <visualize>false</visualize>
      <update_rate>10</update_rate>
      <ray>
        <scan>
          <horizontal>
            <samples>360</samples>
            <resolution>1</resolution>
            <min_angle>-3.14159265</min_angle>
            <max_angle>3.14159265</max_angle>
          </horizontal>
        </scan>
        <range>
          <min>0.12</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="lidar_plugin" filename="libgazebo_ros_ray_sensor.so">
        <ros>
          <remapping>~/out:=scan</remapping>
        </ros>
        <output_type>sensor_msgs/LaserScan</output_type>
        <frame_name>laser_link</frame_name>
      </plugin>
    </sensor>
  </gazebo>

  <!-- ==================== 4. IMU 传感器 ==================== -->
  <link name="imu_link">
    <visual>
      <geometry>
        <box size="0.02 0.02 0.01"/>
      </geometry>
      <material name="green">
        <color rgba="0.1 0.8 0.1 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.02 0.02 0.01"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.01"/>
      <inertia ixx="1e-5" ixy="0" ixz="0" iyy="1e-5" iyz="0" izz="1e-5"/>
    </inertial>
  </link>

  <!-- IMU 安装在底盘内部质心几何中心 -->
  <joint name="imu_joint" type="fixed">
    <parent link="base_link"/>
    <child link="imu_link"/>
    <origin xyz="0 0 ${chassis_height/2}" rpy="0 0 0"/>
  </joint>

  <!-- Gazebo IMU 传感器插件 -->
  <gazebo reference="imu_link">
    <material>Gazebo/Green</material>
    <sensor name="imu_sensor" type="imu">
      <always_on>true</always_on>
      <update_rate>100</update_rate>
      <imu>
        <angular_velocity>
          <x>
            <noise type="gaussian">
              <mean>0.0</mean>
              <stddev>2e-4</stddev>
            </noise>
          </x>
          <y>
            <noise type="gaussian">
              <mean>0.0</mean>
              <stddev>2e-4</stddev>
            </noise>
          </y>
          <z>
            <noise type="gaussian">
              <mean>0.0</mean>
              <stddev>2e-4</stddev>
            </noise>
          </z>
        </angular_velocity>
        <linear_acceleration>
          <x>
            <noise type="gaussian">
              <mean>0.0</mean>
              <stddev>1.7e-2</stddev>
            </noise>
          </x>
          <y>
            <noise type="gaussian">
              <mean>0.0</mean>
              <stddev>1.7e-2</stddev>
            </noise>
          </y>
          <z>
            <noise type="gaussian">
              <mean>0.0</mean>
              <stddev>1.7e-2</stddev>
            </noise>
          </z>
        </linear_acceleration>
      </imu>
      <plugin name="imu_plugin" filename="libgazebo_ros_imu_sensor.so">
        <ros>
          <remapping>~/out:=imu/data</remapping>
        </ros>
        <initial_orientation_as_reference>false</initial_orientation_as_reference>
        <frame_name>imu_link</frame_name>
      </plugin>
    </sensor>
  </gazebo>

  <!-- ==================== 5. ros2_control 硬件接口 ==================== -->
  <ros2_control name="GazeboSystem" type="system">
    <hardware>
      <plugin>gazebo_ros2_control/GazeboSystem</plugin>
    </hardware>
    <xacro:macro name="wheel_control" params="joint_name">
      <joint name="${joint_name}">
        <command_interface name="velocity">
          <param name="min">-10</param>
          <param name="max">10</param>
        </command_interface>
        <state_interface name="position"/>
        <state_interface name="velocity"/>
      </joint>
    </xacro:macro>

    <xacro:wheel_control joint_name="front_left_wheel_joint"/>
    <xacro:wheel_control joint_name="front_right_wheel_joint"/>
    <xacro:wheel_control joint_name="rear_left_wheel_joint"/>
    <xacro:wheel_control joint_name="rear_right_wheel_joint"/>
  </ros2_control>

  <!-- ==================== 6. 控制器参数与重映射 ==================== -->
  <xacro:arg name="controller_params_file" default="$(find-pkg-share your_robot_package)/config/diff_drive_controller.yaml"/>

  <gazebo>
    <plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
      <parameters>$(arg controller_params_file)</parameters>
      <ros>
        <remapping>/diff_drive_base_controller/cmd_vel_unstamped:=/cmd_vel</remapping>
        <remapping>/diff_drive_base_controller/odom:=/odom</remapping>
      </ros>
    </plugin>
  </gazebo>

</robot>

1.2 写入世界文件给gazebo用

在worlds目录下创建indoor.world,并写入如下内容

<?xml version="1.0" ?>
<sdf version="1.6">
  <world name="indoor_world">

    <!-- 基础光源与地面 -->
    <include>
      <uri>model://sun</uri>
    </include>
    <include>
      <uri>model://ground_plane</uri>
    </include>

    <physics type="ode">
      <real_time_update_rate>1000.0</real_time_update_rate>
      <max_step_size>0.001</max_step_size>
    </physics>

    <!-- 室内建筑墙体与障碍物模型 (10m x 8m 多隔间结构) -->
    <model name="indoor_environment">
      <static>true</static>
      
      <!-- 外墙: 北 -->
      <link name="wall_north">
        <pose>0 4 0.5 0 0 0</pose>
        <collision name="col"><geometry><box><size>10 0.15 1.0</size></box></geometry></collision>
        <visual name="vis"><geometry><box><size>10 0.15 1.0</size></box></geometry><material><ambient>0.8 0.8 0.8 1</ambient></material></visual>
      </link>

      <!-- 外墙: 南 -->
      <link name="wall_south">
        <pose>0 -4 0.5 0 0 0</pose>
        <collision name="col"><geometry><box><size>10 0.15 1.0</size></box></geometry></collision>
        <visual name="vis"><geometry><box><size>10 0.15 1.0</size></box></geometry><material><ambient>0.8 0.8 0.8 1</ambient></material></visual>
      </link>

      <!-- 外墙: 西 -->
      <link name="wall_west">
        <pose>-5 0 0.5 0 0 0</pose>
        <collision name="col"><geometry><box><size>0.15 8 1.0</size></box></geometry></collision>
        <visual name="vis"><geometry><box><size>0.15 8 1.0</size></box></geometry><material><ambient>0.8 0.8 0.8 1</ambient></material></visual>
      </link>

      <!-- 外墙: 东 -->
      <link name="wall_east">
        <pose>5 0 0.5 0 0 0</pose>
        <collision name="col"><geometry><box><size>0.15 8 1.0</size></box></geometry></collision>
        <visual name="vis"><geometry><box><size>0.15 8 1.0</size></box></geometry><material><ambient>0.8 0.8 0.8 1</ambient></material></visual>
      </link>

      <!-- 室内隔墙 1 (左侧房间隔离,留出门洞) -->
      <link name="inner_wall_1">
        <pose>-1.8 1.2 0.5 0 0 0</pose>
        <collision name="col"><geometry><box><size>0.15 5.5 1.0</size></box></geometry></collision>
        <visual name="vis"><geometry><box><size>0.15 5.5 1.0</size></box></geometry><material><ambient>0.7 0.7 0.7 1</ambient></material></visual>
      </link>

      <!-- 室内隔墙 2 (右侧房间隔离,留出门洞) -->
      <link name="inner_wall_2">
        <pose>1.8 -1.2 0.5 0 0 0</pose>
        <collision name="col"><geometry><box><size>0.15 5.5 1.0</size></box></geometry></collision>
        <visual name="vis"><geometry><box><size>0.15 5.5 1.0</size></box></geometry><material><ambient>0.7 0.7 0.7 1</ambient></material></visual>
      </link>

      <!-- 房间内圆柱障碍物 1 -->
      <link name="obstacle_cylinder_1">
        <pose>-3.5 -2.0 0.5 0 0 0</pose>
        <collision name="col"><geometry><cylinder><radius>0.3</radius><length>1.0</length></cylinder></geometry></collision>
        <visual name="vis"><geometry><cylinder><radius>0.3</radius><length>1.0</length></cylinder></geometry><material><ambient>0.3 0.6 0.9 1</ambient></material></visual>
      </link>

      <!-- 房间内圆柱障碍物 2 -->
      <link name="obstacle_cylinder_2">
        <pose>3.5 2.0 0.5 0 0 0</pose>
        <collision name="col"><geometry><cylinder><radius>0.3</radius><length>1.0</length></cylinder></geometry></collision>
        <visual name="vis"><geometry><cylinder><radius>0.3</radius><length>1.0</length></cylinder></geometry><material><ambient>0.3 0.6 0.9 1</ambient></material></visual>
      </link>

    </model>
  </world>
</sdf>

1.3 编写launch文件

在launch目录下创建 sim.launch.py,并写入如下内容

import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription, RegisterEventHandler
from launch.event_handlers import OnProcessExit
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
import xacro

def generate_launch_description():
    pkg_share = get_package_share_directory('car_sim_bringup')
    gazebo_ros_pkg = get_package_share_directory('gazebo_ros')

    # 1. 获取文件路径
    xacro_file = os.path.join(pkg_share, 'urdf', 'robot.urdf.xacro')
    controller_params_file = os.path.join(pkg_share, 'config', 'diff_drive_controller.yaml')
    world_file = os.path.join(pkg_share, 'worlds', 'indoor.world')

    # 2. 解析 Xacro
    robot_description_raw = xacro.process_file(
        xacro_file,
        mappings={'controller_params_file': controller_params_file}
    ).toxml()

    # 3. 基础节点
    node_robot_state_publisher = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        output='screen',
        parameters=[{
            'robot_description': robot_description_raw,
            'use_sim_time': True
        }]
    )

    # 启动 Gazebo 并载入 indoor.world
    launch_gazebo = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            os.path.join(gazebo_ros_pkg, 'launch', 'gazebo.launch.py')
        ),
        launch_arguments={'world': world_file}.items()
    )

    # 生成小车实体(指定初始安全坐标 x=0, y=0, z=0.05)
    node_spawn_entity = Node(
        package='gazebo_ros',
        executable='spawn_entity.py',
        arguments=[
            '-topic', 'robot_description',
            '-entity', 'four_wheel_robot',
            '-x', '0.0',
            '-y', '0.0',
            '-z', '0.05'
        ],
        output='screen'
    )

    # 控制器 Spawner
    joint_state_broadcaster_spawner = Node(
        package='controller_manager',
        executable='spawner',
        arguments=['joint_state_broadcaster'],
        output='screen'
    )

    diff_drive_spawner = Node(
        package='controller_manager',
        executable='spawner',
        arguments=['diff_drive_base_controller'],
        output='screen'
    )

    return LaunchDescription([
        node_robot_state_publisher,
        launch_gazebo,
        node_spawn_entity,
        RegisterEventHandler(
            event_handler=OnProcessExit(
                target_action=node_spawn_entity,
                on_exit=[joint_state_broadcaster_spawner],
            )
        ),
        RegisterEventHandler(
            event_handler=OnProcessExit(
                target_action=joint_state_broadcaster_spawner,
                on_exit=[diff_drive_spawner],
            )
        )
    ])

1.4 写入差速控制器配置文件

由于引入了差速控制器,所以需要一个单独的配置文件来和已有的模型关联起来
在config目录下,创建diff_drive_controller.yaml文件并且写入如下内容

controller_manager:
  ros__parameters:
    update_rate: 50

    diff_drive_base_controller:
      type: diff_drive_controller/DiffDriveController

    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster

diff_drive_base_controller:
  ros__parameters:
    # 左右侧分别填入前后两轮关节名
    left_wheel_names: ["front_left_wheel_joint", "rear_left_wheel_joint"]
    right_wheel_names: ["front_right_wheel_joint", "rear_right_wheel_joint"]

    # 左右轮距与轮半径(必须与模型一致)
    wheel_separation: 0.36
    wheel_radius: 0.05

    # 四轮差速转向补偿系数(通常在 1.1 ~ 1.5 之间调节)
    wheel_separation_multiplier: 1.2

    publish_rate: 50.0
    odom_frame_id: odom
    base_frame_id: base_footprint
    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]

    open_loop: false
    enable_odom_tf: true

    cmd_vel_timeout: 0.5
    use_stamped_vel: false

2.安装nav2和slam_toolbox

sudo apt update
sudo apt install -y \
  ros-humble-slam-toolbox \
  ros-humble-navigation2 \
  ros-humble-nav2-bringup

2.1 自定义nav2配置文件

先复制一份其默认的配置文件

cp /opt/ros/humble/share/nav2_bringup/params/nav2_params.yaml \
   ~/ros2_ws/src/当前包/config/custom_nav2_params.yaml

主要修改一下其机器人半径,即robot_radius为footprint(因为不是圆形机器人)

在custom_nav2_params.yaml中,找到local和global的costmap,将其中的robot_radius给注释掉,替换成如下的准确的正方形信息

footprint: "[ [0.22, 0.17], [0.22, -0.17], [-0.22, -0.17], [-0.22, 0.17] ]"

2.2 更新CMakeLists.txt

添加如下内容,把资源让cmake装到share目录下,方便launch脚本找到

install(DIRECTORY
  launch
  DESTINATION share/${PROJECT_NAME}
)
install(DIRECTORY
  urdf
  DESTINATION share/${PROJECT_NAME}
)
install(DIRECTORY
  config
  DESTINATION share/${PROJECT_NAME}
)
install(DIRECTORY
  worlds
  DESTINATION share/${PROJECT_NAME}
)

2.3 构建并加载环境变量

cd ~/ws
colcon build
source ./install/setup.bash

3.1 启动launch文件

ros2 launch car_sim_bringup sim.launch.py

会启动机器人状态发布节点
gazebo仿真
差速控制器节点

3.3 启动rviz2

rviz2

这里我没在launch文件中一起启动rviz2,直接启动,然后手动添加添加需要的内容就行了
在这里插入图片描述

记得map添加三个,一个地图,一个local_cost_map,一个global_cost_map (启动nav2后就有了)

然后等会退出的时候问是否保存,选择save,这样下次这些话题就自己添加上了

3.3 启动slam_toolbox

slam_toolbox的配置我先没整自定义的,暂时没必要
直接启动就行

ros2 launch slam_toolbox online_async_launch.py

使用键盘控制器控制着建图,速度稍微慢点

ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args -p stamped:=false

自己控制着建图完成后,直接保存地图即可
进入maps目录,直接运行

ros2 run nav2_map_server map_saver_cli -f room

然后就会在当前目录下创建room.yaml和rooms.pgm文件

可以启动nav2

由于slam_toolbox启动后会发布/map话题,所以这里已经可以使用不加载地图的nav2了

ros2 launch nav2_bringup navigation_launch.py params_file:=./config/custom_nav2_params.yaml use_sim_time:=true

有/clock话题,就可以把use_sim_time:=true带上

3.4 启动nav2导航

这里就采用加载地图的nav2启动方式

ros2 launch nav2_bringup bringup_launch.py map:=./maps/room.yaml params_file:=./config/custom_nav2_params.yaml use_sim_time:=true
需要发布一个初始位置

记得把Fixed Frame切换为map
然后使用上面的2D Pose Estimate发布一个带方向的初始位置,amcl节点会根据激光雷达数据帮忙在局部校准一下odom->map的映射关系

然后用2D Goal Pose开始测试导航就行

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐