状态:草稿(2026-10-18),实测。 测量于 2026-10-17 在家实验室 Kubernetes 的一个临时 Pod 中进行。节点是一台迷你 PC(Intel Core Ultra 系列,22 线程,内存 31 GiB),Pod 的资源上限为 CPU 3 核、内存 4 GiB,且未分配 GPU 和
/dev/dri(无特权,无附加 capability)。镜像使用了ros:jazzy(ros-base,Ubuntu 24.04.5),并通过 apt 添加了ros-jazzy-ros-gz·ros-jazzy-turtlebot3-gazebo。版本分别为 Gazebo Sim 8.15.0 (Harmonic)、ros_gz 1.0.24、turtlebot3_gazebo 2.3.7。重复次数为 B 组 2 次、C 组 3 次,其余 1 次,表中的数值为每次运行测量一次的结果(非中位数)。原始数据(rosbag2 mcap、汇总 JSON、1 秒间隔的 cgroup 采样)位于工作目录的 ROS 测量文件夹中,文中仅转录了隐藏了节点名称和地址的值。
目录
- 1. 直觉:闭着眼睛数步数的机器人
- 2. 架构:单个 Pod 内的无界面仿真
- 3. 四种世界
- 4. 行驶节点:看 odom 而非时间前进
- 5. 结果:保持实时的代价与角落误差
- 6. 漏掉一行地面摩擦力设定时
- 7. 在没有 GPU 的情况下运行激光雷达
- 8. 可用于 CI 的最大速度模式
- 9. 遇到的问题
- 10. 局限性
- 11. 下期预告
- 参考
1. 直觉:闭着眼睛数步数的机器人
人们很容易相信,闭着眼睛连续四次“向前走十步,向左转 90 度”就能回到起点,因为步数数得很准。然而,如果地面很滑,少走了一步;或者转弯时脚打滑了,只转了 80 度而不是 90 度。只数步数的人是不会知道这些差异的。
轮式机器人的 里程计(odom) 就是这种数步数的方式。它通过对车轮转动的圈数进行积分,来宣称“我现在在这里”。而仿真器则额外提供了一样东西:物理引擎所知道的 真实位置(真实姿态) 。本文通过实际测量展示了并排记录这两个值时会看到什么。
2. 架构:单个 Pod 内的无界面仿真
gz sim -s -r仅启动了服务器。由于没有界面(GUI),因此不需要 X 服务器或 VNC。ros_gz_bridge连接了 Gazebo 主题和 ROS 2 主题。在 TurtleBot3 默认使用的桥接配置中添加了一行,将 Gazebo 的/world/default/dynamic_pose/info导出为名为/gt_poses的话题。这就是真实姿态。ros2 bag record -s mcap将/odom,/cmd_vel,/gt_poses,/imu(带有激光雷达的世界还包括/scan)保存在一个文件中。- 为了防止 DDS 发现机制泄漏到 Pod 网络之外,使用了
ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST进行隔离。
还测量了镜像成本。 ros:jazzy 压缩后为 283 MiB,下载耗时 18.3 s。在此基础上通过 apt 添加仿真软件包,下载了 768 个软件包、554 MB,安装后体积增加了 2,637 MB,耗时 272 s。顺便一提,仅从注册表清单来看, osrf/ros:jazzy-desktop 压缩后为 1,203.9 MiB, jazzy-simulation 为 698.3 MiB(未下载)。如果觉得每次安装的时间很可惜,下一步的工作就是将这个阶段固化为镜像。
3. 四种世界
| 名称 | 有什么不同 |
|---|---|
| A | TurtleBot3 的原始空白世界。由于包含 Sensors 系统(ogre2)以及需要渲染的 gpu_lidar ,因此使用 --headless-rendering 启动。 |
| A2 | 在 A 的基础上放置了两个静态箱子。因为在空白世界中,激光雷达的值全部为无穷大,无法得知渲染是否正常运作。 |
| B | 去掉了 Sensors 系统。地面为内联平面,摩擦力与原始地面相同,即 mu 100,mu2 50。不使用渲染,也不使用网络。 |
| C | 将 B 中的 real_time_factor 改为 0,不绑定实时性,以最大速度运行。 |
4. 行驶节点:看 odom 而非时间前进
行驶节点不是“向前行驶 5 秒”这种开环控制。它是通过查看 /odom 依次寻找角点 (1,0)、(1,1)、(0,1)、(0,0) 的闭环控制,最大速度为 0.2 m/s,最大角速度为 0.6 rad/s,基于仿真时间的频率为 50 Hz。它必须采用与真实机器人航点行驶相同的方式,这样“相对于目标的误差”才能被理解为控制器、里程计和物理特性综合作用的结果。核心是直线行驶阶段的几行代码。
if self.phase == 'drive':
tx, ty = CORNERS[self.leg]
dx, dy = tx - x, ty - y
# 진행 방향 위로 남은 거리. 지나치면 음수가 된다.
seg_heading = HEADINGS[self.leg - 1] if self.leg else 0.0
along = dx * math.cos(seg_heading) + dy * math.sin(seg_heading)
if along <= POS_TOL: # 5 mm 안이면 멈추고 돌기 시작
self.send(0.0, 0.0)
self.mark('corner')
self.phase = 'turn'
return
bearing = math.atan2(dy, dx) if math.hypot(dx, dy) > 0.05 else seg_heading
w = max(-W_MAX, min(W_MAX, 2.0 * wrap(bearing - th)))
v = max(0.02, min(V_MAX, 0.8 * along))
self.send(v, w)
(摘自 TT Lab 实验代码 square_driver.py ,注释根据文章进行了精简或补充。旋转目标为每个角点指定了绝对方位 0、π/2、π、−π/2,以避免累积旋转误差。)
5. 结果:保持实时的代价与角落误差
| 运行 | RTF | 一圈墙钟时间 | 角落实际误差 mm (1→4) | 终点位置实际误差 | odom↔实际最大差 | CPU 平均 | anon 最大值 | gz RSS |
|---|---|---|---|---|---|---|---|---|
| A 渲染激光雷达 | 1.000 | 47.5 s | 11, 33, 40, 25 | 16 mm | 41 mm | 0.91 核 | 467 MiB | 515 MiB |
| A2 A + 箱子 | 1.000 | 47.5 s | 11, 47, 66, 43 | 50 mm | 74 mm | 0.92 核 | 466 MiB | 509 MiB |
| B 仅物理 | 1.000 | 47.6 s | 17, 23, 14, 5 | 16 mm | 26 mm | 0.73 核 | 196 MiB | 156 MiB |
| B 重复 | 1.000 | 47.5 s | 15, 33, 28, 13 | 16 mm | 32 mm | 0.74 核 | 196 MiB | 未测量 |
| C 最大速度 | 11.11 | 4.3 s | 19, 27, 20, 7 | 16 mm | 39 mm | 2.88 核 | 196 MiB | 156 MiB |
| C 重复 2 | 11.76 | 4.1 s | 20, 28, 20, 8 | 17 mm | 39 mm | 2.54 核 | 196 MiB | 未测量 |
| C 重复 3 | 10.88 | 4.4 s | 19, 28, 20, 7 | 16 mm | 39 mm | 2.86 核 | 196 MiB | 未测量 |
RTF(实时因子)是 /odom 序言中的仿真时间增长量除以记录器接收到的墙钟时间增长量。CPU 是通过每秒读取一次 Pod cgroup 的 cpu.stat 来计算行驶区间的平均值,内存则是 memory.stat 的 anon 最大值。
memory.current 显示为 3.0–3.6 GiB,但其中大部分是在 apt 安装期间产生的页面缓存,因此实际持有的内存以 anon 为准。
- 以 odom 为标准,所有运行都几乎是完美的。 角落误差为 4.6–5.0 mm(停止容差 5 mm),旋转角度为 89.74–89.78°(容差 0.005 rad)。表中列出的实际误差几乎全部是 odom 与实际情况之间的差值。
- 每次在原地旋转 90° 时,实际位置会平移 12–13 mm(B 运行,odom 小于 0.3 mm)。这被认为是由于万向轮拖拽导致的,但未经证实(推测)。
- 启动时间从执行
gz sim到第一个/odom为 3.6–3.7 s,只有 A 的第一次运行由于在下载原始地面模型而花了 5.4 s。
6. 漏掉一行地面摩擦力设定时
在最初创建无渲染世界 (B) 时,将原始的地面模型更改为内联平面,却忘记编写摩擦力设置。此时将使用物理引擎 (ODE) 的默认值 mu 1。修改后的地面包含下面这一行。
FRIC = '<surface><friction><ode><mu>100</mu><mu2>50</mu2></ode></friction></surface>' # 원본 바닥과 같게
下图记录了相同的行驶节点在两个世界中运行的情况。点击图片可以 4 倍速回放 rosbag 中记录的实际轨迹。
| 运行 | 每转实际角度 | 角落实际误差 mm (1→4) | 终点位置实际误差 / 方向 | odom 看到了终点位置误差 |
|---|---|---|---|---|
| B(有摩擦力) | 89.2, 89.7, 89.7, 89.7° | 17, 23, 14, 5 | 16 mm / −0.98° | 5 mm |
| B 漏掉摩擦力 | 80.3, 79.7, 80.3, 80.9° | 10, 138, 344, 487 | 503 mm / −37.99° | 5 mm |
| C 漏掉摩擦力 | 78.0, 79.2, 80.5, 80.9° | 19, 164, 399, 533 | 549 mm / −40.24° | 4 mm |
在原地旋转时,车轮发生打滑,只转了大约 80° 而不是 90°,这种偏差在每个角点都累积了下来。然而,由于 odom 是对车轮关节角度进行积分的,因此它无法检测到打滑。因此,odom 一直到最后都报告说“在 5 mm 范围内返回”。如果仅凭 odom 来判断结果,这个世界将顺利通过测试。
我们还单独验证了真实姿态是否正确。运行结束后,将通过 gz model -m burger -p 查询到的位置与 /gt_poses 的最后一个值进行了比较。在漏掉摩擦力的 C 运行中, gz model 返回了 (−0.2505, 0.4883) m,方向旋转了 −40.0°,而 /gt_poses 为 (−0.2505, 0.4884) m,−40.2°。在有摩擦力的 B 中,这两个值完全相同,为 (−0.0130, 0.0099) m。
教训。 在仿真中评估机器人性能时,应将机器人自我估计的位置与仿真器的真实位置 同时 记录下来,并以真实位置作为评估标准。如果只看估计位置,物理设置上的失误会被误认为是机器人的成功。相反,如果只看真实位置,就无法得知估计器有多大程度的偏差。两者的差异(图中的红线)就是里程计漂移,这就是为什么在真实机器人中需要单独配备位置估计器的原因。
7. 在没有 GPU 的情况下运行激光雷达
A 和 A2 使用通过渲染来测量距离的 gpu_lidar(360 线,5 Hz)。Pod 中既没有 GPU 也没有 /dev/dri ,但它通过 --headless-rendering(EGL)启动并保持了 1.000 的 RTF。
/scan周期为 5.00 Hz,与模型中写入的 update_rate 5 完全一致。- 在 A2 中,最后五次扫描的 1,800 个光束中有 185 个是有限距离。这意味着它确实看到了箱子。空白世界 A 的结果为 0/1,800(全部为无穷大),属于正常现象。
- 渲染成本(A 和 B 之间的差异)为:CPU +0.18 核,anon 内存 +270 MiB,gz 进程 RSS 从 156 增加到 515 MiB。
- 日志中只有加载了 ogre2 渲染插件的一行,没有显示渲染器的名称。由于没有 GPU,推测它是用 Mesa 的软件渲染器进行绘制的,但未能证实(未证实)。
- 未测量相机传感器(其分辨率和频率要高得多)(未实测)。单台激光雷达能用并不意味着相机也能用。
8. 可用于 CI 的最大速度模式
C 是将 real_time_factor 设为 0 的 B。沿着 1 米正方形行驶一圈(仿真时间 47.7–51.7 s)在 4.1–4.4 s 的墙钟时间内完成,RTF 为 10.88–11.76。
- 这个速度是针对 CPU 上限 3 个核心测得的值(平均 2.54–2.88 个核心)。物理单线程与 bridge、logger、driving node 共享了上限。我们没有测量改变核心数时 RTF 是如何变化的(未测量)。
- 可重复性反而变好了。 运行三次 C 的角落误差在 1 mm 以内保持一致(19–20、27–28、20、7–8 mm)。如果运行两次实时模式 B,最后的角落误差分别为 5 mm 和 13 mm。 我们认为在实时模式下,与墙上时钟同步的过程每次都会带来微小的差异(推测)。
- 这里有一个需要注意的地方。在 C 中,
dynamic_pose是以墙上时钟为基准输出的,真值位姿的仿真时间间隔会变得比较稀疏,约为 12 Hz。在按时间对齐真值与 odom 时需要进行插值。
这意味着可以通过每个 PR 运行一个 4 秒的测试,来验证“机器人能否在 2 cm 以内绕完 1 m 的正方形”。不过,该判定必须像第 6 节那样基于真值位姿来进行。
9. 遇到的问题
set -u在开启的 shell 脚本中读取/opt/ros/jazzy/setup.bash会因为未定义的变量而停止。- 在没有作业控制的脚本中,通过
&启动的ros2 bag record会忽略 SIGINT。我们将其改为了 SIGTERM,rosbag2 在收到 SIGTERM 时也能正常关闭文件。 ros2 run即使结束了 wrapper,parameter_bridge的子进程仍然残留,因此我们直接运行了二进制文件。Pose_V将tf2_msgs/TFMessage通过 bridge 桥接时,frame_id、child_frame_id 和时间戳会为空。第一项是机器人模型这一点是通过与gz model -p对比确认的,时间戳则对齐为了 logger 接收到的时间。- odom 周期以仿真时间为准测量为 50.0 Hz。模型 SDF 的
odom_publisher_frequency是 30,但我们没有去查证产生差异的原因(未确认)。
10. 局限性
- 单台节点,每次运行仅有一两次。重复次数太少,不足以说明误差的分布情况。
- 只观察了一种轮式机器人(burger)和平坦地面。未涉及坡度、台阶以及其他地面材质。
- 万向轮拖曳假说(每转移动 12–13 mm)可以通过改变万向轮摩擦力的实验来区分,但目前尚未进行。
- GPU 节点上的对比以及相机传感器尚未测量。
11. 下期预告
下一篇将转向乒乓球机器人。我们将放弃轮子,把一个球放到 MuJoCo 中,测量反弹和旋转受模拟器单行配置的影响有多大。 机器人手臂本身的基础知识(坐标系、逆运动学)将在同步连载的“乒乓球机械臂基础”系列(目录)中进行介绍。
Titi 的小贴士。 如果只数步数,我总是回到了原地。但如果地面很滑,实际上我可能站在半米开外。所以在模拟器里,一定要把“真正的所在位置”也一起记下来哦!
参考资料
所有链接均已于 2026-10-18 验证可正常打开。版本均为测量时所使用的版本。
- ROS 2 Jazzy 文档: https://docs.ros.org/en/jazzy/
- Gazebo Harmonic 文档: https://gazebosim.org/docs/harmonic/
- ros_gz (bridge, 1.0.24): https://github.com/gazebosim/ros_gz
- TurtleBot3 仿真 (turtlebot3_gazebo 2.3.7): https://github.com/ROBOTIS-GIT/turtlebot3_simulations
- rosbag2: https://github.com/ros2/rosbag2, MCAP 格式: https://mcap.dev/spec
- 官方镜像
ros:jazzy: https://hub.docker.com/_/ros - 引用的代码全部为 TT Lab 实验代码。