第 15 節

ROS2_Control

0瀏覽次數0訪問次數--跳出率--平均停留

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_vel into 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 -> write control 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 packagepathExplanation
ros2_control/opt/ros/jazzy/share/ros2_controlros2_control metapackage
controller_manager/opt/ros/jazzy/share/controller_managerController manager, providing tools such as ros2_control_node, spawner, etc.
controller_interface/opt/ros/jazzy/share/controller_interfaceThe base interface that a custom controller should inherit
hardware_interface/opt/ros/jazzy/share/hardware_interfaceBase interface to inherit for custom hardware interfaces
ros2_controllers/opt/ros/jazzy/share/ros2_controllersOfficial Common Controller Metapackage
ros2controlcli/opt/ros/jazzy/share/ros2controlcliProvide 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_manager is 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 nameUnderstanding ChineseMain UsesTypical InputTypical output
joint_state_broadcasterJoint State PublisherRead hardware state interfaces, publish /joint_states and /dynamic_joint_statesStatus interfaces such as position, velocity, effort, etc. in the hardwareROS topic, no hardware commands
diff_drive_controllerdifferential chassis controllerDifferential drive cargeometry_msgs/msg/TwistStamped speed commandLeft and right wheels velocity command interface, also publish odom
joint_trajectory_controllerJoint Trajectory ControllerExecute joint-space trajectories for robotic arms, pan-tilts, and multi-joint mechanisms.trajectory_msgs/msg/JointTrajectory or FollowJointTrajectory actionA set of joints' position, velocity, or effort commands
forward_command_controllerCommand Forwarding ControllerForward the received array commands directly to the hardware interface, suitable for testing hardware.std_msgs/msg/Float64MultiArraythe specified command interface for the specified joint
position_controllersPosition Command Group ControllerDirectly send position targets to multiple joints.position arraycommand interface for multiple joints position
velocity_controllersVelocity Command Group ControllerDirectly issue velocity targets to multiple joints.speed arraycommand interface for multiple joints velocity
effort_controllersForce/Torque Command Group ControllerDirectly issue effort targets to multiple jointseffort arraycommand interface for multiple joints effort
pid_controllerPID controllerImplement PID closed-loop control in the ros2_control chainreference interface or controller chain inputCommand Interface After PID Calculation
mecanum_drive_controllerMecanum wheel chassis controllerControl a four-wheel Mecanum chassis to achieve forward/backward, strafe, and rotation.Usually a chassis speed commandFour Mecanum wheels' velocity command interface
ackermann_steering_controllerAckermann steering controllerControl of car-like front-wheel steering, rear-wheel drive structureChassis speed/steering related commandsSteering joint position command and drive wheel speed command
tricycle_controllertricycle controllerControl a three-wheel chassis with a steering wheel and a drive wheel.chassis speed commandSteering Joint and Drive Wheel Commands
omni_wheel_drive_controllerOmnidirectional Wheel Chassis ControllerControl an omnidirectional chassis composed of three or more omni wheels.chassis speed commandMultiple 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 nameMeaning
positionPosition, commonly used for joint angles or linear displacement
velocitySpeed, often used for wheel speed or joint speed.
effortforce or torque, commonly used in torque control
accelerationacceleration
currentcurrent
temperaturetemperature

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:

Stageexecutoreffect
read()hardware interfaceRead status from motors, encoders, sensors, or simulators.
update()controllerCalculate a new command based on the state and target.
write()hardware interfaceWrite 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_vel and 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:

StatusMeaning
unconfiguredLoaded, but not yet configured.
inactiveConfigured but not participating in control loop output.
activeActivated and participating in the control loop
finalizedEnded

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:

typeCorresponds to the C++ base classIntended Audience
systemhardware_interface::SystemInterfaceMulti-joint robot, chassis, robotic arm
sensorhardware_interface::SensorInterfaceread-only sensor
actuatorhardware_interface::ActuatorInterfaceSingle 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_joint and right_wheel_joint must 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 velocity command, so the wheel joint must have a velocity command 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:

parameterdefault valueExplanation
update_rate100Control loop frequency, unit: Hz
enforce_command_limitsfalseWhether to constrain commands according to joint limits in the robot description.
handle_exceptionstrueWhether 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:

