8. tf2 — coordinate frames
This is the heart of a mobile robot. It cannot be skipped.
The problem tf2 solves
The LiDAR reports “something is 2 metres ahead” — ahead of the LiDAR, which is mounted at the front of the robot, 20 cm off the floor, and possibly facing backwards.
The question you need answered is “where is that thing on the map”.
That means converting through several steps: LiDAR to robot centre, robot centre
to map. tf2 does this, and does it backwards in time, because each piece of
data arrives at a different moment.
The standard frames for a mobile robot
This ordering is the convention (REP-105). Every tool expects it.
map
└── odom
└── base_link
├── laser_front
├── laser_rear
└── wheel_left / wheel_right
Frame |
What it is |
Published by |
|---|---|---|
|
Fixed reference for the site; never moves |
AMCL or SLAM |
|
Where the robot started · smooth but drifts |
The encoder reader |
|
Robot centre, usually the wheel axle midpoint |
— |
Sensors |
Fixed offsets from |
URDF |
Important
Why there are both map and odom — the most common confusion.
odom → base_link comes from the encoders, so it is smooth and never jumps, but
its error accumulates. Drive long enough and it is metres off.
map → odom is the correction for that accumulated error. AMCL computes it
by matching the LiDAR against the map. It is allowed to jump.
Together you get both: a position smooth enough to control wheels with, and a position accurate enough to reach a goal.
Publishing a fixed frame
The LiDAR’s position relative to the robot never changes, so publish it once.
For a quick test (LiDAR 40 cm forward, 20 cm up):
ros2 run tf2_ros static_transform_publisher \
--x 0.4 --z 0.2 --frame-id base_link --child-frame-id laser_front
Metres and radians. Anything omitted is zero. Rotation uses --roll,
--pitch, --yaw.
Note
Older examples online use the positional form:
ros2 run tf2_ros static_transform_publisher 0.4 0 0.2 0 0 0 base_link laser_front
That still works in Humble but prints Old-style arguments are deprecated, and
its order is x y z yaw pitch roll — yaw before roll, the reverse of how the
named flags read. Misremember it and the robot faces the wrong way with nothing
to tell you. The named form is safer.
Note
A real system does not do this. The offsets go in the URDF and
robot_state_publisher publishes them all at once (part 2, topic 13).
This command is for experiments and debugging.
Check the tree is complete
As a picture:
ros2 run tf2_tools view_frames
You get frames.pdf — the whole tree, with the publishing rate of each link.
Measure between two frames:
ros2 run tf2_ros tf2_echo base_link laser_front
- Translation: [0.400, 0.000, 0.200]
- Rotation: in RPY (radian) [0.000, 0.000, 0.000]
Tip
Compare that against a tape measure. If it does not match, the URDF is wrong.
The consequence is a distorted map, with nothing to warn you — the system trusts the numbers you gave it.
Common traps
① A broken tree
Could not find a connection between 'map' and 'laser_front'
A link is missing. frames.pdf shows where. Usually the node that should publish
it was never started.
② Two publishers for the same link
The robot jitters or jumps in RViz, because two sources alternate. Common when
robot_state_publisher and a static_transform_publisher publish the same link.
frames.pdf shows a link with more than one publisher.
③ Clocks out of sync
Lookup would require extrapolation into the future
Happens across multiple machines with unsynchronised clocks. Install chrony so
they agree.
Warning
🔴 On a real robot running ROS 2 across machines, clock sync matters more than you would expect. 0.1 s of skew is enough to break tf, and the error message does not mention clocks.
④ Wrong sim time
With Gazebo, every node needs use_sim_time: true. Miss one and it uses wall
time while the rest use simulated time — tf breaks system-wide.
Moving to the real robot, all of them must go back to false.
Previous: 7. Launch files and parameters · Next: 9. RViz