So far we've built up many layers of ARCHO: URDF/Xacro defined the robot's structure, TF2 tracked the position of its parts, RViz displayed it, and Gazebo simulated the physics. But one big question remains unanswered: when we say "move forward at 0.5 meters per second," how does that simple number actually turn into the real rotation of two motors?
ros2_control is a standard framework for controlling robot hardware โ a layer that sits between high-level ROS 2 algorithms (like Nav2) and the robot's real or virtual hardware, and translates software commands into a language the motors understand.
Suppose a factory manager tells a worker, "produce this product faster." But the industrial machine
itself only understands commands like Motor speed = 1500 rpm or
Valve = Open. Someone has to translate the high-level order into a precise machine
command. Nav2 is that manager; the motor is that industrial machine; and ros2_control is the
technical supervisor that translates between them.
| Comparison | Question It Asks |
|---|---|
| Nav2 (later chapters) | Where should the robot go? |
| MoveIt (Chapter 13) | What path should the arm follow? |
| ros2_control | How exactly should the joints execute that motion? |
| Motor Driver | How does the low-level hardware send the command over the wire? |
The Controller Manager is the heart of ros2_control: it's responsible for loading,
activating, and deactivating Controllers, managing the control loop, and reading/writing to hardware
on every cycle. In the previous chapter, we used the command
ros2 control list_controllers to ask this very Node which Controllers were active.
| Controller | Works With | Task |
|---|---|---|
joint_state_broadcaster | Every Joint | Publishes each Joint's instantaneous state on /joint_states |
diff_drive_controller | Differential-drive robots (ARCHO) | Takes /cmd_vel and converts it into separate left/right wheel speeds |
joint_trajectory_controller | Robotic arms | Receives a time-scheduled trajectory of joint angles and executes it smoothly |
forward_command_controller | Any Joint (for testing) | Sends a command almost directly to the Joint |
This Controller completes the loop we saw in Chapter 5: Encoders โ ros2_control โ
/joint_states โ robot_state_publisher โ TF tree โ RViz. Without this Controller,
robot_state_publisher never finds out how far ARCHO's wheels have actually turned, and
the model in RViz stays frozen even if the robot is really moving.
Each Joint is typically controlled through one of three modes:
| Command Type | Meaning | Example Use |
|---|---|---|
| Position | Move the joint to this angle | Robot arm, servo motor |
| Velocity | Spin the joint at this speed | ARCHO's wheels |
| Effort | Apply this amount of force/torque | Force control in industrial arms |
In the previous chapter we saw the block below inside the Xacro โ now let's unpack exactly what each line means:
<ros2_control name="ArchoGazeboSystem" type="system">
<hardware>
<plugin>gz_ros2_control/GazeboSimSystem</plugin>
</hardware>
<joint name="left_wheel_joint">
<command_interface name="velocity"/>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="right_wheel_joint">
<command_interface name="velocity"/>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
</ros2_control>
This block says: we have a control system named ArchoGazeboSystem; its hardware is the
Gazebo simulation plugin; and the left and right Joints each accept a velocity command
(command_interface) and report their real position and velocity
(state_interface).
Because the URDF already knows the Joint names and the robot's mechanical structure. It makes sense for the control Interface definitions to be attached to those same Joints, rather than living in a completely separate file disconnected from the physical structure.
The Controllers themselves are defined and configured in a separate YAML file:
# archo_bringup/config/controllers.yaml
controller_manager:
ros__parameters:
update_rate: 50
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
diff_drive_controller:
type: diff_drive_controller/DiffDriveController
diff_drive_controller:
ros__parameters:
left_wheel_names: [left_wheel_joint]
right_wheel_names: [right_wheel_joint]
wheel_separation: 0.40
wheel_radius: 0.09
base_frame_id: base_link
odom_frame_id: odom
publish_rate: 50.0
enable_odom_tf: true
use_stamped_vel: true
linear.x.has_velocity_limits: true
linear.x.max_velocity: 0.7
linear.x.min_velocity: -0.3
angular.z.has_velocity_limits: true
angular.z.max_velocity: 1.5
These two numbers must match ARCHO's real dimensions exactly (the same ones we wrote into the Xacro
in Chapter 4). If wheel_radius is wrong, the robot's actual speed will differ from its
reported speed and Odometry will be incorrect; if wheel_separation is wrong, the
rotation angle will be miscalculated and the robot will turn less or more than it should โ an error
that gradually produces navigation drift.
diff_drive_controller estimates the robot's linear and angular motion from the two
wheels' encoder data and publishes it on the /odom Topic and the
odom โ base_link Transform โ exactly the same loop we saw in Chapter 5 and will examine
more deeply in Chapter 9.
One of the biggest advantages of ros2_control is that the software architecture stays nearly identical between simulation and the real robot โ only one bottom layer changes:
This means the diff_drive_controller we tested today on the simulated ARCHO โ the exact
same Controller, with no code changes โ also works on the real ARCHO; only the
Hardware Interface changes from gz_ros2_control/GazeboSimSystem to an actual
driver (which we'll build in Chapter 18). This is precisely the power of the layered ROS 2 architecture
we discussed back in Chapter 1.
| Mistake | Consequence |
|---|---|
| Two Controllers claiming the same Interface | Conflict, causing one activation to be rejected |
| Wrong encoder sign | The wheel moves forward but the encoder reports a negative value โ incorrect Odometry |
| Wrong units (RPM instead of rad/s) | ROS expects Position in radians and Velocity in rad/s; a mismatch causes unexpected behavior |
| Wrong wheel radius | Speed and Odometry errors |
| Inappropriate update rate | Unstable control or sluggish response |
| No command timeout | If communication drops, the motor keeps executing the last command forever โ dangerous in the real world |
Suppose wheel_radius in the YAML was mistakenly entered as 0.07, while ARCHO's actual
wheel radius is 0.09 meters. If the robot really moves 1 meter, will the Odometry report a value
greater than or less than 1 meter? Write out your reasoning.
Now we know exactly what happens between a simple cmd_vel command and the actual rotation
of ARCHO's motors: the Controller Manager, two main Controllers (joint_state_broadcaster and
diff_drive_controller), and a Hardware Interface that can be either virtual or real โ all without the
higher layers needing to know anything about it.
ARCHO Project now has a complete, properly tuned controllers.yaml. The cmd_vel command is no longer just a raw number โ it's actually converted into the rotation of two wheels and produces Odometry.
In Chapter 9, we'll combine this same /odom produced by diff_drive_controller with IMU data โ Sensor Fusion using robot_localization โ to make ARCHO's position estimate more accurate and stable.