可以参考这个开源项目的架构
GitHub – Ikunio/Lidar_nav2_ws: 基于 Livox MID-360 3D LiDAR 的 ROS 2 自主导航工作空间,集成 LIO 里程计、重定位、Nav2 导航,支持仿真与实机部署。 · GitHub基于 Livox MID-360 3D LiDAR 的 ROS 2 自主导航工作空间,集成 LIO 里程计、重定位、Nav2 导航,支持仿真与实机部署。 – Ikunio/Lidar_nav2_ws
https://github.com/Ikunio/Lidar_nav2_ws
很多人在第一次部署 ROS2 Nav2 的时候,都会遇到一个很迷惑的问题:
RViz 里机器人能显示,地图也能显示,目标点也能点,但是机器人就是不动。
然后第一反应通常是:
-
是不是 DWB 参数没调好?
-
是不是速度限制太小?
-
是不是 costmap 参数错了?
-
是不是 planner 没起来?
-
是不是 controller 不工作?
这些都有可能,但在我自己做 ROS2 3D LiDAR 导航系统的时候,发现很多 Nav2 问题最后都绕不开一个基础问题:
TF 树有没有接对?
尤其是这一条链:
map → odom → base_footprint → chassis → livox_frame
如果这条链不完整、不连续、时间戳不对,或者被多个节点重复发布,那么 Nav2 很可能会出现各种奇怪问题。
本文不先讲复杂参数,而是从 Nav2 最基础的数据链路讲起: 为什么 Nav2 这么依赖 TF?map、odom、base_footprint 分别负责什么?以及实机部署时应该怎么排查。
1. 先给结论
Nav2 不是只要有 /scan 和 /map 就能跑。
它真正需要的是一套完整的机器人定位链路:
map
↓
odom
↓
base_footprint / base_link
↓
laser_frame / lidar_frame
换句话说,Nav2 必须能回答几个问题:
机器人在地图里的位置在哪里?
机器人相对于自己的 odom 坐标系移动了多少?
雷达装在机器人身体的哪个位置?
激光数据能不能转换到机器人本体坐标系?
costmap 能不能把障碍物投影到正确的位置?
controller 算出来的速度能不能作用到正确的机器人坐标系?
如果 TF 树断了,Nav2 就会不知道机器人在哪。 如果 TF 树错了,Nav2 就会以为机器人在错误的位置。 如果 TF 树重复发布,Nav2 可能会在两个位姿之间跳。 如果 TF 时间戳不对,Nav2 会直接报 transform timeout。
所以一句话:
Nav2 大部分“看起来像参数问题”的问题,本质上可能是 TF 链路问题。
2. Nav2 为什么依赖 TF?
Nav2 是一个导航框架,它本身不是传感器驱动,也不是 SLAM,也不是里程计。
它负责的事情大概是:
知道机器人在哪里
↓
知道目标点在哪里
↓
根据地图规划路径
↓
根据局部障碍物避障
↓
输出 /cmd_vel 控制机器人运动
但是这里面有一个前提:
Nav2 必须知道机器人当前在地图坐标系下的位姿。
这个位姿不是凭空来的,而是通过 TF 查出来的。
例如 Nav2 想知道机器人在 map 下的位置,它通常会查询:
map -> base_link
或者:
map -> base_footprint
但是在 ROS2 里,这个变换通常不是一个节点直接发布的,而是由多段 TF 拼出来的:
map -> odom
odom -> base_footprint
base_footprint -> chassis
chassis -> livox_frame
只要这条链路能连通,Nav2 就能知道机器人在哪里。
如果中间断了一段,例如没有:
odom -> base_footprint
那 Nav2 就没法从 map 查到机器人本体。
如果没有:
base_footprint -> livox_frame
那 costmap 就不知道雷达数据相对于机器人身体的位置。
所以 Nav2 对 TF 的依赖非常强。
3. map、odom、base_footprint 到底分别是什么?
很多初学者最容易混淆的是:
map、odom、base_link、base_footprint
这几个坐标系名字看着简单,但职责完全不同。
4. map:全局地图坐标系
map 是全局坐标系。
它通常代表机器人运行环境中的固定地图坐标系。
例如:
-
2D 栅格地图的原点;
-
3D 点云地图的原点;
-
SLAM 建图时的全局坐标系;
-
重定位成功后的全局参考系。
map 的特点是:
全局一致,但不一定连续。
什么意思?
假设机器人刚开机,里程计认为自己在原点附近:
odom 下机器人位置:x = 0.0, y = 0.0
但是重定位系统发现机器人其实在地图里的:
map 下机器人位置:x = 5.2, y = -1.3
那么 map -> odom 可能会发生一次修正。
也就是说,map 坐标系允许发生全局修正。
在 2D 导航里,AMCL 会维护:
map -> odom
在 3D LiDAR 导航里,如果你做了点云重定位,也可以通过 ICP、small_gicp、KISS-Matcher 这类方法维护:
map -> odom
所以 map 的核心职责是:
提供全局位置参考。
5. odom:局部连续里程计坐标系
odom 是局部连续坐标系。
它通常来自:
-
轮速计;
-
视觉里程计;
-
激光里程计;
-
LiDAR-Inertial Odometry,比如 FAST-LIO、Point-LIO;
-
多传感器融合后的 EKF 输出。
odom 的特点是:
局部连续,但会漂移。
也就是说,odom 不应该突然跳变。
机器人从 A 点移动到 B 点,odom -> base_footprint 应该是平滑变化的。
如果你的 odom 一会儿在原点,一会儿突然跳到 10 米外,Nav2 controller 很容易炸。
对于 Nav2 来说,odom 的意义是:
告诉机器人短时间内自己怎么连续运动。
所以 odom 不追求全局绝对准确,它追求连续、平滑、实时。
在我的 3D LiDAR 导航系统里,LIO 可以作为 odom 来源。 比如 FAST-LIO 或 Point-LIO 输出雷达/IMU的运动估计,然后经过桥接,转换成 Nav2 需要的:
odom -> base_footprint
6. base_link 和 base_footprint:机器人本体坐标系
base_link 一般表示机器人本体中心坐标系。
base_footprint 一般表示机器人在地面上的投影坐标系。
两者区别在于:
base_link 可以包含 roll、pitch、z 高度
base_footprint 通常只保留平面运动,也就是 x、y、yaw
对于地面移动机器人,Nav2 更常用:
base_footprint
因为 Nav2 主要处理 2D 平面导航。
如果你的机器人有轻微俯仰,例如雷达装在车体上,机器人过坡时 base_link 可能有 roll/pitch。 但是对于 2D costmap 和路径规划来说,通常只关心机器人在地面上的投影。
所以常见结构是:
odom -> base_footprint -> base_link
或者:
odom -> base_link
具体用哪个,要和你的 Nav2 参数保持一致。
例如:
robot_base_frame: base_footprint
那么 TF 里必须能查到:
map -> base_footprint
如果你的 TF 只有 base_link,但参数里写的是 base_footprint,Nav2 就会查不到。
7. livox_frame / laser_frame:雷达坐标系
雷达坐标系表示雷达安装在机器人上的位置和姿态。
例如 MID-360 的坐标系可以叫:
livox_frame
或者:
laser_frame
这个坐标系一般通过 URDF 或 static transform 发布到机器人本体上:
base_link -> livox_frame
或者:
chassis -> livox_frame
它的作用是:
告诉系统雷达数据是从机器人身体的哪个位置采集到的。
如果这段 TF 错了,会导致很多问题:
-
RViz 中点云位置不对;
-
LaserScan 投影位置不对;
-
costmap 障碍物偏移;
-
机器人避障方向异常;
-
明明前面有障碍,costmap 却显示在侧面;
-
雷达扫到自己的车体,被当成障碍物。
所以雷达 TF 不是可有可无,它直接影响 costmap。
8. 一棵比较合理的 TF 树
以一个 3D LiDAR 地面机器人为例,可以设计成这样:
map
↓
odom
↓
base_footprint
↓
chassis
↓
livox_frame
每一段的职责如下:
| map -> odom | AMCL / SLAM / 3D 重定位节点 | 全局定位修正 |
| odom -> base_footprint | 轮速计 / LIO / EKF / odom bridge | 局部连续里程计 |
| base_footprint -> chassis | URDF / static tf | 机器人底盘结构 |
| chassis -> livox_frame | URDF / static tf | 雷达安装外参 |
这里面最重要的是:
map -> odom
odom -> base_footprint
前者负责全局定位,后者负责局部连续运动。
二者职责不能混。
9. 谁应该发布 map -> odom?
这是实机部署里很容易混乱的地方。
一般情况下,map -> odom 只能由一个模块维护。
常见情况:
9.1 使用 AMCL
如果你用 2D 栅格地图 + AMCL,那么通常是 AMCL 发布:
map -> odom
这时候你的轮速计或 LIO 发布:
odom -> base_footprint
系统结构是:
map
↓ AMCL
odom
↓ wheel odom / LIO
base_footprint
9.2 使用 SLAM Toolbox
如果你用 slam_toolbox 建图和定位,它也可能发布:
map -> odom
这时也不要再让别的节点重复发这段 TF。
9.3 使用 3D 点云重定位
如果你做的是 3D LiDAR 重定位,比如:
KISS-Matcher + small_gicp
那么可以由重定位节点发布:
map -> odom
LIO 负责:
odom -> base_footprint
整体结构就是:
3D PCD map
↓
KISS-Matcher / small_gicp
↓
map -> odom
FAST-LIO / Point-LIO
↓
odom -> base_footprint
这样 Nav2 最终就可以查询:
map -> base_footprint
10. 谁应该发布 odom -> base_footprint?
odom -> base_footprint 表示机器人局部连续运动。
它可以来自:
-
轮速计;
-
LIO;
-
VIO;
-
robot_localization;
-
自己写的 odom bridge。
如果你使用 FAST-LIO 或 Point-LIO,原始输出的 frame 可能不是 Nav2 想要的格式。
比如 LIO 可能发布的是:
camera_init -> body
或者:
map -> body
但是 Nav2 想要的是:
odom -> base_footprint
这时候就需要桥接。
桥接做的事情不是“凭空造一个 odom”,而是把 LIO 输出的位姿转成 Nav2 约定的坐标关系。
例如:
LIO 输出当前雷达/IMU 位姿
↓
根据雷达到车体外参转换到 base_footprint
↓
发布 nav_msgs/Odometry
↓
发布 odom -> base_footprint
这一步非常关键。
因为 FAST-LIO 很准,不代表它的输出可以直接喂给 Nav2。 Nav2 关心的是 frame 是否正确、TF 是否连续、时间戳是否稳定。
11. 常见错误一:TF 树断了
最典型的错误是:
map -> odom
有,但是没有:
odom -> base_footprint
或者:
odom -> base_footprint
有,但是没有:
base_footprint -> livox_frame
这种情况下 Nav2 可能会报:
Timed out waiting for transform
或者:
Invalid frame ID
或者:
Could not transform from base_link to map
排查方法:
ros2 run tf2_ros tf2_echo map base_footprint
如果查不到,说明从 map 到 base_footprint 中间断了。
再逐段查:
ros2 run tf2_ros tf2_echo map odom
ros2 run tf2_ros tf2_echo odom base_footprint
ros2 run tf2_ros tf2_echo base_footprint chassis
ros2 run tf2_ros tf2_echo chassis livox_frame
哪一段查不到,就先修哪一段。
12. 常见错误二:frame 名字不一致
这是非常低级但非常常见的问题。
例如 Nav2 参数里写的是:
robot_base_frame: base_link
但是你的 TF 里只有:
base_footprint
或者你的 LaserScan 里写的是:
frame_id: laser
但是 TF 树里叫:
livox_frame
这都会导致 Nav2 查 TF 失败。
检查 /scan 的 frame:
ros2 topic echo /scan –once
重点看:
header:
frame_id: livox_frame
然后确认 TF 树里是否存在:
base_footprint -> livox_frame
检查 /odom:
ros2 topic echo /odom –once
重点看:
header:
frame_id: odom
child_frame_id: base_footprint
如果这里写的是:
header:
frame_id: map
child_frame_id: base_link
那就要确认它是否符合你的系统设计。
13. 常见错误三:多个节点重复发布同一段 TF
比如同时有两个节点都在发布:
map -> odom
一个是 AMCL,另一个是你的重定位节点。
这会导致什么?
机器人在 RViz 里可能会出现:
-
抖动;
-
跳变;
-
原地旋转;
-
costmap 漂移;
-
路径规划忽左忽右;
-
Nav2 controller 输出异常。
原则很简单:
同一段父子 TF,最好只允许一个权威节点发布。
也就是说:
map -> odom
只能由 AMCL、SLAM、重定位节点中的一个发布。
odom -> base_footprint
只能由轮速计、LIO、EKF、odom bridge 中的一个发布。
如果你用了 robot_localization,那一般应该让 EKF 发布融合后的 odom,而不是让每个传感器都发布同一段 TF。
14. 常见错误四:TF 时间戳不对
Nav2 很依赖时间同步。
如果 TF 时间戳太旧,或者传感器数据时间戳和 TF 时间戳对不上,会出现:
Lookup would require extrapolation into the future
或者:
Transform data too old
常见原因:
-
使用仿真时没有设置 use_sim_time;
-
Gazebo 时间和系统时间混用;
-
传感器驱动时间戳异常;
-
LIO 输出时间戳和 ROS 时间不一致;
-
TF 发布频率太低;
-
多机通信没有时间同步。
检查是否使用仿真时间:
ros2 param get /controller_server use_sim_time
ros2 param get /planner_server use_sim_time
ros2 param get /bt_navigator use_sim_time
如果是 Gazebo 仿真,相关节点通常都应该使用:
use_sim_time: true
如果是真机,一般是:
use_sim_time: false
检查 TF 频率:
ros2 topic hz /tf
检查某段 TF 是否稳定:
ros2 run tf2_ros tf2_echo odom base_footprint
如果输出断断续续,说明 TF 发布不稳定。
15. 常见错误五:LaserScan 的 frame 没有接到机器人本体
Nav2 的 costmap 通常会订阅 /scan。
但是 /scan 不是孤立数据。
它的 header 里有一个:
frame_id
例如:
frame_id: livox_frame
Nav2 会通过 TF 把这个 LaserScan 转换到机器人本体或地图坐标系下。
所以必须存在:
base_footprint -> livox_frame
或者通过其他链路能连到。
如果没有这段 TF,costmap 就无法正确使用 scan。
排查:
ros2 topic echo /scan –once
看:
header:
frame_id: livox_frame
然后查:
ros2 run tf2_ros tf2_echo base_footprint livox_frame
如果查不到,说明雷达外参没有发布。
16. 常见错误六:RViz 里看着对,但 Nav2 还是不走
RViz 只是可视化工具。
RViz 里能看到机器人,不代表 Nav2 所需链路全部正常。
Nav2 真正需要的是:
/map
/scan
/odom
/tf
/tf_static
/cmd_vel
costmap
lifecycle active
所以遇到“RViz 看着对,但机器人不走”,不要只盯着 RViz。
可以按下面顺序排查。
17. Nav2 排查 checklist
17.1 查节点是否启动
ros2 node list
重点看:
/controller_server
/planner_server
/bt_navigator
/behavior_server
/waypoint_follower
/map_server
/lifecycle_manager_navigation
17.2 查 lifecycle 状态
ros2 lifecycle nodes
然后查具体节点:
ros2 lifecycle get /controller_server
ros2 lifecycle get /planner_server
ros2 lifecycle get /bt_navigator
正常应该是:
active
如果不是 active,Nav2 不会正常工作。
17.3 查 TF 主链路
ros2 run tf2_ros tf2_echo map base_footprint
如果这里查不到,Nav2 基本不可能正常跑。
继续逐段查:
ros2 run tf2_ros tf2_echo map odom
ros2 run tf2_ros tf2_echo odom base_footprint
ros2 run tf2_ros tf2_echo base_footprint livox_frame
17.4 生成 TF 树 PDF
ros2 run tf2_tools view_frames
它会生成一个 TF 树文件,可以直观看到:
-
哪些 frame 存在;
-
哪些 frame 断开;
-
哪些 TF 发布频率异常;
-
哪些 TF 时间戳有问题。
17.5 查 /scan
ros2 topic echo /scan –once
ros2 topic hz /scan
重点看:
header:
frame_id: livox_frame
以及频率是否稳定。
17.6 查 /odom
ros2 topic echo /odom –once
ros2 topic hz /odom
重点看:
header:
frame_id: odom
child_frame_id: base_footprint
如果 frame_id 和 child_frame_id 不符合你的系统设计,Nav2 就可能查错。
17.7 查 /cmd_vel
ros2 topic echo /cmd_vel
如果点了目标点之后没有 /cmd_vel 输出,说明 Nav2 没有成功进入控制阶段。
如果有 /cmd_vel,但是底盘不动,那问题可能在底盘驱动或串口通信。
18. 一个推荐的排查顺序
我建议不要一上来就改 Nav2 参数。
推荐顺序是:
1. 先查节点是否 active
2. 再查 map -> base_footprint 是否能查到
3. 再查 /scan 的 frame_id 是否能接到 base_footprint
4. 再查 /odom 的 frame_id 和 child_frame_id
5. 再查 costmap 是否正常显示障碍物
6. 再查 /cmd_vel 是否输出
7. 最后再调 planner/controller 参数
也就是:
先查系统链路,再查算法参数。
很多人一上来就调 DWB,其实 TF 都没通。 这种情况下,不管你怎么调 controller,机器人都不会正常跑。
19. LIO 接 Nav2 时要特别注意什么?
如果你的系统使用 FAST-LIO、Point-LIO 这类 LIO 作为里程计来源,要额外注意以下几点。
19.1 FAST-LIO 输出的 frame 不一定等于 Nav2 需要的 frame
LIO 可能输出:
camera_init -> body
或者:
map -> body
但是 Nav2 通常希望:
odom -> base_footprint
所以中间需要转换。
不要简单地认为:
FAST-LIO 有 odometry 输出 = Nav2 可以直接用
真正要看:
-
frame 名是否对;
-
child frame 是否对;
-
TF 是否连续;
-
时间戳是否对;
-
是否会和其他节点重复发布 TF;
-
雷达到车体外参是否处理正确。
19.2 LIO 更适合作为 odom,不一定适合直接作为 map
LIO 是局部里程计,长期会漂移。 它很适合发布:
odom -> base_footprint
但如果没有全局约束,直接把 LIO 当成 map 可能会导致全局地图慢慢漂。
比较合理的结构是:
3D 重定位 / SLAM / AMCL
↓
map -> odom
FAST-LIO / Point-LIO
↓
odom -> base_footprint
也就是:
全局定位负责 map -> odom
局部里程计负责 odom -> base_footprint
这样职责更清楚。
19.3 不要让 LIO 和重定位节点同时发布 map -> odom
如果你已经有一个 3D 重定位节点在发布:
map -> odom
那 LIO 就不要再发这段 TF。
否则系统会出现两个全局定位来源。
正确做法通常是:
LIO:发布 odom -> base_footprint
重定位:发布 map -> odom
20. 一个推荐的 Nav2 参数片段
下面是一个示意配置,不是通用答案,但可以作为检查思路。
amcl:
ros__parameters:
use_sim_time: false
base_frame_id: "base_footprint"
odom_frame_id: "odom"
global_frame_id: "map"
bt_navigator:
ros__parameters:
use_sim_time: false
global_frame: "map"
robot_base_frame: "base_footprint"
odom_topic: "/odom"
controller_server:
ros__parameters:
use_sim_time: false
odom_topic: "/odom"
local_costmap:
local_costmap:
ros__parameters:
global_frame: "odom"
robot_base_frame: "base_footprint"
observation_sources: scan
scan:
topic: /scan
data_type: "LaserScan"
marking: true
clearing: true
global_costmap:
global_costmap:
ros__parameters:
global_frame: "map"
robot_base_frame: "base_footprint"
observation_sources: scan
scan:
topic: /scan
data_type: "LaserScan"
marking: true
clearing: true
重点不是照抄参数,而是理解这几个 frame 必须和 TF 树一致:
global_frame: map
local_costmap.global_frame: odom
robot_base_frame: base_footprint
odom_topic: /odom
scan.topic: /scan
21. 一个正确系统应该是什么样?
假设我的系统是:
MID-360 + IMU
↓
FAST-LIO / Point-LIO
↓
odom -> base_footprint
↓
PointCloud2 -> LaserScan
↓
Nav2 costmap
↓
Planner / Controller
↓
/cmd_vel
同时如果需要全局定位:
PCD map
↓
KISS-Matcher / small_gicp
↓
map -> odom
最终 TF 树就是:
map
↓
odom
↓
base_footprint
↓
chassis
↓
livox_frame
这时 Nav2 就能做三件事:
通过 map -> base_footprint 知道机器人在地图里的位置;
通过 base_footprint -> livox_frame 正确使用雷达数据;
通过 /cmd_vel 控制底盘运动。
22. 什么时候才应该开始调 Nav2 参数?
只有当下面这些都正常之后,再去调 planner/controller 参数:
1. map -> base_footprint 能查到;
2. odom -> base_footprint 连续稳定;
3. base_footprint -> livox_frame 正确;
4. /scan 有数据,frame_id 正确;
5. costmap 能正确显示障碍物;
6. Nav2 lifecycle 节点都是 active;
7. 点目标后 /cmd_vel 有输出。
如果这些还没满足,就去调 DWB、NavFn、inflation、footprint,很容易越调越乱。
23. 总结
最后总结一下:
Nav2 不是只要有 /scan 和 /map 就能跑,它依赖完整 TF 链。
map -> odom -> base_footprint 是 ROS2 地面机器人导航的核心链路。
map 负责全局定位,odom 负责局部连续运动,base_footprint 负责机器人本体平面坐标。
同一段 TF 不要让多个节点重复发布,尤其是 map -> odom 和 odom -> base_footprint。
LIO 可以作为 Nav2 的 odom 来源,但需要桥接成 Nav2 需要的 frame。
遇到 Nav2 跑不起来,不要第一时间调 controller 参数,先查 TF、frame_id、时间戳和 lifecycle。
我的建议是:
先让 TF 树正确,再让 costmap 正确,最后再调 Nav2 参数。
因为在 ROS2 导航系统里,很多问题不是“算法不会跑”,而是系统链路一开始就没接对。
如果 TF 树是错的,后面的 planner、controller、costmap 都会跟着错。
所以 Nav2 排坑第一步,不是改参数,而是先问自己一句:
map 到 base_footprint 真的能查到吗?