optioneffect
-c / --controller-managerSpecify the controller manager node name
-p / --param-fileLoad the parameter file for the controller
--load-onlyLoad only, do not configure or activate.
--inactiveLoad and configure, but do not activate.
--activate-as-groupA group of controllers activated together, suitable for cascaded controllers.
--controller-manager-timeoutTimeout waiting for the controller manager service
--switch-timeoutTimeout waiting for controller switching to complete
--unload-on-killUnload 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:

optioneffect
--configureConfigure the hardware component to inactive.
--activateConfigure and activate hardware components
-c / --controller-managerSpecify 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:

TopictypeExplanation
/joint_statessensor_msgs/msg/JointStateStandard joint state, often used by robot_state_publisher and RViz
/dynamic_joint_statescontrol_msgs/msg/DynamicJointStatePublish 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 velocity command interface of the left and right wheel joints;
  • Calculate odometry based on left and right wheel feedback;
  • Optionally publish odom -> base_link TF.

Subscribe to topic:

TopictypeExplanation
~/cmd_velgeometry_msgs/msg/TwistStampedspeed 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:

TopictypeExplanation
~/odomnav_msgs/msg/Odometryodometry
/tftf2_msgs/msg/TFMessagePublish when enable_odom_tf=true
~/cmd_vel_outgeometry_msgs/msg/TwistStampedPublish 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:

InputtypeExplanation
~/joint_trajectorytrajectory_msgs/msg/JointTrajectoryTrajectory Topic
~/follow_joint_trajectorycontrol_msgs/action/FollowJointTrajectoryStandard 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_interfaces Confirm 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:

controllerPlugin NameUsageCommon Hardware Interfaces
Differential chassisdiff_drive_controller/DiffDriveControllerA 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 trajectoryjoint_trajectory_controller/JointTrajectoryControllerMulti-joint trajectory execution, MoveIt2 commonly uses it to execute FollowJointTrajectoryJoint position, velocity, or effort commands, typically read position/velocity status.
Position group controlposition_controllers/JointGroupPositionControllerDirectly write the position array to multiple joints without trajectory planning and kinematics.multiple joint position commands
Speed group controlvelocity_controllers/JointGroupVelocityControllerDirectly write the velocity array to multiple joints, commonly used for testing wheels or velocity-type actuators.multiple joint velocity commands
torque group controleffort_controllers/JointGroupEffortControllerDirectly write the effort array to multiple joints, suitable for torque control or simple testing.multiple joint effort commands
Command Forwardingforward_command_controller/ForwardCommandControllerUniversal command forwarder, can select a specific command interfacedetermined by the interface_name parameter
Multi-interface command forwardingforward_command_controller/MultiInterfaceForwardCommandControllerSimultaneously forward commands to multiple interfaces, suitable for complex hardware debugging.Multiple Command Interfaces for Multiple Joints
gripperparallel_gripper_action_controller/GripperActionControllerParallel gripper action control, suitable for grippers with two-finger linkage.Gripper joint position or related interfaces
Ackermannackermann_steering_controller/AckermannSteeringControllerCar-type Ackerman chassis, usually front-wheel steering, rear-wheel drive.Steering joint position command, drive wheel velocity command
Mecanummecanum_drive_controller/MecanumDriveControllerFour-wheel Mecanum chassis, capable of forward/backward, sideways, and rotational movement.four wheel joints velocity command
tricycletricycle_controller/TricycleControllerThree-wheel chassis, typically one steering wheel plus a drive wheel.Steering Joint and Drive Wheel Commands
omni wheelomni_wheel_drive_controller/OmniWheelDriveControllerThree or more omnidirectional wheel chassis, supporting planar omnidirectional movementMultiple wheel joints velocity command
PIDpid_controller/PidControllerA PID controller based on control_toolbox, usable in a controller chain.Read status/reference, output the command after PID.
IMU Publicationimu_sensor_broadcaster/IMUSensorBroadcasterPublish the hardware status interface as an IMU message.Read IMU-related state interfaces
Force/Torque Publicationforce_torque_sensor_broadcaster/ForceTorqueSensorBroadcasterPublish six-axis force sensor dataRead force/torque state interfaces
Distance Sensor Releaserange_sensor_broadcaster/RangeSensorBroadcasterPublish distance sensor dataRead the range state interface
Battery Status Publicationbattery_state_broadcaster/BatteryStateBroadcasterPublish battery voltage, current, and charge level statusRead battery-related state interfaces
General Status Publishingstate_interfaces_broadcaster/StateInterfacesBroadcasterPublish 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:

  1. Start robot_state_publisher.
  2. Start ros2_control_node.
  3. Start joint_state_broadcaster.
  4. Start diff_drive_controller.
  5. Publish /diff_drive_controller/cmd_vel.
  6. 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:

