我带少年创客营已经第八天了。Day 8的主题只有一个:让 ROS2 机器人自己找到路。说实话,这是整个营里最让人紧张也最兴奋的半天——前面几天我们学会了让小海龟转圈、写话题节点、用服务控制机器人、用动作执行长时间任务,但导航不一样,它把前面学的所有东西一次性串了起来。这篇文章不是官方教程,而是我们第 8 天的完整复盘,里面有教学设计、现场命令、踩坑记录,还有给孩子们准备的引导话术,适合正在带学生做 ROS2 项目的老师、想陪孩子玩机器人的家长,以及自学导航但总被“定位”和“路径规划”绕晕的人。
1. 课程走到第 8 天,为什么把导航当成重点
1.1 前 7 天的基础积累
我们这个营的学员年龄在 11 到 16 岁之间,大部分之前只玩过 Scratch 这类图形化编程,真正接触 Python 和命令行的很少。所以在入营前 7 天,课程安排得非常“碎”,每天只解决一个小问题,但每个问题都是后面导航的零件。
| Day | 主题 | 核心内容 | 孩子们当时的状态 |
|---|---|---|---|
| Day 1 | 操作系统与 ROS2 安装 | Ubuntu 22.04 + ROS2 Humble,了解 ros2、source、工作区 |
装到一半想放弃,但成功 launch 小海龟后全部真香 |
| Day 2 | 小海龟初体验 | ros2 run turtlesim turtle_teleop_key,话题订阅 /turtle1/cmd_vel |
争着遥控小乌龟,顺便理解了“发布者和订阅者” |
| Day 3 | 话题 Topic | 自己写 Python 节点发布坐标,键盘控制机器人 | 第一次觉得“我在编程”,虽然只是 copy 改参数 |
| Day 4 | 服务 Service | 用服务请求让小乌龟画正方形 | 明白了请求-响应模型,和话题的区别 |
| Day 5 | 动作 Action | 用 action 接口控制模拟机械臂执行“抓取-放置” | 开始接触“目标”“反馈”“结果”三层概念 |
| Day 6 | URDF 建模 | 用 URDF 给机器人搭身体,在 rviz2 里看到模型 | 对“坐标系”有了直观认识,知道 base_link 是什么 |
| Day 7 | Gazebo 仿真 | 把 URDF 放进 Gazebo,加入激光雷达、差速驱动 | 已经能看懂 / odom,知道车是靠 /cmd_vel 动的 |
到 Day 7 结束,孩子们其实已经掌握了 ROS2 最核心的通信机制:话题、服务、动作。Day 8 如果继续讲一个新概念,比如 TF2 进阶或者插件开发,可能就劝退了一半人。但导航不一样,它是把话题、服务、动作、URDF、Gazebo 全部串起来的一个完整项目,而且结果可视化程度极高——机器人有没有到达目标点,一眼就能看出来。
1.2 导航为什么是少年营最好的“毕业项目”
我在设计课程时有一个原则:每一个大项目,必须让孩子在 40 分钟内看到一次“完整的成功”。导航完美满足这个条件。它由五个环节组成:
- 机器人通过激光雷达感知周围障碍物,这是话题通信;
- AMCL 不断比较当前扫描和已知地图,输出机器人位姿,这背后是很多服务调用;
- 导航任务本身是一个长时间执行的动作,符合 Action 的模型;
- 机器人最终通过
/cmd_vel话题接收速度指令,完成移动; - 整个过程在 rviz2 里可视化,绿色路径、蓝色箭头、代价地图层层展开。
从行业角度看,移动机器人导航是仓储物流、巡检机器人、服务机器人里最核心的能力,青少年学完之后可以直接迁移到真实项目上。而且它不像机械臂控制那样需要太多数学基础,只要把“我在哪、要去哪、怎么去”这三件事讲清楚,再用仿真验证,孩子们就能理解移动机器人工作的全貌。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 给孩子们讲清楚导航三件事
2.1 我在哪:定位
开讲之前我先问了一个问题:“你闭着眼睛在教室里走五步,还能准确说出自己站在哪吗?”孩子们说不能。我说机器人也一样,它只知道轮子转了多少圈,这就是里程计。里程计在短时间挺准,但时间一长,轮子打滑、地面不平,误差就越来越大,机器人以为自己在一个位置,实际上早就偏了。
ROS2 里解决这个问题的是 AMCL,全称是“自适应蒙特卡洛定位”。名字听着吓人,但本质可以理解成:机器人手里抓着一把“猜测粒子”,每个粒子是一个可能的位姿(位置 + 朝向)。激光雷达每扫描一次,AMCL 就把这些粒子跟已知地图对一遍,跟地图匹配得好的粒子留下并复制多一点,匹配不好的粒子扔掉。反复几次,粒子们就聚成一团,机器人的位置就锁定了。
在 rviz2 里,孩子们能看到一团绿色箭头代表粒子。他们最喜欢干的事是什么?就是在地图另一个位置故意发一个错误的初始位姿,然后看粒子怎么“挣扎”着收敛到真实位置。这个过程比任何公式都直观。
2.2 我的世界长什么样:地图
导航需要一个“已知世界”,也就是地图。Day 8 我们用的地图有两种获取方式:一种是用现成的静态地图,另一种是用 SLAM 现场建图。我的建议是先把建图和导航分开处理,不要一上来就聊“SLAM 是什么”,而是先让孩子们用 SLAM 跑一遍,看到地图慢慢“画”出来,然后再回头解释原理。
SLAM 的英文是 Simultaneous Localization and Mapping,中文叫“同时定位与建图”。它做两件事:一边画地图,一边判断自己在地图的哪个位置。这有点像一个人蒙着眼走进一间空房间,用双手摸墙,一边摸一边在脑海里画房间的平面图,同时还要根据摸到的墙来判断自己走到了哪。
我们生成的是二维栅格地图,术语叫 OccupancyGrid。可以想象成一张像素画,每个格子有三种状态:自由、占据、未知。自由是白色,可以走;占据是黑色,不能走;未知是灰色,还没探测到。ROS2 地图的标准格式是 .pgm 图片 + .yaml 配置文件,pgm 存格子颜色,yaml 存分辨率、原点、栅格尺寸这些参数。很多孩子第一次打开地图 yaml 文件时,看到 resolution: 0.05 还不太理解,我解释为“每个格子边长 5 厘米,一平方米的地面由 400 个格子组成”,他们立刻就懂了。
2.3 我怎么过去:路径规划与代价地图
定位和地图解决的是“我在哪”和“世界长什么样”,接下来就是“怎么过去”。孩子们一开始觉得这很简单:两点之间直线最短,直接朝目标开不就行了?如果真是个空房间确实可以,但房间里有墙、有桌子、有不知从哪里冒出来的箱子,机器人必须能绕开。
ROS2 导航栈(Nav2)里有两个规划器:全局规划器和局部规划器。全局规划器在已知静态地图上找一条从当前位置到目标点的路径,给出一条大方向的“粗路线”;局部规划器根据实时激光雷达数据,把这条粗路线拆成一条条速度指令,发给 /cmd_vel。我举了个例子:全局规划就像高德地图先告诉你“从 A 到 B 走哪条路”,局部规划就像你真正走在路上,前面突然出现一个坑,你会临时绕半步,然后再回到原路线。
代价地图是理解导航的关键,它的作用是把障碍物“膨胀”一圈,给机器人留出安全距离。就像一个怕磕碰的人走路时会自觉离墙远一点。代价地图分两层:静态层来自已知地图,障碍物层来自激光雷达实时扫描,膨胀层负责把障碍物边缘往外扩。如果膨胀半径设得太小,机器人会贴着墙走,很吓人;如果设得太大,窄的走廊可能直接被认为是不可通行,路径规划就会失败。
3. 实操记录:从建图到自主导航的完整链路
3.1 环境准备与仿真世界
Day 8 上午,我们统一使用 TurtleBot3 Burger 仿真包。为什么不直接用实体车?最早我也买过两台入门机器人小车,结果上课光排队充电、抢网、换轮子就用掉半小时,教学节奏全乱了。仿真环境最大的好处是每个人都能上手,且不会撞坏机器。TurtleBot3 在 ROS2 里是最成熟的入门平台,配套教程齐全,参数合理,后续迁移到真车也非常方便。
先确认环境。我们的电脑是 Ubuntu 22.04 + ROS2 Humble,需要安装三个包:
bash复制sudo apt install ros-humble-turtlebot3-gazebo ros-humble-turtlebot3-cartographer ros-humble-turtlebot3-navigation2
然后告诉终端我们用的是 Burger 车型:
bash复制echo "export TURTLEBOT3_MODEL=burger" >> ~/.bashrc
source ~/.bashrc
这里有个常见的坑:如果忘记设置 TURTLEBOT3_MODEL,launch 文件会直接报错或不显示模型。我让每个孩子开营前都检查一遍这个环境变量。
3.2 用 SLAM 建图
建图需要开三个终端,每个终端先 source /opt/ros/humble/setup.bash(如果写入了 .bashrc 就不用)。
终端 A 启动仿真世界:
bash复制ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py
终端 B 启动 Cartographer SLAM:
bash复制ros2 launch turtlebot3_cartographer cartographer.launch.py use_sim_time:=True
终端 C 启动键盘遥控:
bash复制ros2 run turtlebot3_teleop teleop_keyboard
然后孩子们轮流拿到键盘,把机器人从起点开出去,把整个房间的墙、柱子、箱子全部“扫”进地图。这里有一个我反复强调的建图要领:速度要慢,转弯要小,键盘要“点按”而不是“长按”。如果机器人转得太猛,激光雷达的扫描角度覆盖不过来,地图就会变成一团糊;如果速度太快,Cartographer 的实时位姿估计跟不上,地图边界会出现重影。每个孩子开三分钟,旁边同学负责盯着 rviz2 里的地图看有没有“糊”,一有不对就喊停。
3.3 保存和加载地图
建图完成后,地图只是存在于内存里,我们需要把它保存到文件。用 Nav2 自带的 map_saver:
bash复制ros2 run nav2_map_server map_saver_cli -f ~/map/my_map
执行完会生成两个文件:my_map.pgm 是图片,my_map.yaml 是配置。我让每个组把 yaml 打开看一眼,里面大概是这样:
yaml复制image: my_map.pgm
resolution: 0.05
origin: [-2.0, -1.5, 0.0]
negate: 0
occupied_thresh: 0.65
free_thresh: 0.25
origin 是地图左下角在世界坐标系中的位置,resolution 是每个像素代表的米数。我顺手给他们留了一个思考题:如果把这个 0.05 改成 0.1,地图会变得更清楚还是更模糊?放到真机上,地图文件占用会变大还是变小?这个问题在下午总结时揭晓:分辨率变大后单个格子边长变为 10 厘米,细节丢失,地图文件占内存更小,但机器人通过窄门时更容易误判。
加载地图并启动导航的方式很简单:
bash复制ros2 launch turtlebot3_navigation2 navigation2.launch.py map:=$HOME/map/my_map.yaml use_sim_time:=True
3.4 启动导航并让机器人到达目标点
启动后 rviz2 会自动加载 Nav2 的界面,这时地图会显示出来,但机器人还不知道自己在地图的哪个位置,所以第一步是手动告诉它。
在 rviz2 顶部点击 2D Pose Estimate,然后在地图上机器人模型所在的位置点击并拖出一个箭头,箭头的方向就是机器人的朝向。这个动作非常重要,它相当于给 AMCL 一个初始猜测。如果这里胡点一个位置,机器人会以为自己在地图另一端,后面的导航会非常难看。
然后点击 Nav2 Goal 在地图上设置一个目标点,机器人就会开始规划并移动。孩子们第一次看到这个流程时,基本都是“哇”的一声。绿色路径是全局路径,红色点云是激光雷达实时扫描到的障碍物,浅色区域是膨胀层。我们的任务是让每个小组都成功跑通一次从起点到黄色柱子的导航,然后再开始折腾参数。
4. 现场调试:踩过的坑全记录
4.1 坐标系与 TF 树问题
Day 8 上午的第一个经典报错是 rviz2 左下角冒出红色错误:No transform from [map] to [base_link]。出现这个的时候,地图显示不出来,机器人模型也消失。原因很简单:导航要靠 TF 树知道每个坐标系之间的关系。完整的树长这样:
code复制map -> odom -> base_footprint -> base_link -> laser
map是地图坐标系,固定不动;odom是里程计坐标系,由轮式编码器推算出来,存在漂移;base_link是机器人本体的坐标系;laser是激光雷达坐标系。
如果某个坐标系没有发布,后面的所有环节都断掉。排查思路很固定:先用 ros2 run tf2_tools view_frames 生成当前 TF 树图,看看哪条边断了;再检查 robot_state_publisher 和 joint_state_publisher 有没有在跑;最后用 ros2 topic hz /odom 看看里程计发布频率是不是 0。绝大多数情况是某个 launch 没有正常启动,不是代码问题。
4.2 定位不准或初始位姿设置错误
有组孩子设置初始位姿的时候,把箭头方向拖反了 90 度,机器人一启动就斜着冲出去,然后开始原地转圈,全场爆笑。这其实不是故障,AMCL 会发现激光扫描和地图对不上,然后不断调整粒子分布,最终还是会收敛,但收敛过程对孩子来说就是“失控”。所以我都会提醒:设置初始位姿时,箭头方向必须和机器人模型在地图里的朝向一致,误差超过 45 度就可能卡在“虚假定位”里。
如果发现 AMCL 收敛很慢,可以修改参数。Nav2 的 AMCL 配置一般在 nav2_params.yaml 里,核心参数是:
yaml复制amcl:
ros__parameters:
min_particles: 200
max_particles: 1000
update_min_d: 0.2
update_min_a: 0.2
默认的粒子数量是 100~5000,对 11 岁的孩子来说太费 CPU,调低到 200~1000 在仿真里完全够用,收敛速度会明显变快。update_min_d 和 update_min_a 表示机器人至少移动 0.2 米或转 0.2 弧度才重新计算粒子权重,这是为了减少无谓计算。如果机器人停着不动但粒子还在抖动,通常就是这个参数设得太小。
4.3 路径规划乱窜或找不到路径
下午有个小组把目标点设在了墙壁里面,机器人先是直直地朝墙开过去,然后原地打转,最后干脆报“未找到路径”。这个现象特别适合解释一个概念:Nav2 的目标点必须放在代价地图的“可通行区域”里,不能点在障碍物内部。我教孩子们把鼠标移到地图上,看到目标点周围有浅色膨胀层覆盖的地方就别点,点到干净白色区域再放手。
如果目标点没问题但机器人还是绕远路,这通常是膨胀层参数的问题。Nav2 参数里有两个数值很关键:
inflation_radius:障碍物向外膨胀的半径,默认 0.55 米,大于这个距离的地图格子会被标记为“代价递增”;cost_scaling_factor:代价衰减系数,值越小,障碍物影响范围传播得越远,路径会离障碍物越远。
把 inflation_radius 从 0.55 调到 0.8,机器人会自动离墙更远一点,路径看起来更“从容”。但如果调到 1.2 以上,窄走廊会被完全堵死,反而找不到路径。这是典型的“过犹不及”。
4.4 机器人乱撞与代价地图参数
另一个非常有教学价值的坑:我们在仿真世界里放了一个小纸箱,但纸箱没有提前出现在静态地图里。机器人导航经过它时,先是正常前进,然后突然急停,再退半步,又试一次,最后还是撞了上去。其实激光雷达是能看到纸箱的,问题出在代价地图的障碍物层没有及时更新。
排查时用三个命令逐步确认:
bash复制ros2 topic echo /scan --once
ros2 topic list | grep costmap
ros2 topic hz /scan
如果 /scan 有数据但代价地图没反应,就在 rviz2 里添加一个 Map 显示,选择 local_costmap,切到 Color 和 Costmap 话题,看看膨胀区域是否出现。最后我发现是总有人关掉了 Gazebo 窗口导致仿真时钟暂停,/scan 的发布频率掉到了 0,代价地图的 obstacle layer 认为激光雷达“死了”,就不更新障碍物了。解决方法是把 Gazebo 窗口最小化而不是关闭,同时重启一下导航 launch。
4.5 性能不足与资源受限优化
营里有一部分旧台式机,跑 Gazebo + Cartographer + rviz2 三件套时风扇狂转,画面也卡。优化思路和真实机器人上遇到资源受限时是一样的:
- 关掉 rviz2 的点云显示,只保留地图和路径;
- 把 AMCL 的粒子数从 5000 降到 1000;
- 设置
use_sim_time但不要让 Gazebo 和 rviz2 同时开最高画质; - 如果条件允许,把分辨率从 0.05 调整到 0.1,地图扫描开销会少一半。
这个经验对将来上树莓派这类低算力设备很有用。真机上不能老想着“加内存换 CPU”,很多时候通过降频率、降分辨率、降粒子数就能跑起来,只是精度会牺牲一点。我给高年级的孩子留了一个额外任务:亲手把地图分辨率改成 0.1,重新跑一遍导航,结果发现机器人虽然对窄通道的判断变差了,但在空旷区域完全够用。这个体验比讲十个公式都有用。
5. 少年创客营现场:分组任务、翻车与惊喜
5.1 分组任务设计
下午我们把营员分成了 4 组,每组一台电脑,比赛同一个地图。任务分三个等级,每组必须至少完成前两个:
- 青铜任务:让机器人从起点开到地图中央的黄色地垫区域,不撞墙、不停顿超过 30 秒;
- 白银任务:机器人要依次经过地图上的两个固定坐标点(一个在走廊尽头,一个在柱子旁边),最后停在出口;
- 黄金任务:在迷宫里找到一条从起点到终点的路径,到达后原路返回起点。
每组选一个“导航指挥官”负责设置目标点,一个“实验记录员”负责记录机器人的路径轨迹,其他成员负责“监察”——判断机器人有没有偏离预期路线。这个设计的好处是每个人都有明确任务,不会出现一个孩子操作、其他人在旁边围观的情况。而且“导航指挥官”必须学会看代价地图,因为目标点不能点在障碍物里,这个限制本身就让孩子开始关注地图语义。
5.2 现场“翻车”案例
记录一下最经典的几个翻车瞬间,以后做课程复盘可以直接用:
第一个案例。有一组孩子把目标点设在了墙壁里面,机器人对着墙笔直冲过去,撞上之后开始原地打转,全场笑得前仰后合。我没有直接让他们改,而是问:“机器人为什么觉得能穿墙?”孩子们想了想,发现是因为目标点周围没有膨胀层覆盖,机器人不知道自己“不能去”。这个问答比讲十遍代价地图理论都有用。
第二个案例。有个组的大哥急了,遥控建图时一直按着前进键不放,小车在走廊里高速狂奔,建出来的地图整个错位。我现场让所有人对比了这两组地图:慢速建图的地图线条清晰,高速建图的地图像喝醉了一样模糊。孩子们自己总结出了结论:建图要慢、要稳。这个过程花了五分钟,但效果比老师喊十遍“注意安全”都好。
第三个案例,也是最常见的。初始位姿方向放反了,机器人在定位还没收敛之前先朝反方向冲出去,然后 AMCL 不断修正,最后才回到正确路线。有个孩子说“机器人刚才好像喝醉了”,我顺势解释“这就是定位误差在收敛”。
5.3 孩子们最惊喜的三个瞬间
营里最有价值的是那些“哇”的瞬间,它们证明孩子真的建立起了直觉。
第一个瞬间。机器人从起点出发,先规划出一条绕远的大路,走到一半,另一个组的同学在 Gazebo 里把路上的箱子搬走了,机器人立刻重新规划,改走一条更近的路线。孩子们第一次看到“路径是动态的”,不是提前写死的,而是每时每刻根据世界状态重新计算。这个体验让我之前讲的“局部规划器会实时避障”变成真的了。
第二个瞬间。有个组故意把初始位姿设在地图的错误角落,比如本来机器人在地图中央,他们偏要在右下角点一下。按我们的预期,AMCL 会慢慢修正,但修正过程的粒子会先散开再聚拢,看起来非常奇妙。孩子们盯着绿色箭头从一小团散成一大片,再逐渐聚拢到真实位置,整个过程像魔术。有人问“这些箭头为什么会自己回来”,我只回答了一句“因为激光扫描到的墙壁和地图对上了”。
第三个瞬间。两个小组同时用同一张地图导航,目标点正好是同一个地方,两台机器人几乎同时到达,过程中还互相让了路。这个场面让我顺势抛出了“多机器人协同”的概念:如果两台以上机器人要在同一张地图里跑,怎么保证不撞车?孩子们马上开始了各种猜测,而这正是 Day 9 可以深入的内容。
6. 从 Day 8 到未来:进阶路线
6.1 从仿真到实体机器人
Day 8 结束后,问得最多的问题是“能不能把代码放到真车上”。答案是“能,且很顺滑”。实体机器人其实只是把 Gazebo 这个“虚拟传感器”换成了真实传感器,话题名和控制接口是一样的。Nav2 只要保证激光雷达话题、里程计话题、cmd_vel 话题都存在,它甚至不知道自己在跑虚拟环境还是真机器。
但实体车有三个需要注意的点:一是真实激光雷达有噪声和盲区,如果地面反光或者离地太低,数据会毛刺很多,最好先做滤波;二是里程计来源如果是轮式编码器,肯定会打滑,AMCL 会尽力修正,但如果打滑太严重,粒子也救不回来;三是实体车的处理器通常不如实验室台式机,刚才说的降粒子数、降地图分辨率这些优化手段就是必备项。
6.2 3D 环境感知与八叉树地图导航
二维栅格地图只能表达“地面上一圈障碍物”,它想象不出头顶的墙壁、悬空的障碍物,更无法用于无人机和机械臂。等二维导航跑顺之后,可以往三维方向走:OctoMap,中文叫八叉树地图。简单说就是把空间切成小方块,然后只用“有信息”的方块来存储,避免像普通三维数组那样浪费大量内存。这也解释了为什么八叉树地图在资源受限机器人上特别流行。
八叉树地图导航通常还会搭配体素代价地图,把三维障碍物投影到二维面上供导航规划器使用。这个方向听起来复杂,但作为少年营的“未来课题”完全合适,因为它的视觉回报很高,能在 rviz2 里直接看到机器人周围生成一坨彩色三维格子,比二维地图酷很多。
6.3 多机器人协同与竞赛方向
如果少年营里有一部分孩子觉得二维导航已经“玩腻了”,可以给他们抛出最带劲的方向:多台机器人共享地图、互相避让、避免死锁。这不仅仅是把几套导航系统放在一起,还要考虑任务分配、交通管理、优先级调度。
我做了一个非常粗浅的先导演示:两个终端分别启动两台 TurtleBot3 仿真机器人,设置两个不同的目标点,让它们在路径交叉处互相避让。孩子们发现,当两台机器人同时经过同一个狭窄走廊时,其中一台会先停下来等另一台过去,这个“等待”并不是 Navigation 自带的,而是因为我提前在任务的规划里加了优先级。这里如果往上深挖,就会碰到“改进冲突搜索”这类多机器人路径规划算法,虽然是大学生的课程内容,但少年营不需要推导公式,只需要知道“存在一种算法,能让多台机器人各走各的、总时间最短”,这本身就足以点燃他们的兴趣。
最后说点带营的真实体会。Day 8 真正值钱的不是那套命令,而是让孩子们相信,机器人的“智能”是可以被拆解和控制的。定位、地图、规划,这些词听着高大上,但用蒙眼走路、手机导航这些例子一讲,他们立刻就懂了。如果你也想带一个类似的课程,我的建议是别急着讲算法细节,先让机器人动起来,让小孩亲眼看到它自己绕开障碍、自己修正位置,再回头补理论,顺序千万不能反。这个营的孩子们现在已经在催我准备 Day 9 了。
