Engineering Guides · Module 14
Integrating a controller with ros2_control
ros2_control connects controllers to hardware through interfaces. A controller only works if its parameters match the hardware: names, geometry, frames and timeouts.
Assumptions: Numbers in the examples are illustrative and the code is executed by the site’s tests. Facts taken from a cited source are marked; other statements are general engineering knowledge and must be confirmed on the datasheet of the part you use.
1. Engineering problem
A controller that is not configured for the hardware either refuses to start or behaves wrongly in ways that look like tuning problems. The diff_drive_controller needs the wheel joint names, the geometry, and sensible frames and timeouts.
2. When is this needed?
- Bringing up a new differential-drive base with ros2_control.
- When the controller will not activate.
- When odometry or velocity commands behave oddly.
3. Selection and design decisions
From the diff_drive_controller parameter file (ros2_controllers, jazzy, read 2026-11-06): required parameters are left_wheel_names and right_wheel_names (not empty), wheel_separation and wheel_radius (greater than 0). Other parameters and defaults: odom_frame_id: odom, base_frame_id: base_link, enable_odom_tf: true, open_loop: false, position_feedback: true, cmd_vel_timeout: 0.5 seconds (“a command received on the cmd_vel topic is considered stale”; 0.0 disables the timeout), publish_rate: 50.0 Hz, velocity_rolling_window_size: 10, plus pose_covariance_diagonal and twist_covariance_diagonal (defaults all zero: tune them to your robot), the radius and separation multipliers (default 1.0) and per-axis velocity, acceleration and jerk limits. The controller reads wheel feedback and writes wheel velocity commands through the hardware interfaces of the named joints.
| Item | Check |
|---|---|
| Wheel joint names | Match the hardware interface and the URDF |
| Geometry | Wheel radius and separation measured, then calibrated |
| Frames | odom and base frames match what the rest of the stack expects; only one publisher of odom→base_link |
| Covariances | Non-zero and realistic, or fusion treats odometry as perfect |
| cmd_vel_timeout | Short enough to stop on loss of commands, long enough for your publisher rate |
| Limits | Velocity and acceleration limits match the robot |
| Open vs closed loop | Closed loop needs wheel feedback |
4. Advantages and disadvantages
- enable_odom_tf true is convenient and conflicts with a fusion filter publishing the same transform.
- Open loop works without feedback and drifts.
- A short command timeout is safer and requires a steady publisher.
5. Requirements and constraints
- Names, geometry and frames are consistent between the URDF, the controller and the hardware.
- Covariances are set from the measured odometry error.
- Only one node publishes odom→base_link.
6. Architecture
cmd_vel ─▶ diff_drive_controller ─▶ wheel velocity command interfaces ─▶ hardware ─▶ wheel position/velocity states ─▶ odometry (odom→base_link)7. Implementation
- Write the controller configuration from the hardware description.
- Check it (below) and load the controller.
- Command a small velocity with wheels off the ground and check the sign and magnitude.
- Check the odometry and the transform.
- Calibrate geometry and set covariances.
REQUIRED = ["left_wheel_names", "right_wheel_names", "wheel_separation", "wheel_radius"]
DEFAULTS = {"odom_frame_id": "odom", "base_frame_id": "base_link", "cmd_vel_timeout": 0.5, "publish_rate": 50.0, "open_loop": False, "enable_odom_tf": True}
def check(cfg):
out = []
for k in REQUIRED:
v = cfg.get(k)
if v in (None, [], 0, 0.0):
out.append(f"{k} is missing or empty (the controller's validation rejects it)")
for k in ("wheel_separation", "wheel_radius"):
if isinstance(cfg.get(k), (int, float)) and cfg[k] < 0:
out.append(f"{k} must be greater than 0")
if len(cfg.get("left_wheel_names", [])) != len(cfg.get("right_wheel_names", [])):
out.append("left and right wheel lists have different lengths")
for k, d in DEFAULTS.items():
if k in cfg and cfg[k] != d:
out.append(f"{k} = {cfg[k]!r} differs from the default {d!r}")
return out or ["no findings"]
cfg = {"left_wheel_names": ["left_wheel_joint"], "right_wheel_names": ["right_wheel_joint", "right_rear_joint"], "wheel_separation": 0.4, "wheel_radius": 0, "cmd_vel_timeout": 0.0, "enable_odom_tf": False}
print("\n".join(check(cfg)))Printed (checked by the site’s tests):
wheel_radius is missing or empty (the controller's validation rejects it)
left and right wheel lists have different lengths
cmd_vel_timeout = 0.0 differs from the default 0.5
enable_odom_tf = False differs from the default TrueReading it: the checker flags the zero wheel radius (rejected by the controller’s validation), the mismatch between one left wheel and two right wheels, the disabled command timeout (0.0 turns the safety stop off) and the disabled odometry transform. The configuration is an invented example.
8. Real examples
- Commands (Jazzy):
ros2 control list_controllersandros2 control list_hardware_interfacesshow what is loaded and which interfaces exist (not executed by the site’s tests).
9. Failure scenarios
- A joint name typo, so the controller cannot claim its interfaces.
- Zero covariance, so a fusion filter trusts odometry completely.
- Two publishers of odom→base_link.
10. Troubleshooting
- Run
ros2 control list_hardware_interfacesand compare the joint names. - Check the controller state (active or not) and its log.
- Check the sign of the wheel directions on a lifted robot.
11. Validation
- The checker prints the lines shown (executed by the site tests).
- The controller activates, and a measured 1 m command moves the robot 1 m within the calibration tolerance.
12. Engineering checklist
13. Further references
- diff_drive_controller parameters (ros2_controllers, jazzy branch): diff_drive_controller_parameter.yaml, read 2026-11-06.