ROS2_Control
ROS2 Control Overview
A Brief Introduction to ROS2 Control
Concept
ros2_control is the standard framework for robot control in ROS 2. Its core idea is to break down robot control into three layers:
- Controller: Responsible for control algorithms. For example, a differential drive chassis controller converts
cmd_velinto left and right wheel speeds, and a trajectory controller converts joint trajectories into joint commands. - Controller Manager: Responsible for loading, configuring, activating, and stopping controllers, and executing the
read -> update -> writecontrol loop at a fixed frequency. - Hardware Interface: responsible for communicating with real hardware, simulated hardware, or emulated hardware, so that the controller does not need to worry about underlying serial ports, CAN, EtherCAT, Gazebo, or custom drivers.
In simple terms, ros2_control is the standard socket between "controllers" and "hardware" in ROS 2. Controllers only face the standard interface, hardware only exposes the standard interface, and they are managed by controller_manager in between.
In Ubuntu 24.04 + ROS 2 Jazzy, the default installation path of ROS 2 is:
/opt/ros/jazzy
Common ros2_control package paths are as follows:
| software package | path | Explanation |
|---|---|---|
ros2_control | /opt/ros/jazzy/share/ros2_control | ros2_control metapackage |
controller_manager | /opt/ros/jazzy/share/controller_manager | Controller manager, providing tools such as ros2_control_node, spawner, etc. |
controller_interface | /opt/ros/jazzy/share/controller_interface | The base interface that a custom controller should inherit |
hardware_interface | /opt/ros/jazzy/share/hardware_interface | Base interface to inherit for custom hardware interfaces |
ros2_controllers | /opt/ros/jazzy/share/ros2_controllers | Official Common Controller Metapackage |
ros2controlcli | /opt/ros/jazzy/share/ros2controlcli | Provide the ros2 control ... command |
ROS2 Control 解决了什么问题
If you don't use ros2_control, the robot control program can easily be written like this:
- Directly subscribe to node
/cmd_vel; - The node calculates the left and right wheel speeds by itself;
- The node reads the encoder itself;
- Node implements its own serial port or CAN;
- When changing hardware, the entire set of control nodes must be modified;
- When migrating from simulation to the real robot, you have to modify it again.
After using ros2_control, the recommended structure is:
- The controller is responsible for algorithms, such as differential kinematics, robotic arm trajectory interpolation, PID, and gripper control.
- Hardware interfaces are responsible for communication, such as serial port protocols, CAN messages, driver board registers, and Gazebo plugins.
- URDF is responsible for declaring which joints, command interfaces, and state interfaces the robot has.
- YAML is responsible for declaring which controllers to load and the controller parameters.
controller_manageris responsible for putting them together.
The benefit of this is that the controller can be reused, hardware can be replaced, and simulation and real hardware can share most of the upper-layer configuration.
ROS2 Control Installation
Basic Installation
When using ROS 2 Jazzy on Ubuntu 24.04, the installation commands are as follows:
sudo apt update
sudo apt install ros-jazzy-ros2-control ros-jazzy-ros2-controllers
These two packages can already meet most needs for real robot control, controller development, and basic learning. If you also want to fully install this chapter's debugging tools, Gazebo Sim examples, and test support packages, it is recommended to continue installing:
sudo apt install \
ros-jazzy-gz-ros2-control \
ros-jazzy-gz-ros2-control-demos \
ros-jazzy-rqt-controller-manager \
ros-jazzy-rqt-joint-trajectory-controller \
ros-jazzy-ros2-controllers-test-nodes \
ros-jazzy-hardware-interface-testing \
ros-jazzy-joint-state-topic-hardware-interface \
ros-jazzy-battery-state-broadcaster
Among them, ros-jazzy-gz-ros2-control is the core package for Gazebo Sim to interface with ros2_control, ros-jazzy-gz-ros2-control-demos provides official runnable examples, and the two rqt packages provide graphical controller management and trajectory sending tools. The following packages are not necessary for writing ordinary robots, but are very helpful for reading examples, testing hardware interfaces, and learning the broadcaster.
Every time you open a new terminal, you need to source the ROS 2 environment:
source /opt/ros/jazzy/setup.bash
Verify Installation
You can view the base package:
ros2 pkg list | grep -E '^(ros2_control|controller_manager|hardware_interface|controller_interface|ros2controlcli)$'
Under normal circumstances, you can see:
controller_interface
controller_manager
hardware_interface
ros2_control
ros2controlcli
You can view common controllers:
ros2 pkg list | grep -E '^(joint_state_broadcaster|diff_drive_controller|joint_trajectory_controller|forward_command_controller|position_controllers|velocity_controllers|effort_controllers|pid_controller|mecanum_drive_controller|ackermann_steering_controller|tricycle_controller|omni_wheel_drive_controller)$'
Common outputs include:
ackermann_steering_controller
diff_drive_controller
effort_controllers
forward_command_controller
joint_state_broadcaster
joint_trajectory_controller
mecanum_drive_controller
omni_wheel_drive_controller
pid_controller
position_controllers
tricycle_controller
velocity_controllers
These package names will seem abstract the first time you see them. You can first understand them as follows:
| package name | Understanding Chinese | Main Uses | Typical Input | Typical output |
|---|---|---|---|---|
joint_state_broadcaster | Joint State Publisher | Read hardware state interfaces, publish /joint_states and /dynamic_joint_states | Status interfaces such as position, velocity, effort, etc. in the hardware | ROS topic, no hardware commands |
diff_drive_controller | differential chassis controller | Differential drive car | geometry_msgs/msg/TwistStamped speed command | Left and right wheels velocity command interface, also publish odom |
joint_trajectory_controller | Joint Trajectory Controller | Execute joint-space trajectories for robotic arms, pan-tilts, and multi-joint mechanisms. | trajectory_msgs/msg/JointTrajectory or FollowJointTrajectory action | A set of joints' position, velocity, or effort commands |
forward_command_controller | Command Forwarding Controller | Forward the received array commands directly to the hardware interface, suitable for testing hardware. | std_msgs/msg/Float64MultiArray | the specified command interface for the specified joint |
position_controllers | Position Command Group Controller | Directly send position targets to multiple joints. | position array | command interface for multiple joints position |
velocity_controllers | Velocity Command Group Controller | Directly issue velocity targets to multiple joints. | speed array | command interface for multiple joints velocity |
effort_controllers | Force/Torque Command Group Controller | Directly issue effort targets to multiple joints | effort array | command interface for multiple joints effort |
pid_controller | PID controller | Implement PID closed-loop control in the ros2_control chain | reference interface or controller chain input | Command Interface After PID Calculation |
mecanum_drive_controller | Mecanum wheel chassis controller | Control a four-wheel Mecanum chassis to achieve forward/backward, strafe, and rotation. | Usually a chassis speed command | Four Mecanum wheels' velocity command interface |
ackermann_steering_controller | Ackermann steering controller | Control of car-like front-wheel steering, rear-wheel drive structure | Chassis speed/steering related commands | Steering joint position command and drive wheel speed command |
tricycle_controller | tricycle controller | Control a three-wheel chassis with a steering wheel and a drive wheel. | chassis speed command | Steering Joint and Drive Wheel Commands |
omni_wheel_drive_controller | Omnidirectional Wheel Chassis Controller | Control an omnidirectional chassis composed of three or more omni wheels. | chassis speed command | Multiple omnidirectional wheels' velocity command interface |
Here's an important distinction: joint_state_broadcaster is a broadcaster — it only publishes status and doesn't actually make the robot move; diff_drive_controller, joint_trajectory_controller, and mecanum_drive_controller are controllers that claim command interfaces and write commands to the hardware. position_controllers, velocity_controllers, effort_controllers, and forward_command_controller are more like tools that "directly forward commands," which are good for testing hardware interfaces but won't compute complex kinematics for you.
If Gazebo Sim and the debugging aid package are installed, you can also check:
ros2 pkg list | grep -E '^(gz_ros2_control|gz_ros2_control_demos|rqt_controller_manager|rqt_joint_trajectory_controller|hardware_interface_testing|joint_state_topic_hardware_interface|battery_state_broadcaster)$'
Normally you will see:
battery_state_broadcaster
gz_ros2_control
gz_ros2_control_demos
hardware_interface_testing
joint_state_topic_hardware_interface
rqt_controller_manager
rqt_joint_trajectory_controller
controller_manager package provides four commonly used executable files:
ros2 pkg executables controller_manager
Output should include:
controller_manager hardware_spawner
controller_manager ros2_control_node
controller_manager spawner
controller_manager unspawner
Graphical debugging tool can be started like this:
ros2 run rqt_controller_manager rqt_controller_manager
ros2 run rqt_joint_trajectory_controller rqt_joint_trajectory_controller
rqt_controller_manager is suitable for viewing, loading, configuring, activating, and stopping controllers; rqt_joint_trajectory_controller is suitable for manually sending simple joint trajectories to joint_trajectory_controller.
ROS2 Control Core Concepts
command interface and state interface
The most important thing when learning ros2_control is understanding the interfaces.
- command interface: target values written by the controller. For example, motor target velocity, joint target position, joint target torque.
- state interface : status values read back from the hardware. For example, encoder position, current speed, torque, current, temperature.
In Jazzy, the standard interface names are defined in:
/opt/ros/jazzy/include/hardware_interface/hardware_interface/types/hardware_interface_type_values.hpp
Common interfaces are as follows:
| interface name | Meaning |
|---|---|
position | Position, commonly used for joint angles or linear displacement |
velocity | Speed, often used for wheel speed or joint speed. |
effort | force or torque, commonly used in torque control |
acceleration | acceleration |
current | current |
temperature | temperature |
For example, a wheel joint can be declared like this:
<joint name="left_wheel_joint">
<command_interface name="velocity"/>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
The meaning is that the controller can write speed commands to left_wheel_joint/velocity, and the hardware interface must be able to read back left_wheel_joint/position and left_wheel_joint/velocity.
control loop
controller_manager's core loop is:
read() -> update() -> write()
The specific meaning is as follows:
| Stage | executor | effect |
|---|---|---|
read() | hardware interface | Read status from motors, encoders, sensors, or simulators. |
update() | controller | Calculate a new command based on the state and target. |
write() | hardware interface | Write commands to motor drivers, emulators, or low-level hardware. |
For example, differential chassis:
read()Read left and right wheel encoders, update left and right wheel position/velocity.diff_drive_controller.update()calculates the target speeds of the left and right wheels based on/diff_drive_controller/cmd_veland wheel feedback, and publishes odometry.write()write the target speeds for the left and right wheels to the motor driver board.
Lifecycle state
Both controllers and hardware components in ros2_control have lifecycles. Common states are as follows:
| Status | Meaning |
|---|---|
unconfigured | Loaded, but not yet configured. |
inactive | Configured but not participating in control loop output. |
active | Activated and participating in the control loop |
finalized | Ended |
Common workflow:
load -> configure -> activate
spawner will, by default, load, configure, and activate the controller. If you only want to load without activating, you can use --load-only or --inactive.
ros2_control tag in URDF
basic structure
The hardware description for ros2_control is written in the <ros2_control> tag of URDF or xacro.
The minimal structure is as follows:
<ros2_control name="MyRobotSystem" type="system">
<hardware>
<plugin>硬件插件名</plugin>
</hardware>
<joint name="关节名">
<command_interface name="命令接口名"/>
<state_interface name="状态接口名"/>
</joint>
</ros2_control>
type has three common values:
| type | Corresponds to the C++ base class | Intended Audience |
|---|---|---|
system | hardware_interface::SystemInterface | Multi-joint robot, chassis, robotic arm |
sensor | hardware_interface::SensorInterface | read-only sensor |
actuator | hardware_interface::ActuatorInterface | Single Actuator |
Testing with mock_components
Jazzy’s hardware_interface package provides a mock hardware plugin for testing:
/opt/ros/jazzy/share/hardware_interface/mock_components_plugin_description.xml
The plugin name is:
mock_components/GenericSystem
You can use it to first verify the controller configuration without immediately connecting to real hardware.
Differential drive chassis example:
<ros2_control name="DiffBotSystem" type="system">
<hardware>
<plugin>mock_components/GenericSystem</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>
Note three points:
left_wheel_jointandright_wheel_jointmust be joint names that actually exist in the URDF.- The joint names in the controller YAML must exactly match those in the URDF.
- Output wheel
velocitycommand, so the wheel joint must have avelocitycommand interface.
<param> pass hardware parameters
Real hardware typically requires parameters such as serial port name, baud rate, CAN device name, reduction ratio, which can be written in <hardware>:
<hardware>
<plugin>my_robot_hardware/MyRobotSystem</plugin>
<param name="serial_port">/dev/ttyUSB0</param>
<param name="baud_rate">115200</param>
<param name="gear_ratio">30.0</param>
</hardware>
Within the on_init(const hardware_interface::HardwareInfo & info) of the custom hardware interface, these parameters can be read via info.hardware_parameters.
Controller Manager
controller_manager configuration
controller_manager is the core node of ros2_control. Its default executable is:
ros2 run controller_manager ros2_control_node
In actual engineering, it is usually launched via launch. Controller configuration is generally written in YAML files, for example config/controllers.yaml:
controller_manager:
ros__parameters:
update_rate: 100
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
diff_drive_controller:
type: diff_drive_controller/DiffDriveController
controller_manager Common Parameters:
| parameter | default value | Explanation |
|---|---|---|
update_rate | 100 | Control loop frequency, unit: Hz |
enforce_command_limits | false | Whether to constrain commands according to joint limits in the robot description. |
handle_exceptions | true | Whether to catch exceptions in controller and hardware component operations |
hardware_components_initial_state.unconfigured | [] | Hardware components that remain unconfigured after a specified startup. |
hardware_components_initial_state.inactive | [] | Specify hardware components that remain inactive after startup |
launch method
A typical launch syntax is as follows:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
control_node = Node(
package="controller_manager",
executable="ros2_control_node",
parameters=[
{"robot_description": "<这里传入完整 URDF 字符串>"},
"config/controllers.yaml",
],
output="both",
)
return LaunchDescription([control_node])
In actual projects, robot_description are usually generated by xacro. The general workflow is:
xacro 文件 -> robot_description 参数 -> robot_state_publisher
-> ros2_control_node
Controller Loading and Debugging Commands
spawner
spawner is used to load the controller. By default, it completes the three steps of loading, configuration, and activation:
ros2 run controller_manager spawner joint_state_broadcaster
ros2 run controller_manager spawner diff_drive_controller
If the controller manager name is not the default /controller_manager, you can specify it with -c:
ros2 run controller_manager spawner joint_state_broadcaster -c /controller_manager
Common options are as follows:
| option | effect |
|---|---|
-c / --controller-manager | Specify the controller manager node name |
-p / --param-file | Load the parameter file for the controller |
--load-only | Load only, do not configure or activate. |
--inactive | Load and configure, but do not activate. |
--activate-as-group | A group of controllers activated together, suitable for cascaded controllers. |
--controller-manager-timeout | Timeout waiting for the controller manager service |
--switch-timeout | Timeout waiting for controller switching to complete |
--unload-on-kill | Unload controller when spawner exits |
hardware_spawner
hardware_spawner is used to configure or activate hardware components:
ros2 run controller_manager hardware_spawner DiffBotSystem --configure
ros2 run controller_manager hardware_spawner DiffBotSystem --activate
Common options are as follows:
| option | effect |
|---|---|
--configure | Configure the hardware component to inactive. |
--activate | Configure and activate hardware components |
-c / --controller-manager | Specify the controller manager node name |
ros2 control commands
ros2controlcli provides the ros2 control command.
View controller:
ros2 control list_controllers
View the controller's detailed interfaces:
ros2 control list_controllers --verbose
View hardware components:
ros2 control list_hardware_components
View hardware interfaces:
ros2 control list_hardware_interfaces
ros2 control list_hardware_interfaces --verbose
View available controller types:
ros2 control list_controller_types
Switch controller:
ros2 control switch_controllers --activate diff_drive_controller --deactivate old_controller
Uninstall controller:
ros2 control unload_controller diff_drive_controller
Here's a textual representation of a chain controller diagram (commonly used in robotics or cascaded control systems):
+---------+ +---------+ +---------+
| Outer | | Inner | | Plant |
Setpoint | Control | | Control | | (System)|
-------->| Law |------>| Law |------>| G(s) |----> Output
+---------+ +---------+ +---------+
| |
v v
+---------+ +---------+
| Sensor | | Sensor |
| (Pos.) | | (Vel.) |
+---------+ +---------+
^ ^
| |
+-----------------+
Explanation:
- Outer Loop Controller (e.g., position control) receives the setpoint and the feedback from the outer sensor. It computes the reference (e.g., desired velocity) for the inner loop.
- Inner Loop Controller (e.g., velocity or current control) tracks that reference using its own sensor feedback. It outputs a control signal (e.g., torque or voltage) to the plant.
- Plant is the physical system (motor, robot arm, etc.).
- Sensors provide measurements for each loop.
This type of cascade structure improves disturbance rejection and performance by separating slow (outer) and fast (inner) dynamics.
ros2 control view_controller_chains
Common Controllers
Joint State Broadcaster
joint_state_broadcaster is used to publish robot joint states. The plugin description file is:
/opt/ros/jazzy/share/joint_state_broadcaster/joint_state_plugin.xml
The plugin name is:
joint_state_broadcaster/JointStateBroadcaster
It will read state interfaces, and publish:
| Topic | type | Explanation |
|---|---|---|
/joint_states | sensor_msgs/msg/JointState | Standard joint state, often used by robot_state_publisher and RViz |
/dynamic_joint_states | control_msgs/msg/DynamicJointState | Publish all available state interfaces, including custom interfaces. |
Common Configurations:
joint_state_broadcaster:
ros__parameters:
use_local_topics: false
use_urdf_to_filter: true
publish_dynamic_joint_states: true
It is recommended that almost all robots start joint_state_broadcaster; otherwise, the robot model in RViz typically will not move.
Diff Drive Controller
diff_drive_controller used for differential drive mobile robots. The plugin description file is:
/opt/ros/jazzy/share/diff_drive_controller/diff_drive_plugin.xml
The plugin name is:
diff_drive_controller/DiffDriveController
Its function is:
- Subscribe speed command;
- Calculate the target speed of the left and right wheels based on differential kinematics;
- Write a command to the
velocitycommand interface of the left and right wheel joints; - Calculate odometry based on left and right wheel feedback;
- Optionally publish
odom -> base_linkTF.
Subscribe to topic:
| Topic | type | Explanation |
|---|---|---|
~/cmd_vel | geometry_msgs/msg/TwistStamped | speed command, using linear.x and angular.z |
If the controller name is diff_drive_controller, the default command topic is generally:
/diff_drive_controller/cmd_vel
Publish a topic:
| Topic | type | Explanation |
|---|---|---|
~/odom | nav_msgs/msg/Odometry | odometry |
/tf | tf2_msgs/msg/TFMessage | Publish when enable_odom_tf=true |
~/cmd_vel_out | geometry_msgs/msg/TwistStamped | Publish the limited speed when publish_limited_velocity=true |
Minimum configuration:
diff_drive_controller:
ros__parameters:
left_wheel_names: ["left_wheel_joint"]
right_wheel_names: ["right_wheel_joint"]
wheel_separation: 0.40
wheel_radius: 0.05
odom_frame_id: odom
base_frame_id: base_link
position_feedback: true
open_loop: false
enable_odom_tf: true
publish_rate: 50.0
If you are just using mock hardware for learning, you can first use open-loop:
diff_drive_controller:
ros__parameters:
left_wheel_names: ["left_wheel_joint"]
right_wheel_names: ["right_wheel_joint"]
wheel_separation: 0.40
wheel_radius: 0.05
position_feedback: false
open_loop: true
enable_odom_tf: true
Send speed command:
ros2 topic pub /diff_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped "{
header: {frame_id: base_link},
twist: {
linear: {x: 0.2, y: 0.0, z: 0.0},
angular: {x: 0.0, y: 0.0, z: 0.5}
}
}"
Note: Jazzy's diff_drive_controller uses TwistStamped. If an old tutorial uses geometry_msgs/msg/Twist, it may not work directly in Jazzy.
Joint Trajectory Controller
joint_trajectory_controller is commonly used in robotic arms, gimbals, and multi-joint robots. The plugin description file is:
/opt/ros/jazzy/share/joint_trajectory_controller/joint_trajectory_plugin.xml
The plugin name is:
joint_trajectory_controller/JointTrajectoryController
It executes joint space trajectories. Common inputs are:
| Input | type | Explanation |
|---|---|---|
~/joint_trajectory | trajectory_msgs/msg/JointTrajectory | Trajectory Topic |
~/follow_joint_trajectory | control_msgs/action/FollowJointTrajectory | Standard trajectory action, commonly used in MoveIt2 |
Common configurations:
arm_controller:
ros__parameters:
joints:
- joint1
- joint2
- joint3
command_interfaces:
- position
state_interfaces:
- position
- velocity
The corresponding joint in URDF must include at least:
<joint name="joint1">
<command_interface name="position"/>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
If the robot's low-level only accepts speed commands, you can change command_interfaces to velocity. If the low-level uses torque control, you can use effort, but this requires matching hardware interfaces and controller parameters.
Forward Command Controller
forward_command_controller is a controller that is very suitable for beginners to debug hardware interfaces. The plugin description file is:
/opt/ros/jazzy/share/forward_command_controller/forward_command_plugin.xml
Plugin names include:
forward_command_controller/ForwardCommandController
forward_command_controller/MultiInterfaceForwardCommandController
It doesn't perform complex control; it just directly writes the received array commands to the specified command interface of the specified joint.
Example configuration:
forward_velocity_controller:
ros__parameters:
joints:
- left_wheel_joint
- right_wheel_joint
interface_name: velocity
For someone who has just finished writing the hardware interface, it is recommended to first test using the forward controller:
ros2 control list_hardware_interfacesConfirm the interface exists;- Start
forward_velocity_controller; - Send a simple speed array;
- Check whether the hardware interface's
write()received a command.
Other controllers in ros2_controllers
After installing ros-jazzy-ros2-controllers, it will also come with many controllers. Common types are as follows:
| controller | Plugin Name | Usage | Common Hardware Interfaces |
|---|---|---|---|
| Differential chassis | diff_drive_controller/DiffDriveController | A two-wheel or multi-wheel differential drive robot that calculates left and right wheel speeds from the chassis velocity and publishes odometry. | Wheel joint velocity command, wheel joint position/velocity state |
| robotic arm trajectory | joint_trajectory_controller/JointTrajectoryController | Multi-joint trajectory execution, MoveIt2 commonly uses it to execute FollowJointTrajectory | Joint position, velocity, or effort commands, typically read position/velocity status. |
| Position group control | position_controllers/JointGroupPositionController | Directly write the position array to multiple joints without trajectory planning and kinematics. | multiple joint position commands |
| Speed group control | velocity_controllers/JointGroupVelocityController | Directly write the velocity array to multiple joints, commonly used for testing wheels or velocity-type actuators. | multiple joint velocity commands |
| torque group control | effort_controllers/JointGroupEffortController | Directly write the effort array to multiple joints, suitable for torque control or simple testing. | multiple joint effort commands |
| Command Forwarding | forward_command_controller/ForwardCommandController | Universal command forwarder, can select a specific command interface | determined by the interface_name parameter |
| Multi-interface command forwarding | forward_command_controller/MultiInterfaceForwardCommandController | Simultaneously forward commands to multiple interfaces, suitable for complex hardware debugging. | Multiple Command Interfaces for Multiple Joints |
| gripper | parallel_gripper_action_controller/GripperActionController | Parallel gripper action control, suitable for grippers with two-finger linkage. | Gripper joint position or related interfaces |
| Ackermann | ackermann_steering_controller/AckermannSteeringController | Car-type Ackerman chassis, usually front-wheel steering, rear-wheel drive. | Steering joint position command, drive wheel velocity command |
| Mecanum | mecanum_drive_controller/MecanumDriveController | Four-wheel Mecanum chassis, capable of forward/backward, sideways, and rotational movement. | four wheel joints velocity command |
| tricycle | tricycle_controller/TricycleController | Three-wheel chassis, typically one steering wheel plus a drive wheel. | Steering Joint and Drive Wheel Commands |
| omni wheel | omni_wheel_drive_controller/OmniWheelDriveController | Three or more omnidirectional wheel chassis, supporting planar omnidirectional movement | Multiple wheel joints velocity command |
| PID | pid_controller/PidController | A PID controller based on control_toolbox, usable in a controller chain. | Read status/reference, output the command after PID. |
| IMU Publication | imu_sensor_broadcaster/IMUSensorBroadcaster | Publish the hardware status interface as an IMU message. | Read IMU-related state interfaces |
| Force/Torque Publication | force_torque_sensor_broadcaster/ForceTorqueSensorBroadcaster | Publish six-axis force sensor data | Read force/torque state interfaces |
| Distance Sensor Release | range_sensor_broadcaster/RangeSensorBroadcaster | Publish distance sensor data | Read the range state interface |
| Battery Status Publication | battery_state_broadcaster/BatteryStateBroadcaster | Publish battery voltage, current, and charge level status | Read battery-related state interfaces |
| General Status Publishing | state_interfaces_broadcaster/StateInterfacesBroadcaster | Publish specified state interfaces, suitable for debugging custom states. | Read user-specified state interfaces |
If you just want to verify whether the hardware interface can receive commands, prioritize using forward_command_controller, position_controllers, velocity_controllers, or effort_controllers. If you already have a clear kinematic model, then choose diff_drive_controller, mecanum_drive_controller, ackermann_steering_controller, tricycle_controller, or omni_wheel_drive_controller. Robotic arms and multi-joint mechanisms typically start by learning from joint_trajectory_controller.
When choosing a controller, first ask yourself three questions:
- Is my robot kinematics officially supported?
- Does my hardware receive position, velocity, or effort?
- Do I need the controller to calculate kinematics itself, or does it just need to forward commands to the hardware?
If the official controller can meet the requirements, prioritize using the official controller. Only when the kinematics is special, the control law is special, or special sensors need to be integrated, is it recommended to write a custom controller.
A Minimal Bringup for a Differential Drive Chassis
controllers.yaml
controller_manager:
ros__parameters:
update_rate: 100
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
diff_drive_controller:
type: diff_drive_controller/DiffDriveController
joint_state_broadcaster:
ros__parameters:
use_local_topics: false
publish_dynamic_joint_states: true
diff_drive_controller:
ros__parameters:
left_wheel_names: ["left_wheel_joint"]
right_wheel_names: ["right_wheel_joint"]
wheel_separation: 0.40
wheel_radius: 0.05
odom_frame_id: odom
base_frame_id: base_link
position_feedback: false
open_loop: true
enable_odom_tf: true
publish_limited_velocity: true
URDF Key Fragment
<ros2_control name="DiffBotSystem" type="system">
<hardware>
<plugin>mock_components/GenericSystem</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>
Startup sequence
Recommended order:
- Start
robot_state_publisher. - Start
ros2_control_node. - Start
joint_state_broadcaster. - Start
diff_drive_controller. - Publish
/diff_drive_controller/cmd_vel. - View
/joint_states,/diff_drive_controller/odom, and/tf.
The debugging commands are as follows:
ros2 control list_hardware_components
ros2 control list_hardware_interfaces
ros2 control list_controllers --verbose
ros2 topic echo /joint_states
ros2 topic echo /diff_drive_controller/odom
Custom hardware interface
When is it necessary to write a hardware interface
If your robot uses a real motor driver board, you usually need to write hardware interfaces. Typical scenarios include:
- Serial Port Motor Driver Board
- CAN bus motor
- EtherCAT drive;
- Self-developed STM32 microcontroller;
- Custom sensor data;
- Non-Gazebo custom simulator.
If you just want to verify the controller configuration, you can first use mock_components/GenericSystem. If you want to connect to the actual device, you need to inherit hardware_interface::SystemInterface.
SystemInterface Lifecycle
The commonly used functions for system hardware interfaces in Jazzy are as follows:
| function | Call Timing | effect |
|---|---|---|
on_init(const HardwareInfo & info) | When loading hardware | Read joint, interface, and param from URDF |
on_configure(...) | When configuring hardware | Open the serial port, initialize the driver board, allocate resources. |
on_activate(...) | When activating hardware | Enable the motor, clear command |
on_deactivate(...) | When disabling hardware | Motor disabled, output stopped. |
read(const rclcpp::Time &, const rclcpp::Duration &) | each control cycle | Read hardware state and write to state interfaces |
write(const rclcpp::Time &, const rclcpp::Duration &) | each control cycle | Read command interfaces and write to hardware |
In Jazzy, if the interface has already been declared in the URDF's <ros2_control>, SystemInterface will create the interface based on the URDF by default. In custom hardware, you can use:
set_state("left_wheel_joint/position", value);
set_state("left_wheel_joint/velocity", value);
double cmd = get_command("left_wheel_joint/velocity");
These functions come from Jazzy's hardware_interface::SystemInterface.
Minimal Hardware Interface Framework
Below is a hardware interface skeleton for a differential drive chassis. It shows the structure and does not contain a specific serial port protocol:
#include <string>
#include "hardware_interface/system_interface.hpp"
#include "hardware_interface/types/hardware_interface_return_values.hpp"
#include "rclcpp/rclcpp.hpp"
namespace my_robot_hardware
{
class MyDiffBotSystem : public hardware_interface::SystemInterface
{
public:
hardware_interface::CallbackReturn on_init(const hardware_interface::HardwareInfo & info) override
{
if (hardware_interface::SystemInterface::on_init(info) !=
hardware_interface::CallbackReturn::SUCCESS)
{
return hardware_interface::CallbackReturn::ERROR;
}
serial_port_ = info_.hardware_parameters.at("serial_port");
return hardware_interface::CallbackReturn::SUCCESS;
}
hardware_interface::CallbackReturn on_configure(const rclcpp_lifecycle::State &) override
{
// 在这里打开串口、CAN 或网络连接。
return hardware_interface::CallbackReturn::SUCCESS;
}
hardware_interface::CallbackReturn on_activate(const rclcpp_lifecycle::State &) override
{
set_command("left_wheel_joint/velocity", 0.0);
set_command("right_wheel_joint/velocity", 0.0);
return hardware_interface::CallbackReturn::SUCCESS;
}
hardware_interface::return_type read(
const rclcpp::Time &, const rclcpp::Duration &) override
{
// 从编码器读取真实位置和速度。
set_state("left_wheel_joint/position", left_position_);
set_state("left_wheel_joint/velocity", left_velocity_);
set_state("right_wheel_joint/position", right_position_);
set_state("right_wheel_joint/velocity", right_velocity_);
return hardware_interface::return_type::OK;
}
hardware_interface::return_type write(
const rclcpp::Time &, const rclcpp::Duration &) override
{
const double left_cmd = get_command("left_wheel_joint/velocity");
const double right_cmd = get_command("right_wheel_joint/velocity");
// 把 left_cmd 和 right_cmd 写给电机驱动板。
return hardware_interface::return_type::OK;
}
private:
std::string serial_port_;
double left_position_ = 0.0;
double left_velocity_ = 0.0;
double right_position_ = 0.0;
double right_velocity_ = 0.0;
};
} // namespace my_robot_hardware
Plugin Export:
#include "pluginlib/class_list_macros.hpp"
PLUGINLIB_EXPORT_CLASS(
my_robot_hardware::MyDiffBotSystem,
hardware_interface::SystemInterface)
Corresponding plugin XML:
<library path="my_robot_hardware">
<class
name="my_robot_hardware/MyDiffBotSystem"
type="my_robot_hardware::MyDiffBotSystem"
base_class_type="hardware_interface::SystemInterface">
<description>My differential drive robot hardware interface.</description>
</class>
</library>
URDF 中使用:
<hardware>
<plugin>my_robot_hardware/MyDiffBotSystem</plugin>
<param name="serial_port">/dev/ttyUSB0</param>
</hardware>
Hardware interface debugging sequence
When writing hardware interfaces, it is recommended to debug in this order:
- First, without using a real motor, confirm that
pluginlibcan load the hardware plugin. - Start
ros2_control_node, runros2 control list_hardware_components. - Confirm that the hardware components can enter
inactive. - Confirm that
list_hardware_interfacescontains the expected command/state interfaces. - Use
forward_command_controllerto send commands directly to the command interface. - In
write(), print the command once to confirm that the controller has indeed written it. - Connect the real motor, but first lift the wheels or turn off high-power output.
- Confirm that
read()can correctly update/joint_states. - Finally, connect
diff_drive_controllerorjoint_trajectory_controller.
Custom Kinematic Controller
When do you need to write a controller?
It is not recommended to write a custom controller at the beginning. Prioritize determining whether the official controller is sufficient:
| Requirement | Recommended solution |
|---|---|
| Differential chassis | diff_drive_controller |
| robotic arm trajectory | joint_trajectory_controller |
| I just want to directly write joint positions. | position_controllers/JointGroupPositionController |
| Just want to set joint velocity directly | velocity_controllers/JointGroupVelocityController |
| just want to test hardware commands | forward_command_controller |
| Special chassis kinematics | custom controller |
| Custom impedance, admittance, and force control strategies | Custom controller or extension based on admittance_controller |
If your robot uses four-wheel differential drive, Mecanum wheels, steerable wheels, a special parallel mechanism, or is a legged robot, the official controller may not fully match. In that case, you need to write a custom controller.
ControllerInterface Key Functions
Normal Controller Inheritance in Jazzy:
controller_interface::ControllerInterface
Must pay attention to these functions:
| function | effect |
|---|---|
on_init() | Declare parameters, initialize non-real-time objects. |
command_interface_configuration() | Declare which command interfaces to occupy. |
state_interface_configuration() | Declare which state interfaces to read |
on_configure() | read parameters, create subscribers, initialize cache |
on_activate() | Check interface before activation, clear command |
on_deactivate() | Stop the controller, release the state. |
update(time, period) | Real-time control loop, compute and write commands. |
Among them, update() executes in the real-time control loop and should not do these things:
- Do not dynamically allocate a large amount of memory;
- Do not use blocking I/O;
- Do not wait for the lock;
- Don't print logs too frequently;
- Do not create publisher/subscriber inside it;
- Do not perform potentially blocking parameter reads.
When subscribing to a ROS topic, the common practice is to write to realtime_tools::RealtimeBuffer in the ordinary callback, and only read this buffer in update().
An idea for a custom chassis controller
假设要写一个自定义底盘控制器,输入是 cmd_vel,输出是四个轮子的速度命令。核心步骤是:
- Declare the joint names of the four wheels in the parameters.
command_interface_configuration()returns four<joint>/velocity.state_interface_configuration()returns the<joint>/positionor<joint>/velocitythat need to be read.on_configure()createscmd_velsubscriber.- The subscriber writes the latest speed command to the real-time buffer.
update()Read the latest speed command.- Calculate the speed of each wheel based on the kinematic model.
- Write the result into
command_interfaces_.
The pseudocode is as follows:
controller_interface::InterfaceConfiguration
MyKinematicsController::command_interface_configuration() const
{
controller_interface::InterfaceConfiguration config;
config.type = controller_interface::interface_configuration_type::INDIVIDUAL;
config.names = {
"front_left_wheel_joint/velocity",
"front_right_wheel_joint/velocity",
"rear_left_wheel_joint/velocity",
"rear_right_wheel_joint/velocity"
};
return config;
}
controller_interface::return_type MyKinematicsController::update(
const rclcpp::Time &, const rclcpp::Duration &)
{
const auto cmd = *command_buffer_.readFromRT();
const double vx = cmd.linear.x;
const double wz = cmd.angular.z;
const double left = vx - wz * wheel_separation_ * 0.5;
const double right = vx + wz * wheel_separation_ * 0.5;
command_interfaces_[0].set_value(left / wheel_radius_);
command_interfaces_[1].set_value(right / wheel_radius_);
command_interfaces_[2].set_value(left / wheel_radius_);
command_interfaces_[3].set_value(right / wheel_radius_);
return controller_interface::return_type::OK;
}
This code is just the core logic of the kinematic controller. A complete controller also requires parameter declarations, subscribers, interface ordering checks, timeout protection, plugin export, and CMake configuration.
Custom controller package.xml
Common dependencies are as follows:
<depend>controller_interface</depend>
<depend>hardware_interface</depend>
<depend>pluginlib</depend>
<depend>rclcpp</depend>
<depend>rclcpp_lifecycle</depend>
<depend>realtime_tools</depend>
<depend>geometry_msgs</depend>
If you want to publish odometry, you also need:
<depend>nav_msgs</depend>
<depend>tf2</depend>
<depend>tf2_msgs</depend>
<depend>tf2_ros</depend>
Custom Controller CMakeLists
The core approach is as follows:
add_library(my_kinematics_controller SHARED
src/my_kinematics_controller.cpp
)
ament_target_dependencies(my_kinematics_controller
controller_interface
hardware_interface
pluginlib
rclcpp
rclcpp_lifecycle
realtime_tools
geometry_msgs
)
pluginlib_export_plugin_description_file(
controller_interface
my_kinematics_controller.xml
)
install(
TARGETS my_kinematics_controller
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(
FILES my_kinematics_controller.xml
DESTINATION share/${PROJECT_NAME}
)
Plugin XML:
<library path="my_kinematics_controller">
<class
name="my_kinematics_controller/MyKinematicsController"
type="my_kinematics_controller::MyKinematicsController"
base_class_type="controller_interface::ControllerInterface">
<description>My custom mobile robot kinematics controller.</description>
</class>
</library>
Loading in YAML:
controller_manager:
ros__parameters:
my_kinematics_controller:
type: my_kinematics_controller/MyKinematicsController
my_kinematics_controller:
ros__parameters:
wheel_radius: 0.05
wheel_separation: 0.40
Ordinary controller or cascade controller?
In Jazzy, controllers fall into two categories:
| type | base class | Applicable Scenarios |
|---|---|---|
| generic controller | controller_interface::ControllerInterface | Receive commands from ROS topics/actions, directly write hardware command interface |
| cascaded controller | controller_interface::ChainableControllerInterface | The upstream controller outputs a reference interface, and the downstream controller continues processing. |
Beginners are advised to first write an ordinary controller. Only when you need multiple controllers in series (cascade), then study cascaded controllers. For example:
导航速度命令 -> 运动学控制器 -> PID 控制器 -> 硬件接口
A chain controller can be used:
ros2 control view_controller_chains
View the controller link.
Gazebo and ROS2 Control
ros2_control itself is not a simulator. It is just a control framework. To use it in Gazebo Sim, the gz_ros2_control plugin is required.
Installation:
sudo apt install ros-jazzy-gz-ros2-control ros-jazzy-gz-ros2-control-demos
During Gazebo integration, you still need:
- In URDF
<ros2_control>; - Controller YAML;
controller_manager;joint_state_broadcasterand specific controller.
The difference is that <hardware><plugin>...</plugin></hardware> is usually replaced with the corresponding hardware plugin for Gazebo, rather than mock_components/GenericSystem or your actual hardware plugin.
In Jazzy, the hardware plugin description file for gz_ros2_control is:
/opt/ros/jazzy/share/gz_ros2_control/gz_hardware_plugins.xml
The Gazebo Sim system hardware plugin name is:
gz_ros2_control/GazeboSimSystem
In URDF or xacro, the key fragment of <ros2_control> is usually written as:
<ros2_control name="GazeboSimSystem" 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>
Also, add Gazebo system plugins to the robot description, so that Gazebo Sim creates the ros2_control resource manager and loads the controller parameters:
<gazebo>
<plugin filename="gz_ros2_control-system" name="gz_ros2_control::GazeboSimROS2ControlPlugin">
<parameters>$(find my_robot_bringup)/config/controllers.yaml</parameters>
</plugin>
</gazebo>
After installing gz_ros2_control_demos, you can directly refer to the official examples:
ros2 launch gz_ros2_control_demos diff_drive_example.launch.py
ros2 launch gz_ros2_control_demos cart_example_position.launch.py
ros2 launch gz_ros2_control_demos cart_example_velocity.launch.py
ros2 launch gz_ros2_control_demos cart_example_effort.launch.py
ros2 launch gz_ros2_control_demos mecanum_drive_example.launch.py
ros2 launch gz_ros2_control_demos ackermann_drive_example.launch.py
ros2 launch gz_ros2_control_demos tricycle_drive_example.launch.py
These example files are installed at:
/opt/ros/jazzy/share/gz_ros2_control_demos
Important files include:
| file | effect |
|---|---|
urdf/test_diff_drive.xacro.urdf | Gazebo differential drive chassis URDF/xacro example |
config/diff_drive_controller.yaml | Example of differential controller parameters |
launch/diff_drive_example.launch.py | Complete Differential Chassis Startup Example |
urdf/test_mecanum_drive.xacro.urdf | Mecanum chassis example |
config/mecanum_drive_controller.yaml | Mecanum controller parameter example |
When learning Gazebo integration, don't just look at the launch files. It's better to read in this order:
- First look at the
<ros2_control>and<gazebo>plugins inurdf/test_diff_drive.xacro.urdf. - Take another look at the controller parameters in
config/diff_drive_controller.yaml. - Finally, see how
launch/diff_drive_example.launch.pyconnects the robot description, Gazebo, and spawner.
Recommended learning order:
- First, use
mock_components/GenericSystemto get the controller running. - Continuing with Gazebo.
- Finally, connect to real hardware.
This makes it easier to locate problems when errors occur.
Frequently Asked Questions
Failed to load controller
Please provide the Simplified Chinese Markdown fragment you'd like me to translate.
ros2 control list_controller_types
If your controller type is not in the list, usually:
- Plugin XML not installed;
pluginlib_export_plugin_description_file()typo;- The
typefield in YAML is inconsistent withnamein the plugin XML; - No source workspace:
source install/setup.bash
Controller activation failed
View interface:
ros2 control list_hardware_interfaces --verbose
ros2 control list_controllers --verbose
Common causes:
- The command interface required by the controller does not exist;
- The state interface required by the controller does not exist;
- Another controller has already occupied the same command interface;
- The joint names in the URDF and the YAML are inconsistent;
- Hardware components have not yet been activated.
/joint_states has no data
Check:
ros2 control list_controllers
ros2 topic echo /dynamic_joint_states
ros2 topic echo /joint_states
Common causes:
- Did not start
joint_state_broadcaster; - The hardware interface does not have state interfaces;
- When
use_urdf_to_filter=true, there is no corresponding joint in the URDF; - Hardware
read()has not updated state.
cmd_vel sent but the chassis does not move.
Jazzy's diff_drive_controller default subscription TwistStamped:
ros2 topic info /diff_drive_controller/cmd_vel
Confirm the message type is:
geometry_msgs/msg/TwistStamped
我注意到了你的简短请求 "还要检查:",但看起来缺少需要翻译或检查的文本内容。为了更好地协助你,请提供具体的简体中文 Markdown 片段,我会严格按照你之前给出的指示进行翻译:保留占位符、术语、标记结构和技术名称不变,仅将中文译为地道美式英语。
如果你有新的片段需要检查或翻译,请直接附上文本。
- Is the topic name
/diff_drive_controller/cmd_vel; - Whether the controller is active;
- Does the wheel joint
velocitycommand interface exist; left_wheel_names,right_wheel_namesare they written correctly;cmd_vel_timeoutis too short;- Whether the actual hardware interface
write()truly sends commands to the motor.
Odometry direction is incorrect.
Please provide the Simplified Chinese Markdown fragment you'd like me to translate.
- Whether the left and right wheel joints are written reversed;
- Whether the positive direction of the wheel is consistent with the URDF axis direction;
wheel_radiusis it correct;wheel_separationis it correct;- Is the encoder position unit radians;
- Is the speed unit in the hardware interface rad/s?
TF conflict
If another node in the system has already published odom -> base_link, do not let diff_drive_controller publish the same TF:
diff_drive_controller:
ros__parameters:
enable_odom_tf: false
Learning Path Suggestions
Suggested learning order:
- Understand command interface and state interface.
- Use
mock_components/GenericSystemto getjoint_state_broadcasterrunning. - Use
forward_command_controllerto directly write joint commands. - Run the differential chassis using
diff_drive_controller. - Run through the robotic arm or multi-joint model using
joint_trajectory_controller. - Write a minimal
SystemInterface, first just print the command. - Connect to a real serial port/CAN, let
read()update/joint_states. - Reconnect the official controller.
- Finally, write a custom kinematics controller.
Don't start by writing the hardware interface, kinematics controller, simulation plugin, and navigation all at once. If something goes wrong, it's hard to tell whether the issue is with URDF, YAML, the controller, the hardware interface, or the underlying communication.
references
- ROS2 Control Jazzy official documentation: https://control.ros.org/jazzy/index.html
- Getting Started:https://control.ros.org/jazzy/doc/getting_started/getting_started.html
- Controller Manager:https://control.ros.org/jazzy/doc/ros2_control/controller_manager/doc/userdoc.html
- Hardware Interface:https://control.ros.org/jazzy/doc/ros2_control/hardware_interface/doc/hardware_interface_types_userdoc.html
- Joint State Broadcaster:https://control.ros.org/jazzy/doc/ros2_controllers/joint_state_broadcaster/doc/userdoc.html
- Diff Drive Controller:https://control.ros.org/jazzy/doc/ros2_controllers/diff_drive_controller/doc/userdoc.html
- Joint Trajectory Controller:https://control.ros.org/jazzy/doc/ros2_controllers/joint_trajectory_controller/doc/userdoc.html
- ros2_control GitHub:https://github.com/ros-controls/ros2_control
- ros2_controllers GitHub:https://github.com/ros-controls/ros2_controllers