ZERO Notificatons

NO Feedback yet!!

okay

xparo
X.P.A.R.O



project - Differential Drive Calculator



In ROS 2, everything that wants a robot to move, from a teleop keyboard to the Nav2 navigation stack, publishes a velocity command on a cmd_vel topic. Something has to turn that command into two wheel speeds and send them to the motors. This guide follows a command from the topic to the wheels, using the standard diff_drive_controller from ros2_control, with the parameter names and message types checked against ROS 2 Jazzy.

The command

A velocity command carries a linear and an angular velocity. A differential-drive robot uses only two of the six fields: linear.x (forward speed in m/s) and angular.z (turn rate in rad/s, positive to the left). The others are ignored, because the robot cannot move sideways or tilt.

On ROS 2 Jazzy, diff_drive_controller subscribes to ~/cmd_vel as geometry_msgs/TwistStamped only; there is no option to accept a plain Twist. A tool publishing Twist appears to work (the topic shows up) but the robot never moves. Teleop keyboard needs -p stamped:=true; Nav2's controller server has an enable_stamped_cmd_vel parameter; and the twist_stamper package converts between the two when a tool has no option.

From command to wheel speeds

The controller applies the inverse kinematics explained in our kinematics guide:

ω_left = (v − ω × separation/2) ÷ radius ω_right = (v + ω × separation/2) ÷ radius

and writes the two results, in radians per second, to the wheels' velocity command interfaces. Your hardware interface, or the microcontroller behind it, then runs a speed loop on each motor to hold those speeds, as in our guide to closed-loop motor speed control.

Configuring diff_drive_controller

A minimal configuration for a robot with 100 mm wheels 300 mm apart:

controller_manager:
  ros__parameters:
    update_rate: 100
    diff_drive_controller:
      type: diff_drive_controller/DiffDriveController
    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster

diff_drive_controller:
  ros__parameters:
    left_wheel_names: ["left_wheel_joint"]
    right_wheel_names: ["right_wheel_joint"]
    wheel_separation: 0.30
    wheel_radius: 0.05
    odom_frame_id: odom
    base_frame_id: base_link
    enable_odom_tf: true
    publish_rate: 50.0
    cmd_vel_timeout: 0.5
    linear.x.max_velocity: 1.0
    linear.x.max_acceleration: 0.8
    angular.z.max_velocity: 2.0
    angular.z.max_acceleration: 3.0
  • left_wheel_names / right_wheel_names must match the joint names in your URDF and hardware interface. Lists allow several wheels per side, such as skid-steer robots.
  • wheel_separation and wheel_radius are the kinematic constants. The wheel_separation_multiplier, left_wheel_radius_multiplier and right_wheel_radius_multiplier parameters apply calibration corrections without editing the measured values; see calibrating wheel radius and separation.
  • cmd_vel_timeout (seconds): if no command arrives for this long, the controller commands zero. Keep it short; it is your protection against a crashed planner.
  • Limits: the linear.x.* and angular.z.* groups cap velocity, acceleration and jerk, discussed in our guide to velocity and acceleration limits.

Odometry comes back the other way

The same controller reads the wheels' measured positions (or velocities, if position_feedback is false), applies the forward kinematics, integrates the robot's pose and publishes it on ~/odom as nav_msgs/Odometry, and, with enable_odom_tf, as the odom → base_link transform. If another node (for example robot_localization fusing an IMU) publishes that transform, set enable_odom_tf: false so two publishers don't fight. With open_loop: true, odometry is computed from the commanded velocities instead of the measured ones, which is only acceptable when there are no encoders.

Topic names

The controller's topics live under its own name: /diff_drive_controller/cmd_vel and /diff_drive_controller/odom. Most other software expects /cmd_vel and /odom, so remap them, typically in the launch file that starts ros2_control_node, or point Nav2 and your teleop tool at the controller's names directly.

Testing the chain

# drive forward at 0.2 m/s while turning left at 0.5 rad/s (stamped, for Jazzy)
ros2 topic pub --rate 10 /diff_drive_controller/cmd_vel geometry_msgs/msg/TwistStamped \
  "{twist: {linear: {x: 0.2}, angular: {z: 0.5}}}"

# what the controller sends to the wheels
ros2 topic echo /joint_states

Compare the wheel velocities with the Differential Drive Calculator: enter your radius and separation, the same v and ω, and the left and right wheel speeds should match. A sign flip (the robot turns right when told to turn left) usually means the left and right wheel names are swapped, or one motor's direction is inverted in the hardware interface.

Without ros2_control

If your robot uses a microcontroller and a custom bridge instead, the bridge does the same conversion itself, as in the bridge node in our serial protocol guide. Keep the same safety habits: a timeout that stops the robot, limits on speed and acceleration, and odometry computed from measured wheel motion.

More guides

Oct. 4, 2026, 9:20 a.m.
Velocity and Acceleration Limits for Smooth Differential-Drive Motion
Read more..
Oct. 4, 2026, 9:21 a.m.
Calibrating Wheel Radius and Wheel Separation for Better Odometry
Read more..
Oct. 4, 2026, 9:22 a.m.
Wheel Odometry from Encoder Ticks: Equations and Error Sources
Read more..
Oct. 4, 2026, 9:24 a.m.
Differential Drive Kinematics Explained: Forward and Inverse
Read more..

If you have any query or problem
feel free to contact us
email: [email protected]