跳转至

第 13 章 升级动机与仿真环境搭建

第 12 章我们已经让 Go2 在实机上跑通了 L1 + 2D Nav2 基线。但如果你还记得章末那张“能走但有限”的清单,就会发现一个残酷事实:继续在这条链上硬抠参数,大概率只会越调越累。这一章,我们先把“为什么要换路”讲透,再把一套能走能扫的 Gazebo 仿真基础盘搭起来,给下一章真正的 3D 升级做准备。


本章你将学到

  • 用真实实验数据说明:为什么 Go2 的 L1 + 实机 3D LIO 这条路,不是“再调一调就能救回来”
  • 看懂 L1 的四个关键限制:步态 IMU 噪声、坐标系问题、稀疏单环点云、扩展坞官方支持范围
  • 比较 Gazebo Classic、Gazebo Gz、Isaac Sim、Webots 这几条仿真路线,理解为什么本书先选 Gazebo Classic
  • go2_sim_ws 里搭起 unitree-go2-ros2 + CHAMP + VLP-16 这一套仿真底座
  • 识别 8 个真正容易把人卡住的坑,避免把一周时间浪费在路径、DDS、控制器时序和传感器插件上

背景与原理

引子:为什么第 12 章不是终点

如果只看阶段性成果,第 11 章和第 12 章其实已经很像“胜利前夜”了:

  • 第 11 章我们有了 2D 地图
  • 第 12 章我们看到了 Go2 能自己走到目标点

但我当时很快就发现,这条路线有一个非常难受的现实:

我们想要的不是“平地里慢慢到点”,而是后面那条完整任务链:自主导航、感知、识别、再去放置。

一旦目标变成这个级别,2D 基线就只是起点,不是终点。

更糟的是,我最开始以为“再把 L1 接到 3D LIO 上,事情就自然升级了”。结果后面的几轮实验基本把这个幻想打碎了。

第一部分:L1 为什么升不动

这一节不是为了贬低 L1,而是为了把边界讲清楚。讲清楚边界,后面的换路才不是拍脑袋。

硬伤 1:步态 IMU 噪声淹没了真实运动

Point-LIO 在 Go2 上最迷惑人的地方是:

  • 静止时很好看
  • 一走起来就崩

实验里最稳定的一次结果是:

  • 机器人静止时,建图精度可以压到 ±2 cm
  • 一旦开始行走,点云会穿透墙体、旧信息丢失、位姿估计直接发散

我当时最先怀疑的是“是不是 IMU 太吵,低通滤波一下就好了”。结果实验数据很无情:

方案 截止频率 结果
一阶低通滤波 1 Hz 静止还行,旋转时位置飘到 -98 m
一阶低通滤波 3 Hz 启动就炸,z 直接飘到 -557 m

这组数字很值钱,因为它直接说明了一个工程事实:

Go2 步态振动的频段,和机器人真实运动引起的有效加速度变化频段,高度重叠。

也就是说:

  • 你把滤波器调狠了,步态噪声是少了,但真实运动信号也一起被你削掉了
  • 你把滤波器调松了,有效信号是保住了,步态噪声又原封不动灌进去了

这不是“参数还没调到位”,而是线性滤波在这个场景里的能力边界。

硬伤 2:L1 IMU 坐标系问题不是普通的小偏差

第二个坑比“IMU 很吵”更阴。

按官方 L1 手册的定义:

  • IMU 坐标系和 LiDAR 坐标系是平行对齐的
  • +Z 指向上方

但真正装到 Go2 身上之后,事情就没这么简单了。

一方面,Go2 上的 L1 是倒装的;另一方面,我在实测里又发现:

  • 直接照搬 CMU 开源项目里的 negate Y/Z + 15.1° 变换,重力方向会偏掉将近 30°
  • 进一步拆开看,加速度和角速度的坐标约定甚至不像是同一套“简单轴翻转”关系

也就是说,这里不是一个“差个 5° 再微调一下”的问题,而是传感器契约本身就不够规整。

来自宇树的回复

我当时把问题提给了宇树技术支持,拿到的几个关键信息如下:

  • L1 安装外参给到的是 xyzrpy = [0.28945, 0, -0.046825, 0, 2.8782, 0]
  • 官方给过一个 point_lio_unilidar 仓库,但它是 ROS1 Noetic 方案,不是 Go2 ROS2 直接可抄的成品
  • slam_operate 这套导航接口只支持 MID-360XT-16, L1 不支持
  • 机器人里的 uslam 主要是 App 侧功能,不提供这条开发链可直接接入的开放接口

这个回复其实非常关键。因为它帮我们确认了:

  • 你不是“姿势没找对”
  • 而是官方支持范围本来就不包括“L1 直接走这条开发链”