functionCall Timingeffect
on_init(const HardwareInfo & info)When loading hardwareRead joint, interface, and param from URDF
on_configure(...)When configuring hardwareOpen the serial port, initialize the driver board, allocate resources.
on_activate(...)When activating hardwareEnable the motor, clear command
on_deactivate(...)When disabling hardwareMotor disabled, output stopped.
read(const rclcpp::Time &, const rclcpp::Duration &)each control cycleRead hardware state and write to state interfaces
write(const rclcpp::Time &, const rclcpp::Duration &)each control cycleRead 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:

  1. First, without using a real motor, confirm that pluginlib can load the hardware plugin.
  2. Start ros2_control_node, run ros2 control list_hardware_components.
  3. Confirm that the hardware components can enter inactive.
  4. Confirm that list_hardware_interfaces contains the expected command/state interfaces.
  5. Use forward_command_controller to send commands directly to the command interface.
  6. In write(), print the command once to confirm that the controller has indeed written it.
  7. Connect the real motor, but first lift the wheels or turn off high-power output.
  8. Confirm that read() can correctly update /joint_states.
  9. Finally, connect diff_drive_controller or joint_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:

RequirementRecommended solution
Differential chassisdiff_drive_controller
robotic arm trajectoryjoint_trajectory_controller
I just want to directly write joint positions.position_controllers/JointGroupPositionController
Just want to set joint velocity directlyvelocity_controllers/JointGroupVelocityController
just want to test hardware commandsforward_command_controller
Special chassis kinematicscustom controller
Custom impedance, admittance, and force control strategiesCustom 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:

functioneffect
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,输出是四个轮子的速度命令。核心步骤是:

  1. Declare the joint names of the four wheels in the parameters.
  2. command_interface_configuration() returns four <joint>/velocity.
  3. state_interface_configuration() returns the <joint>/position or <joint>/velocity that need to be read.
  4. on_configure() creates cmd_vel subscriber.
  5. The subscriber writes the latest speed command to the real-time buffer.
  6. update() Read the latest speed command.
  7. Calculate the speed of each wheel based on the kinematic model.
  8. 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:

typebase classApplicable Scenarios
generic controllercontroller_interface::ControllerInterfaceReceive commands from ROS topics/actions, directly write hardware command interface
cascaded controllercontroller_interface::ChainableControllerInterfaceThe 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_broadcaster and 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:

fileeffect
urdf/test_diff_drive.xacro.urdfGazebo differential drive chassis URDF/xacro example
config/diff_drive_controller.yamlExample of differential controller parameters
launch/diff_drive_example.launch.pyComplete Differential Chassis Startup Example
urdf/test_mecanum_drive.xacro.urdfMecanum chassis example
config/mecanum_drive_controller.yamlMecanum controller parameter example

When learning Gazebo integration, don't just look at the launch files. It's better to read in this order:

  1. First look at the <ros2_control> and <gazebo> plugins in urdf/test_diff_drive.xacro.urdf.
  2. Take another look at the controller parameters in config/diff_drive_controller.yaml.
  3. Finally, see how launch/diff_drive_example.launch.py connects the robot description, Gazebo, and spawner.

Recommended learning order:

  1. First, use mock_components/GenericSystem to get the controller running.
  2. Continuing with Gazebo.
  3. 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 type field in YAML is inconsistent with name in 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 velocity command interface exist;
  • left_wheel_names, right_wheel_names are they written correctly;
  • cmd_vel_timeout is 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_radius is it correct;
  • wheel_separation is 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:

  1. Understand command interface and state interface.
  2. Use mock_components/GenericSystem to get joint_state_broadcaster running.
  3. Use forward_command_controller to directly write joint commands.
  4. Run the differential chassis using diff_drive_controller.
  5. Run through the robotic arm or multi-joint model using joint_trajectory_controller.
  6. Write a minimal SystemInterface, first just print the command.
  7. Connect to a real serial port/CAN, let read() update /joint_states.
  8. Reconnect the official controller.
  9. 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

音乐页