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

map

Fixed reference for the site; never moves

AMCL or SLAM

odom

Where the robot started · smooth but drifts

The encoder reader

base_link

Robot centre, usually the wheel axle midpoint

—

Sensors

Fixed offsets from base_link

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