硬伤 3:ring = 1 的稀疏点云,只够做 2D 基线

第 10 章我们已经看过 L1 的点云性格了:

  • 单帧大约 4193 ~ 4194
  • 发布频率大约 10 Hz
  • ring 字段实测基本全是 1

这种点云当然不是“完全不能用”。第 11 章和第 12 章已经证明了:

  • 把它压成 /scan,做 2D SLAM 和 2D Nav2 是能跑的

但问题在于,3D LIO 对点云的要求比“能压成一圈二维扫描”高得多。

至少在我当时验证的这几条路上:

  • KISS-ICP 会嫌点云几何特征太稀
  • Point-LIO 会越来越依赖 IMU,而 IMU 又刚好是最不稳的那环
  • 依赖更强结构特征或稳定逐点时间的 3D LIO/回环方案,会更加难受

所以这里别把“L1 能输出 PointCloud2”误听成“L1 天然适合做 3D LIO 前端”。

硬伤 4:扩展坞这条官方链,本来就没把 L1 放进来

根据第 4 周那轮官方文档调研,以及后面自己查 graph_pid_ws 的结果:

  • 官方 SLAM 导航文档默认前提是 EDU + 扩展坞 + 官方购入激光雷达
  • 支持口径明确写的是 MID-360XT-16
  • 扩展坞那套预装链路里,雷达 IP、驱动脚本和 lio_sam 配置也都是围着它们写的

比如当时已经能确认的事实就包括:

  • 雷达默认 IP 直指 192.168.123.20
  • mid360_lidar.sh 调的就是 livox_ros_driver2
  • lio_sam 相关配置是 sensor: livox, N_SCAN: 4

这意味着什么?

意味着就算你愿意继续为 L1 坐标系和 IMU 数据硬啃,扩展坞那条官方“教育版 SLAM 导航主线”也并没有在前方等你。

此路不通

到这里,我们其实已经能下结论了:

  • 步态 IMU 噪声 不是调一两个滤波参数能救的
  • IMU 坐标系问题 不是普通的“小偏差”,而是传感器契约不够规整
  • L1 稀疏单环点云 足够做 2D 基线,但并不适合作为这条 3D LIO 升级链的稳定前端
  • 扩展坞官方支持 本来就站在 MID-360 / XT-16 这边,不站在 L1 这边

所以第 12 章之后最理性的决定,不是继续在实机 L1 上死磕 3D,而是先把算法链放到一个传感器契约更干净的地方跑通。

第二部分:为什么先去仿真

我当时最终切到仿真,不是因为“仿真更高级”,恰恰相反,是因为它更朴素:

  • IMU 数据更干净
  • 雷达模型更标准
  • 外参、TF、控制链都更容易穷举排查
  • 场景可控,出了问题能快速复现

这类转向最容易被误会成“逃避实机”,其实不是。

真正合理的顺序是:

  1. 先在仿真里把 算法和工程链 跑通
  2. 再带着一套已经验证过的系统回到实机
  3. 最后决定值不值得采购更适合的传感器,比如 MID-360

这比一边踩 L1 的硬件坑,一边还想同时调 3D 算法,要省时间得多。

第三部分:仿真方案怎么选

先比仿真器

这一步我没有追求“最强”,而是优先看 ROS2 Humble 生态四足现成例子

方案 学习曲线 ROS2 Humble 生态 四足现成示例 这一章是否推荐
Gazebo Classic 低到中 最成熟 有 CHAMP + Velodyne 现成链路 推荐
Gazebo Gz 新,但迁移成本高 有,但资料分散 暂不选
Isaac Sim 很强,但偏重 GPU 和生态整合 有,但更适合后期 暂不选
Webots 低到中 够用 四足案例少 暂不选

如果你现在的目标是“先把 Go2 在仿真里走起来,再让它能扫、能给下一章上 3D SLAM”,Gazebo Classic 是最省心的选择。

再比控制框架

控制层我最后也做过取舍。

方案 优点 缺点 结论
直接走 Go2 SDK 仿真链 接口更像实机 现成示例分散,对 URDF/传感器改造不友好 暂不选
CHAMP 四足步态框架成熟,ROS2/Gazebo 资料多,能直接接 cmd_vel 不是 Unitree 官方控制栈 选它

本书这一套教程里,CHAMP 的价值非常直接:

  • 它不是最终产品
  • 但它非常适合搭一块“能走、能看、能接传感器、能继续接 SLAM/Nav2”的实验底板

最后比雷达模型

这一步最容易想当然。

如果只考虑“以后可能买 MID-360”,你会本能地想直接模拟 MID-360。但我当时最后先选的是 VLP-16

