Wheeled Mobile Robot Kinematics: Differential, Skid-Steer and Ackermann Models in Practice
Tom Cosgrove,

TL;DR: Mobile robot kinematics converts a desired body motion (linear velocity v, angular velocity ω) into wheel commands, and wheel encoder readings back into an odometry estimate. Differential drive uses two independently driven wheels separated by track width b: v_L = v – ωb/2, v_R = v + ωb/2. Skid-steer uses the same equations plus a correction factor that accounts for lateral slip during turning. Ackermann steering decouples drive from steering and computes individual steering angles from the wheelbase L and turn radius R = v/ω. This article gives the forward and inverse equations for all three models, a worked Twist-to-wheel-velocity conversion, and runnable Python code you can drop into a ROS 2 node.
What Problem Does Mobile Robot Kinematics Actually Solve?
A ROS 2 navigation stack commonly uses geometry_msgs/Twist or geometry_msgs/TwistStamped for velocity commands. In a typical Nav2 configuration, the controller server publishes a command with linear.x and angular.z on /cmd_vel, and it does not know or care whether your chassis has two wheels, four wheels, or a steering rack. Your hardware speaks a different language: motor RPM setpoints, or steering servo angles.
Mobile robot kinematics is the translation layer between those two. It has two directions:
- Inverse kinematics – given a body-frame Twist, compute wheel velocities and steering angles. This is what your base controller runs every control cycle, typically at 20-100 Hz.
- Forward kinematics – given measured wheel velocities, estimate the resulting body velocity. This feeds odometry, which feeds the EKF, which feeds the localization and planning stack.
Get the inverse wrong and the robot does not follow the planned path. Get the forward wrong and the robot thinks it is somewhere it is not. Both failure modes look the same from the outside – the robot drifts off course – so it pays to be precise about which model you are using and what assumptions it carries.
This article covers the mathematics and the implementation. The practical trade-offs between drivetrain choices (traction, wear, power draw, terrain behavior) are a separate topic covered in the Differential Drive or Skid Steering article.
The Shared Assumption: Rolling Without Slipping
Every model below starts from the same idealization. A wheel of radius r spinning at angular velocity ω_w rad/s contacts the ground at a single point and that point has zero velocity relative to the ground. The wheel’s hub therefore moves forward at:
v_wheel = r · ω_w
There is also a nonholonomic constraint: the wheel cannot slide sideways. For a standard wheel, lateral velocity at the contact point is zero. This is why a differential-drive robot cannot strafe, and why its configuration space (3 DOF: x, y, θ) exceeds its controllable velocity space (2 DOF: v, ω).
Real robots violate both assumptions. How badly they violate them is exactly what separates the three models.
How Do You Derive Differential Drive Kinematics?
Differential drive is the cleanest case in mobile robot kinematics and the right starting point. Two wheels on a common axle, each driven independently, with optional casters or ball transfers for support.
Define:
- r – wheel radius (m)
- b – track width, the distance between the two wheel contact points (m)
- v_L, v_R – linear velocities of the left and right wheel contact points (m/s)
- v – body linear velocity along the robot’s x-axis (m/s)
- ω – body angular velocity about the z-axis (rad/s), positive counterclockwise
Forward Kinematics
The body center sits midway between the wheels, so its velocity is the average of the two wheel velocities. The rotation rate is the velocity difference divided by the separation:
v = (v_R + v_L) / 2ω = (v_R - v_L) / b
Expressed directly in terms of measured wheel angular velocities ω_L and ω_R in rad/s:
v = r · (ω_R + ω_L) / 2ω = r · (ω_R - ω_L) / b
These two equations are the basis of wheel odometry. Integrate them over a timestep Δt to update pose. The standard exact integration for a constant-velocity arc segment is:
When |ω| > ε:Δθ = ω · ΔtΔx = (v/ω) · (sin(θ + Δθ) - sin(θ))Δy = -(v/ω) · (cos(θ + Δθ) - cos(θ))
When |ω| ≤ ε (near-straight motion), fall back to the first-order form to avoid dividing by zero:Δx = v · Δt · cos(θ), Δy = v · Δt · sin(θ), Δθ = ω · Δt
Inverse Kinematics
Invert the system to get the wheel commands your motor controllers need:
v_L = v - (ω · b) / 2v_R = v + (ω · b) / 2
And in motor-shaft terms:
ω_L = (v - ω·b/2) / rω_R = (v + ω·b/2) / r
That is the complete differential-drive controller. Four lines of arithmetic. Note the instantaneous center of rotation (ICR) lies on the wheel axle at signed distance R = v/ω from the body center. When v = 0, R = 0 and the robot spins in place.
Why Does Skid-Steer Kinematics Need a Correction Factor?
Skid-steer (4WD or 6WD, wheels fixed parallel to the chassis, no steering joints) uses the same inverse equations as differential drive. Left-side wheels receive v_L, right-side wheels receive v_R. The geometry looks identical on paper.
It is not identical in reality. A skid-steer platform has wheels at different longitudinal positions. During a turn, those wheels trace circles of different radii around the ICR, but they are mechanically forced to rotate at the same speed within each side. Something has to give, and what gives is traction: the wheels slip laterally and longitudinally. The robot “skids” – hence the name.
The consequence for mobile robot kinematics is that the effective track width is not the geometric track width. The standard engineering fix is an ICR correction factor (also called the slip or track-width multiplier), commonly written α or χ:
ω = r · (ω_R - ω_L) / (α · b)
and inversely:
v_L = v - (α · ω · b) / 2v_R = v + (α · ω · b) / 2
The linear velocity equation is unchanged. Only yaw is affected in this simplified model, although real skid-steer vehicles can also experience longitudinal slip and wheel-radius-related scale errors.
Determining α Empirically
There is no closed-form value for α. It depends on wheel-ground friction, tire compound, normal load distribution, and wheelbase-to-track ratio – so it changes between tarmac, grass, and loose gravel. Treat it as a calibration parameter.
Here is a reproducible procedure for measuring it on any skid-steer platform, including the Leo Rover:
- Place the robot on the surface you care about. Mark its heading precisely (a laser line or a taped reference edge works well).
- Command a pure rotation:
linear.x = 0,angular.z = ω_cmd. Use a modest value such as 0.5 rad/s to stay out of the high-slip regime. - Run for a fixed duration and count complete revolutions – ten full turns gives good averaging.
- Record the total wheel rotation reported by encoders and compute the yaw the uncorrected model predicts.
- Compute
α = θ_predicted / θ_measured. For four-wheel skid-steer on hard surfaces, values greater than 1.0 are typical, since the robot under-rotates relative to the ideal model.
Repeat on each surface type your deployment involves and log the spread. If you need a single value, choose the one matching your dominant terrain and let the EKF and an IMU absorb the rest. In practice, fusing a gyroscope’s yaw-rate measurement with wheel odometry is more effective than any single α, because a gyroscope does not infer yaw from wheel-ground contact, although it still has bias and noise.
A practical note for field robotics: because α is surface-dependent, never treat skid-steer wheel odometry as a trustworthy yaw source over long distances. Use it for short-horizon velocity estimates and let IMU, visual odometry, or LiDAR scan matching carry heading.
How Does Ackermann Steering Kinematics Differ?
Ackermann steering separates propulsion from direction. A common arrangement drives the rear wheels and pivots the front wheels, and the geometry is designed so all four wheels roll without lateral scrub – the ICR lies on the extended rear axle line.
Define:
- L – wheelbase, front axle to rear axle distance (m)
- b – track width (m)
- δ – virtual steering angle of the bicycle-model center wheel (rad)
- δ_in, δ_out – inner and outer front wheel steering angles (rad)
The Bicycle Model
Collapse both front wheels into one centerline wheel. This single-track approximation is accurate enough for planning and control at moderate speed:
Forward:v = r · ω_rear_avgω = (v / L) · tan(δ)
Inverse, given a Twist:δ = atan(ω · L / v)
Note immediately that this breaks when v = 0. An Ackermann vehicle cannot rotate in place, and a zero-velocity, nonzero-angular-velocity Twist is simply infeasible. Your controller must clamp or reject such commands rather than produce a division blowup. For Ackermann platforms, configure a planner and controller that respect the vehicle’s minimum turning radius; in Nav2, Smac Hybrid-A* is a common choice for this purpose.
True Ackermann Geometry
Because the inner wheel traces a tighter circle than the outer wheel, the two front wheels need different angles. With R = L / tan(δ) as the turn radius measured to the rear-axle center:
δ_in = atan(L / (R - b/2))δ_out = atan(L / (R + b/2))
The rear wheels also need different speeds to avoid scrub, which is what a mechanical differential provides on a car and what your software must provide if you drive each rear wheel independently:
v_rear_in = v · (R - b/2) / Rv_rear_out = v · (R + b/2) / R
ROS 2 ships ackermann_steering_controller in ros2_controllers. It implements Ackermann steering kinematics, accepts a body-velocity reference, commands steering and traction joints, and can publish odometry using its kinematic model.
What Does the Twist-to-Wheel Conversion Look Like in Code?
Here is a self-contained Python module implementing all three models. It has no ROS dependencies, so you can unit-test it directly, then wrap it in a node.
import math
class DiffDriveKinematics:
def __init__(self, wheel_radius, track_width, max_wheel_rads=None):
self.r = wheel_radius
self.b = track_width
self.max_w = max_wheel_rads
def inverse(self, v, omega):
"""Twist -> (left, right) wheel angular velocity in rad/s."""
wl = (v - omega * self.b / 2.0) / self.r
wr = (v + omega * self.b / 2.0) / self.r
return self._scale(wl, wr)
def forward(self, wl, wr):
"""Wheel rad/s -> (v, omega)."""
v = self.r * (wr + wl) / 2.0
omega = self.r * (wr - wl) / self.b
return v, omega
def _scale(self, wl, wr):
"""Preserve the v/omega ratio when saturating."""
if self.max_w is None:
return wl, wr
peak = max(abs(wl), abs(wr))
if peak > self.max_w:
k = self.max_w / peak
wl, wr = wl * k, wr * k
return wl, wr
class SkidSteerKinematics(DiffDriveKinematics):
def __init__(self, wheel_radius, track_width, alpha=1.0, max_wheel_rads=None):
super().__init__(wheel_radius, track_width, max_wheel_rads)
self.alpha = alpha # empirical ICR correction
def inverse(self, v, omega):
eff = self.alpha * self.b
wl = (v - omega * eff / 2.0) / self.r
wr = (v + omega * eff / 2.0) / self.r
return self._scale(wl, wr)
def forward(self, wl, wr):
v = self.r * (wr + wl) / 2.0
omega = self.r * (wr - wl) / (self.alpha * self.b)
return v, omega
class AckermannKinematics:
def __init__(self, wheelbase, track_width, max_steer=0.52):
self.L = wheelbase
self.b = track_width
self.max_steer = max_steer
def inverse(self, v, omega, eps=1e-4):
"""Returns (delta_inner, delta_outer, v_inner, v_outer)."""
if abs(v) < eps:
return 0.0, 0.0, 0.0, 0.0 # in-place rotation infeasible
if abs(omega) < eps:
return 0.0, 0.0, v, v
delta = math.atan(omega * self.L / v)
delta = max(-self.max_steer, min(self.max_steer, delta))
R = self.L / math.tan(delta)
Rin, Rout = abs(R) - self.b / 2.0, abs(R) + self.b / 2.0
s = 1.0 if R > 0 else -1.0
d_in = s * math.atan(self.L / Rin)
d_out = s * math.atan(self.L / Rout)
return d_in, d_out, v * Rin / abs(R), v * Rout / abs(R)
def forward(self, v, delta):
return v, (v / self.L) * math.tan(delta)
A Worked Numeric Example
Assume a four-wheel skid-steer chassis with wheel radius r = 0.0625 m and track width b = 0.358 m, with an assumed correction factor α = 1.5 (illustrative – measure your own). Nav2 publishes linear.x = 0.3 m/s, angular.z = 0.4 rad/s.
Effective track: 1.5 × 0.358 = 0.537 m
Left wheel: (0.3 - 0.4 × 0.537/2) / 0.0625 = (0.3 - 0.1074) / 0.0625 = 3.08 rad/s
Right wheel: (0.3 + 0.1074) / 0.0625 = 6.52 rad/s
That is approximately 29.4 RPM left and 62.3 RPM right. Push those through your motor controllers, and if either exceeds the motor’s rated speed, the saturation routine in _scale reduces both proportionally – which preserves the turn radius while slowing the robot, rather than distorting the trajectory.
Wiring It Into ROS 2
You rarely need to write a base controller from scratch. The ros2_control framework provides maintained implementations covering all three models:
diff_drive_controller– two-wheel differential drive, also used for skid-steer by tuningwheel_separation_multiplier, which serves as the α factor described above.ackermann_steering_controller– Ackermann steering kinematics with per-wheel steering correction.tricycle_controller– single steered drive wheel plus two passive rear wheels.
Verify your parameters empirically rather than trusting the CAD. Drive a measured straight line, compare commanded distance against tape-measure distance, and adjust the wheel radius until they agree. Then run the rotation test to fit α. Twenty minutes of calibration eliminates systematic odometry error that no filter can fully recover.
FAQ
Can you use differential drive equations on a four-wheel skid-steer robot?
Yes, with a correction. The inverse kinematics for linear velocity are identical, and the structure of the angular equation is identical. What changes is that the geometric track width underestimates the effective track width, because the wheels skid laterally during turns. Multiply b by an empirically measured factor and the model works well enough for closed-loop control.
Why does the Ackermann model fail at zero velocity?
The steering angle formula δ = atan(ωL/v) divides by v. More fundamentally, an Ackermann vehicle’s ICR must lie on the rear axle line, so with the rear wheels stationary, the vehicle cannot rotate. Zero-radius turning is physically impossible. Handle this by detecting near-zero v and either holding the steering angle, returning zero, or rejecting the command upstream in the planner.
How often should the kinematics calculation run?
Match it to your motor control loop. Common ROS 2 base controllers run at 50 Hz, with some firmware-level PID loops running at 100 Hz or faster. The kinematic math itself is trivially cheap – a handful of floating-point operations – so the limiting factor is your encoder read rate and communication bus latency, not the arithmetic.
Should you trust wheel odometry alone for localization?
No. Wheel odometry integrates velocity, so every error accumulates without bound. It is excellent for short-horizon, high-rate velocity estimation and terrible for absolute position over minutes. Standard practice is to fuse it with an IMU through robot_localization‘s EKF, and correct against an absolute reference such as AMCL scan matching, visual-inertial odometry, or RTK GNSS in outdoor field robotics.
What is the difference between the bicycle model and full Ackermann geometry?
The bicycle model collapses each axle to a single centerline wheel, giving one steering angle and one drive velocity. It is used for planning and high-level control because it is simple and accurate for the body trajectory. Full Ackermann geometry additionally computes the individual inner and outer wheel angles and speeds, which you need at the actuator level to avoid tire scrub.
Does wheel radius matter more than track width for odometry accuracy?
They affect different error types. Wheel radius error produces a scale error – the robot consistently reports traveling more or less distance than it actually did, growing linearly with distance. Track width error produces heading error, which is worse because heading error turns into position error that grows with the distance traveled after the error occurred. Calibrate radius first with a straight-line test, then track width with a rotation test.
How do you handle wheel velocity saturation correctly?
Do not clamp each wheel independently. Clamping only the faster wheel changes the ratio between v and ω, which distorts the commanded turn radius and sends the robot off the planned arc. Instead, find the largest magnitude among the computed wheel speeds and, if it exceeds the limit, scale all wheel commands by the same factor. The robot then follows the correct path, just more slowly.
Where to Go From Here
The equations in this article are the ones you will implement, calibrate, and debug on any wheeled platform. Differential drive gives you the clean baseline. Skid-steer gives you the same math plus one honest parameter that admits the model is approximate. Ackermann gives you a different constraint structure that propagates all the way up into your choice of planner.
If you want to test these models on real hardware rather than in simulation, a compact open-source platform lets you measure α on actual terrain, compare encoder-only odometry against IMU-fused estimates, and see where each model breaks. Fictionlab builds the Leo Rover specifically for that kind of hands-on work, and its ROS 2 stack exposes the controller parameters directly. Explore how teams use it in teaching labs and coursework on the education applications page, or see current research deployments for field-robotics context.