原因有三个:

  1. Gazebo 现成插件和资料最多
  2. 输出是标准的 PointCloud2 多环点云,更容易接通后续 3D 算法链
  3. /velodyne_points 这条话题在 ROS 生态里几乎是“默认公约”,很多现成配置直接就能吃

不过这里要诚实说一句:

VLP-16 也不是完美答案

它解决的是“先把多环点云 + 标准 TF + 可控仿真场景搭起来”这件事,不是一步到位解决所有 3D SLAM 细节。比如我后来在第 14 章的联调里就发现,某些 Gazebo VLP-16 插件对逐点 time 字段的实现并不理想。这不影响本章把基础盘搭起来,但你别把它误会成“仿真里就没有坑了”。

本书这一章的最终选型

所以这章的结论很明确:

Gazebo Classic + CHAMP + VLP-16

原因可以压缩成三句话:

  • Gazebo Classic:最稳,和 ROS2 Humble 配合成熟
  • CHAMP:最省时间地把 Go2 变成“能走的四足机器人”
  • VLP-16:最容易先给下一章准备一份像样的 3D 点云输入

架构总览

仿真基础盘的数据流

本章我们先不急着接 Fast-LIO,也不急着接 Nav2。先把一块“能走能扫”的底板搭牢。

flowchart LR
    A[Gazebo World<br/>indoor.world] --> B[spawn_entity]
    B --> C[Go2 Robot Model<br/>robot_VLP.xacro]

    C --> D[gazebo_ros2_control]
    D --> E[controller_manager]
    E --> F[joint_states_controller]
    E --> G[joint_group_effort_controller]

    H[champ_teleop / 键盘输入] --> I["/cmd_vel"]
    I --> J[quadruped_controller_node]
    J --> G

    C --> K["/imu/data"]
    C --> L["/velodyne_points"]
    C --> M["/joint_states"]
    C --> N["/odom + TF"]

    K --> O[RViz]
    L --> O
    M --> O
    N --> O

    K --> P[第 14 章 3D SLAM]
    L --> P

仿真里这几个包分别干什么

为了后面不被目录绕晕,你先记住这一层分工:

unitree-go2-ros2/
├── champ/                         ← 四足控制框架本体
│   ├── champ_base/
│   ├── champ_bringup/
│   ├── champ_gazebo/
│   └── champ_description/
├── robots/descriptions/go2_description/
│   └── xacro/                     ← Go2 模型和传感器挂载
├── robots/configs/go2_config/
│   ├── launch/                    ← Gazebo / RViz / 后续导航入口
│   ├── worlds/                    ← 仿真世界
│   └── rviz/                      ← RViz 配置
└── champ_teleop/                  ← 键盘遥控

这套结构的好处是:

  • 机器人模型和传感器在 go2_description
  • 世界、launch 和 RViz 在 go2_config
  • 步态控制和状态估计在 champ/*

后面你查问题时,知道该去哪一层下手。

一个必须先说的坐标系提醒

第 12 章实机链路里,我们一直强调 Go2 用的是 base。但这一章的仿真链不是那套驱动,它的根 link 叫的是:

  • base_link

所以请你现在先把这条规则记住:

  • 实机章节跟 Go2 原生驱动时,优先认 base
  • 这一章仿真跟 CHAMP / URDF / Gazebo 时,优先认 base_link

如果你硬要在这章里把它们统一成一个名字,大概率会把 launch、RViz、EKF 和传感器 frame 一起搅乱。

环境准备

前置条件

开始本章前,你最好已经具备两类前置:

  1. 概念上读过 第 10 章 激光雷达原理与选型第 11 章 2D SLAM 建图实战第 12 章 实机 Nav2 2D 基线
  2. 机器上已经装好了 ROS2 Humble,并能正常 ros2 topic list

这一章不依赖你已经把实机链跑通,但它强依赖你知道:

  • 什么是 /cmd_vel
  • 什么是 TF
  • 什么是 PointCloud2

统一工作空间

原始笔记里,我当时单独开过一个仿真工作空间。但这本书为了统一命名,我们这里全部放进:

~/go2_sim_ws/

这样后面你在第 14 章继续加 3D SLAM,不会又多出一套新路径。

安装系统依赖

先把 Gazebo、ros2_control、Velodyne 仿真相关依赖补齐:

# Gazebo Classic + ROS2 控制 + 机器人建模 + VLP-16 插件
sudo apt install -y \
    gazebo \
    ros-humble-gazebo-ros-pkgs \
    ros-humble-gazebo-ros2-control \
    ros-humble-controller-manager \
    ros-humble-ros2-control \
    ros-humble-ros2-controllers \
    ros-humble-robot-localization \
    ros-humble-xacro \
    ros-humble-velodyne \
    ros-humble-velodyne-gazebo-plugins \
    ros-humble-velodyne-description \
    python3-rosdep

如果你的系统里还没初始化过 rosdep,先做一次:

# 只需要首次执行一次
sudo rosdep init
rosdep update

仿真开工前:清掉真机遗留的 DDS 绑定

这是本章最容易被忽视、但一旦踩到就很难定位的一步。

前面 4–12 章为了连真机,我们通常会:

  • export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
  • export CYCLONEDDS_URI=file://.../cyclonedds-xxx.xml(把 DDS 绑到实机网段或某块有线网卡)

到了第 13 章,场景完全反过来:所有节点(包括 Gazebo 内嵌的 ROS 节点)都只在本机互相发现、互相订阅。如果真机的 DDS 绑定还留着,你会遇到一个非常迷惑的现象:

  • Gazebo 启动了
  • robot_state_publisher 也起来了、能看到全部 link
  • spawn_entity.py 一直卡在 Waiting for entity xml on /robot_description,模型永远进不了 Gazebo

原因是 /robot_descriptionlatched / transient_local 话题,需要 DDS 在 QoS 握手时补发历史消息给晚加入的订阅者。真机场景下那份 CycloneDDS 配置,对 Gazebo 内嵌 ROS 节点来说并不总能完成这次握手。

推荐做法:仿真终端里直接 unset,让 ROS 2 回落到默认 FastDDS。

# 开每一个仿真终端前都执行这两行
unset CYCLONEDDS_URI
unset RMW_IMPLEMENTATION

这不是一条"绕过"建议,而是本书验证下来在当前机器上最稳的做法。默认 FastDDS 对 Gazebo 的 latched 话题握手很顺,spawn_entity 能在 1–2 秒内拿到 URDF。

为了避免每次手抖忘记,建议在 ~/go2_sim_ws/ 根目录放一个小启动脚本:

# ~/go2_sim_ws/run_sim.sh
#!/bin/bash
# 仿真专用环境:清掉真机 DDS 绑定,用默认 FastDDS
unset CYCLONEDDS_URI
unset RMW_IMPLEMENTATION
source /opt/ros/humble/setup.bash
source "$HOME/go2_sim_ws/install/setup.bash"
exec "$@"
chmod +x ~/go2_sim_ws/run_sim.sh

之后所有仿真命令都用 ./run_sim.sh ros2 launch ... 的形式跑,就不会再被残留环境变量咬。

为什么不推荐 CYCLONEDDS_URI=file://.../cyclonedds-lo.xml 这条路

之前这一节曾写过一份"只走 lo 的 CycloneDDS 配置"方案,思路是让仿真和真机可以并存切换。但实测下来,这种 loopback-only 配置会让 spawn_entity.py 始终收不到 robot_state_publisher 的 latched 话题,卡在 Waiting for entity xml。想走这条路可以,但要额外调 AllowMulticastPeers 等参数,不在本章射程内。本章的保底方案就是 unset

实现步骤

步骤一:把仿真仓库拉进工作空间

现在我们正式搭环境。

先创建工作空间并克隆聚合仓库:

# 创建教程统一工作空间
mkdir -p ~/go2_sim_ws/src
cd ~/go2_sim_ws/src

# 这一个仓库里已经打包了 CHAMP、Go2 描述包和配置包
git clone https://github.com/anujjain-dev/unitree-go2-ros2.git

这一步为什么我推荐直接用这个聚合仓库,而不是自己手动拼 champ + go2_description + go2_config?

原因很简单:

  • 目录关系已经排好
  • launch 链已经串好
  • 你现在最缺的不是“再多控制一份仓库”,而是先把底座跑起来

接着在工作空间根目录安装依赖:

# 让 rosdep 把仓库声明过的依赖一次装齐
cd ~/go2_sim_ws
rosdep install --from-paths src --ignore-src -r -y

步骤二:先看清这套仓库里谁负责什么

别急着 colcon build。先看明白你后面会改到的关键位置:

  • robots/descriptions/go2_description/xacro/robot_VLP.xacro 负责决定 Go2 模型最终挂了哪些传感器
  • robots/descriptions/go2_description/xacro/velodyne.xacro 负责定义 VLP-16 的 link、joint 和 Gazebo 插件
  • champ/champ_gazebo/launch/gazebo.launch.py 负责 Gazebo 启动、spawn 机器人和控制器加载顺序
  • robots/configs/go2_config/launch/gazebo_velodyne.launch.py 负责把 Go2 + Velodyne + RViz 整体拉起来

这一步虽然只是认目录,但非常重要。因为后面一旦“机器人不站”“点云不出”“控制器没加载”,你得知道去哪个文件里找。

步骤三:把 VLP-16 正式挂到 Go2 模型上

这一章最核心的模型改造,其实就两步:

  1. 在 Go2 的主 xacro 里包含 velodyne.xacro
  2. velodyne.xacro 里把传感器挂到 base_link,并声明 Gazebo 插件

先在主模型里包含 VLP-16

robot_VLP.xacro 的关键就是这一行:

<xacro:include filename="$(find go2_description)/xacro/velodyne.xacro"/>

这意味着这份模型不再是“裸 Go2”,而是“挂了 VLP-16 的 Go2”。

如果你以后想切回 2D 激光模型,也正是在这里把 velodyne.xacro 换成 laser.xacro

再看 velodyne.xacro 里真正发生了什么

下面这段是你最该看懂的部分:

<joint name="velodyne_base_mount_joint" type="fixed">
    <origin rpy="0 0 0" xyz="0.2 0 0.08"/>
    <parent link="base_link"/>
    <child link="velodyne_base_link"/>
</joint>

<joint name="velodyne_base_scan_joint" type="fixed">
    <origin rpy="0 0 0" xyz="0 0 0.0377"/>
    <parent link="velodyne_base_link"/>
    <child link="velodyne"/>
</joint>

它做了两件事:

  • 把雷达基座固定在 base_link 上方前侧
  • 再把真正输出点云的 velodyne link 接到基座上

也就是说,你在 RViz 里看到的 /velodyne_points,并不是“悬浮在世界里的一团点”,而是明确挂在机器人本体上的。

最后是 Gazebo 插件

真正让 /velodyne_points 出来的,是同一个文件里的这段插件配置:

<gazebo reference="velodyne">
  <sensor name="velodyne-VLP16" type="ray">
    <update_rate>10</update_rate>
    <ray>
      <scan>
        <horizontal>
          <samples>1800</samples>
          <min_angle>-3.141592653589793</min_angle>
          <max_angle>3.141592653589793</max_angle>
        </horizontal>
        <vertical>
          <samples>16</samples>
          <min_angle>-0.2617993877991494</min_angle>
          <max_angle>0.2617993877991494</max_angle>
        </vertical>
      </scan>
    </ray>
    <plugin filename="libgazebo_ros_velodyne_laser.so" name="gazebo_ros_laser_controller">
      <ros>
        <remapping>~/out:=velodyne_points</remapping>
      </ros>
      <frame_name>velodyne</frame_name>
    </plugin>
  </sensor>
</gazebo>

这里最关键的不是每个数字本身,而是你要知道:

  • samples=16 对应的是 16 线垂直扫描
  • 输出话题被 remap 到了 /velodyne_points
  • 点云 frame 叫 velodyne

下一章所有 3D SLAM 实验,其实都建立在这个话题契约上。

IMU 插件已经在 Go2 本体里

除了 VLP-16,go2_description/xacro/gazebo.xacro 里还已经把 IMU 插件挂到 imu_link 上了,输出是:

  • /imu/data

这也是为什么我们说仿真环境的“传感器契约更干净”:

  • 雷达有标准 3D 点云
  • IMU 有标准 sensor_msgs/Imu
  • 两者都在 URDF / Gazebo 插件层被明确定义

步骤四:确保控制器按顺序加载

四足机器人在 Gazebo 里最常见的第一类崩法不是点云没有,而是:

  • Gazebo 窗口开了
  • 机器人也 spawn 了
  • 但就是软趴趴地瘫在地上

根因通常是:

controller_manager 还没准备好,你就抢着去加载控制器了。

这里本书沿用的修法是 RegisterEventHandler + OnProcessExit

champ_gazebo/launch/gazebo.launch.py 里的关键逻辑长这样:

load_joint_state_after_spawn = RegisterEventHandler(
    event_handler=OnProcessExit(
        target_action=start_gazebo_spawner_cmd,
        on_exit=[load_joint_state_controller],
    )
)

load_effort_after_joint_state = RegisterEventHandler(
    event_handler=OnProcessExit(
        target_action=load_joint_state_controller,
        on_exit=[load_joint_trajectory_effort_controller],
    )
)

意思是:

  1. 先 spawn 机器人
  2. spawn 完成后再激活 joint_states_controller
  3. joint_states_controller 激活后,再加载 joint_group_effort_controller

这几步顺序不能乱。乱了以后,你最容易看到的报错就是:

Controller 'xxx' not found

或者更讨厌一点,表面没报错,但机器人就是不站。

步骤五:编译整个仿真工作空间

现在可以开始编译了。

我建议第一次就带上两个参数:

  • --symlink-install
  • -DCMAKE_POLICY_VERSION_MINIMUM=3.5

命令如下:

# 编译整个仿真工作空间
cd ~/go2_sim_ws
colcon build --symlink-install \
    --cmake-args -DCMAKE_POLICY_VERSION_MINIMUM=3.5

source install/setup.bash

这条命令里的 -DCMAKE_POLICY_VERSION_MINIMUM=3.5 看着像个小怪招,其实是当时排掉 CMake 兼容问题后留下来的保险丝。

如果你只编这一章的 Gazebo 基础盘,它未必每次都救场;但后面一旦你把更多第三方包拉进同一个工作区,这个参数很容易省你一轮莫名其妙的构建报错。

步骤六:启动 Gazebo + Go2 + VLP-16 + RViz

本章的主入口就是:

# 启动 Go2 + VLP-16 仿真,并顺手打开 RViz
cd ~/go2_sim_ws
./run_sim.sh ros2 launch go2_config gazebo_velodyne.launch.py rviz:=true

默认情况下,这个 launch 会做三件事:

  1. 拉起 champ_bringup,发布机器人描述和控制节点
  2. 拉起 champ_gazebo,进入 Gazebo 世界并 spawn 机器人
  3. 使用 go2_config/rviz/vlp16.rviz 打开一份已经配好点云显示的 RViz

如果一切正常,你会看到:

  • Gazebo 里 Go2 站起来
  • RViz 里有机器人模型
  • /velodyne_points 开始持续刷新

步骤七:用键盘让 Go2 先走起来

本章的目标不是一上来就上导航,而是先确认:

  • 控制链通了
  • 机器人能走
  • 雷达和点云会跟着动

如果你想尽量贴近当前代码,我建议直接用仓库里的 champ_teleop.py,并把它的输出话题改到 /cmd_vel:

# 用仓库里的键盘遥控节点控制仿真中的 Go2
cd ~/go2_sim_ws
./run_sim.sh ros2 run champ_teleop champ_teleop.py \
    --ros-args -p cmd_vel_topic:=/cmd_vel

它的好处是:

  • 本来就是为 CHAMP 四足控制写的
  • 后面你如果想顺手试身体姿态控制,它也已经留好了接口

如果你只想最快验证“能不能走”,也可以直接用标准键盘工具:

# 最小可用方案:标准 Twist 键盘控制
cd ~/go2_sim_ws
./run_sim.sh ros2 run teleop_twist_keyboard teleop_twist_keyboard \
    --ros-args -r cmd_vel:=/cmd_vel

这一章先别急着开 use_slam

你会在 gazebo_velodyne.launch.py 里看到 use_slam 这个参数。先别开。本章只做“仿真底座搭好,Go2 能走能扫”。真正要把 3D SLAM 接上来,放到下一章再做,你会轻松很多。

编译与运行

这一节把前面零散的动作收成一套可直接照抄的顺序。

终端 1:启动仿真

直接走本章前面准备好的 run_sim.sh,它会在同一行里把真机 DDS 绑定清掉、再 source 仿真工作空间:

# 启动 Gazebo + Go2 + VLP-16 + RViz
cd ~/go2_sim_ws
./run_sim.sh ros2 launch go2_config gazebo_velodyne.launch.py rviz:=true

没准备 run_sim.sh?那就每次手动清一次:

# 手动版:每个仿真终端前都要执行
unset CYCLONEDDS_URI
unset RMW_IMPLEMENTATION
source ~/go2_sim_ws/install/setup.bash
ros2 launch go2_config gazebo_velodyne.launch.py rviz:=true

终端 2:启动键盘控制

等 Gazebo 完全加载、Go2 站稳以后,再开第二个终端(同样用 run_sim.sh 包一层):

# 键盘控制,让 Go2 在室内世界里走起来
cd ~/go2_sim_ws
./run_sim.sh ros2 run champ_teleop champ_teleop.py \
    --ros-args -p cmd_vel_topic:=/cmd_vel

如果你不想用仓库里的 teleop,就改成:

# 只用标准 Twist 键盘也可以
cd ~/go2_sim_ws
./run_sim.sh ros2 run teleop_twist_keyboard teleop_twist_keyboard \
    --ros-args -r cmd_vel:=/cmd_vel

一次性回顾启动顺序

第一次上手时,请强迫自己按这个顺序来:

  1. source 工作空间
  2. 再启动 gazebo_velodyne.launch.py
  3. 看 Go2 是否站稳
  4. 看 RViz 是否已经有 /velodyne_points
  5. 最后才启动 teleop

这个顺序看起来保守,其实是在帮你把问题隔离开:

  • Gazebo 没起好,就别怪 teleop
  • 点云没出,就别怪 SLAM
  • 机器人没站稳,就别急着按键乱跑

结果验证

这一章的完成标志很明确:

  • Go2 在 Gazebo 里站起来
  • 键盘能让它前进、转向
  • RViz 能看到随运动变化的 3D 点云

验证一:机器人是否真的站稳

最直观的现象就是 Gazebo 里的姿态:

  • 不趴地
  • 不抽搐
  • 不原地疯狂抖腿

如果你一开场就看到 Go2 像失去灵魂一样瘫着,不要继续往下按键。先回去查控制器加载时序。

验证二:点云和 IMU 是否都在出

打开新终端,抽查两个核心传感器:

# 看 VLP-16 点云有没有在发
cd ~/go2_sim_ws
./run_sim.sh ros2 topic echo /velodyne_points --once

# 看 IMU 有没有在发
./run_sim.sh ros2 topic echo /imu/data --once

你还可以进一步确认点云频率:

# VLP-16 仿真插件默认是 10 Hz
cd ~/go2_sim_ws
./run_sim.sh ros2 topic hz /velodyne_points

验证三:键盘控制是否真的生效

按一次前进或转向键,你应该同时看到三件事:

  1. Gazebo 里的 Go2 开始移动
  2. RViz 里的机器人模型跟着动
  3. 点云位置相对环境发生变化

如果只是 Gazebo 动了,但 RViz 不动,先去查 Fixed Frame 和时间同步。

这就是本章理想中的最终画面:

Gazebo 中的 Go2 站立,并在 RViz 中显示 VLP-16 点云

常见问题

这些坑我建议你按“先看现象,再动手”来排

下面这 8 个坑,是我当时真正花时间的地方。你不一定会全踩,但踩到时基本都很烦。

坑 1: CMake / jsoncpp 报错,构建死在第三方包

现象:你把后续的 liorfsc_pgo 之类包也一起拉进工作区后,colcon build 在 CMake 配置阶段直接炸掉,日志里能看到 jsoncpp、策略版本或查找模块相关错误。

原因:这通常不是 Gazebo 本体坏了,而是某些第三方包的 CMake 写法比较老,在你当前系统上的 CMake 策略版本下不够稳。

解决:

# 先用更保守的 CMake 策略版本重跑
cd ~/go2_sim_ws
colcon build --symlink-install \
    --cmake-args -DCMAKE_POLICY_VERSION_MINIMUM=3.5

如果还是卡在 jsoncpp,再补系统依赖:

sudo apt install -y libjsoncpp-dev pkg-config

对那种老 CMakeLists,可以显式改成:

find_package(PkgConfig REQUIRED)
pkg_check_modules(JSONCPP jsoncpp REQUIRED)
include_directories(${JSONCPP_INCLUDE_DIRS})
link_directories(${JSONCPP_LIBRARY_DIRS})

坑 2: 工作区路径里有空格或中文,xacro 直接炸

现象:xacro 命令报错,模型起不来,有时 Gazebo 里直接空空如也。

原因:很多 launch 文件最终还是要经过 shell 调 xacro,路径里一旦混入空格或中文,最容易在这里翻车。

解决:

  • 最稳的方案:从一开始就把工作区放在 ~/go2_sim_ws
  • 如果你已经把项目放进了带空格的路径,那就给 Command(["xacro '", path, "'"]) 这种写法补上引号

这也是为什么这本书全程都坚持统一工作空间命名,不是洁癖,是被坑过。

坑 3: 路径已经有空格了,怎么办

现象:你不止 xacro 报错,连 mesh 加载、file:// URI、RViz 模型显示都开始抽风。

原因:xacro 只是第一层。后面 mesh URI、Gazebo 资源路径也会继续被空格污染。

解决:最实用的补救不是全仓搜改,而是给工作区做一层无空格软链接:

# 给带空格的工作区做一层干净入口
ln -sfn "$HOME/我的原始路径/项目目录" "$HOME/go2_sim_ws_alias"

然后后续统一从这个无空格路径 source install/setup.bash。这招很土,但真有用。

坑 4: spawn_entity.py 卡在 Waiting for entity xml on /robot_description

现象:Gazebo 起来了、robot_state_publisher 也把所有 link 打出来了,但 spawn_entity.py-7 这一节的日志永远停在:

[spawn_entity]: Loading entity published on topic /robot_description
[spawn_entity]: Waiting for entity xml on /robot_description

模型一直不出现,后续 controller_manager 也没法建立,整条链从这里死。

原因:真机章节里为了连 Go2 实体,通常会设置 RMW_IMPLEMENTATION=rmw_cyclonedds_cpp + 一份绑网卡的 CYCLONEDDS_URI。仿真里 /robot_description 是 latched(transient_local)话题,Gazebo 内嵌的 ROS 节点在当前 CycloneDDS 配置下完不成 QoS 握手,订阅者拿不到已经发布的 URDF。

解决:

# 仿真终端里显式清掉真机 DDS 绑定
unset CYCLONEDDS_URI
unset RMW_IMPLEMENTATION

然后重新 source install/setup.bash 再 launch。细节见本章"仿真开工前:清掉真机遗留的 DDS 绑定"一节,建议直接用那里的 run_sim.sh 包一层。

坑 4.5: Gazebo 起了,ROS2 节点发现整体发抽

现象:不限于 spawn_entity,ros2 topic list 看起来怪怪的、节点偶发失联、topic 订阅率莫名其妙地低。

原因:和坑 4 是同一根源——残留的真机 DDS 配置干扰了仿真节点的发现/QoS。

解决:先按坑 4 的 unset 方式清干净再判断。如果清完还抽,才继续去查防火墙、domain id、多 ROS 版本混装这些方向。

坑 5: 控制器没加载,Go2 软趴趴地躺地上

现象:Gazebo 里机器人 spawn 出来了,但腿不受控,或者日志里有 Controller 'xxx' not found

原因:controller_manager 还没准备好,控制器加载动作就已经发出去了。

解决:

  • 别把几个 ExecuteProcess 并行一丢就完事
  • RegisterEventHandler + OnProcessExit 串联 spawn 和 controller load

这一步是四足仿真里最容易被低估的工程细节之一。

坑 6: contact_sensor 一开,CPU 直接冒烟

现象:仿真实时因子掉得很厉害,contact_sensor 吃掉大把 CPU,整个 Gazebo 肉眼可见卡顿。

原因:默认接触传感器更新频率很高,而四足机器人四只脚一起算,非常容易把机器拖慢。

解决有两条路:

  • 保守修法:把更新频率降到 200 Hz
  • 我最后在当前代码里采用的修法:直接在 champ_gazebo/launch/gazebo.launch.py 里禁用 contact_sensor,然后让 quadruped_controller 在 Gazebo 下继续发布 foot_contacts

当前教程跟的是第二条,因为它和本地代码一致,也最省 CPU。

坑 7: 键盘 teleop 在 Humble 下不好使

现象:按键没反应、首键被吞、布局别扭,或者你明明发了速度,机器人却像没听见。

原因:CHAMP 自带 teleop 的兼容性和默认话题并不总是正好匹配你现在这套链路。

解决:

  • 最简单:用 teleop_twist_keyboard 直接 remap 到 /cmd_vel
  • 更贴当前代码:用仓库里的 champ_teleop.py,并显式传 cmd_vel_topic:=/cmd_vel

记住一句话:这章只需要先让 Go2 在仿真里走起来,别把自己绑死在某一个 teleop 实现上。

坑 8: RViz 里什么都不对,其实只是 Fixed Frame 错了

现象:模型不显示、点云飘、TF 看着断断续续,甚至你以为整条链都炸了。

原因:这一章仿真的参考系应该优先站在 odom 上看,而不是沿用你在别的章节里记住的 basebase_footprint

解决:

  • 打开 RViz
  • Fixed Frame 先设成 odom
  • 再检查 RobotModelPointCloud2、TF 是否一起恢复正常

这一步尤其容易把第 12 章的实机记忆带进来,然后把自己绕进去。

本章小结

这一章我们先做了两件很重要的事。

第一件事,是把“为什么不再继续硬磕 L1 实机 3D 升级”讲明白了。关键证据不是情绪,而是数据:

  • 静态可以做到 ±2 cm
  • 一到动态,低通 1 Hz 会飘到 -98 m
  • 低通 3 Hz 甚至会直接飘到 -557 m

这已经足够说明:问题不在于“再调一两个参数”,而在于这条硬件和软件组合本身就不适合作为稳定的 3D 前端。

第二件事,是我们把一套 Gazebo Classic + CHAMP + VLP-16 的仿真底座搭起来了。现在你已经有:

  • 一台能在 Gazebo 里站起来、走起来的 Go2
  • 一路标准的 /velodyne_points
  • 一路标准的 /imu/data

这块基础盘,就是下一章真正接入 3D SLAM 的出发点。

下一步

现在“能走能扫”的仿真底板已经搭好了。下一章,我们就不再停留在环境搭建,而是正式把 3D SLAM 接进来,看看仿真里的 Go2 到底能不能稳定建图、它又会暴露哪些新的工程细节。

继续阅读:第 14 章 仿真中的 3D SLAM 与 Nav2 升级

拓展阅读