Blog

Calibrating Wheel Odometry on a Skid-Steer Rover: Fixing Drift Before It Reaches Your EKF

Tom Cosgrove,

Reading Room: The Fictionlab Blog

TL;DR: Wheel odometry calibration on a skid-steer rover comes down to three correctable parameters: effective wheel radius, effective track width, and the left/right radius ratio. Measure them with a straight-line test and a bidirectional square test (UMBmark), apply the corrections to your ROS 2 differential drive controller, then set odometry covariances that honestly reflect the residual error. Do this before fusing odometry with IMU or GNSS in robot_localization, because an EKF cannot reliably compensate for a systematic bias when its measurements are configured as highly trustworthy.

Why does wheel odometry calibration on a skid-steer platform matter more than on a differential drive robot?

A four-wheel skid-steer rover like the Leo Rover turns by forcing its wheels to slip laterally. That is the whole point of the kinematic design: no steering joints, no articulation, just differential wheel speeds and controlled scrub. The mechanical simplicity is excellent. The odometry consequences are not.

A two-wheel differential drive robot with a caster has a well-defined instantaneous center of rotation located on the wheel axle. A skid-steer rover does not have a fixed, purely geometric instantaneous center of rotation. Its effective center of rotation shifts depending on surface friction, weight distribution, tire wear, and turn rate. The standard differential drive model can still be useful, but only if you treat the track width as an effective parameter fitted to measured behavior rather than a number read off a CAD drawing.

This is why wheel odometry calibration for skid-steer matters so much. On a differential drive robot, the geometric track width is often reasonably close to the correct value. On a skid-steer rover, the effective track width can be larger than the physical one, sometimes substantially, because slip during rotation can make the robot rotate less than the ideal kinematic model predicts for a given wheel speed difference. Use the CAD number without validation and turns may be over-reported.

The failure mode is subtle. Straight-line motion often looks fine. Then the robot drives a loop, comes back, and the map is rotated. Or the EKF receives IMU yaw-rate and odometry yaw-rate measurements that disagree by 10 to 20 percent on every turn, which can lead to inconsistent state estimates and slowly accumulating heading error.

What are the actual sources of error in skid-steer wheel odometry?

Before touching any parameters, it helps to separate systematic errors (repeatable, potentially correctable by calibration) from non-systematic errors (variable errors that require robust estimation and sensor fusion). Borenstein and Feng’s UMBmark work, published in IEEE Transactions on Robotics and Automation in 1996, formalized this distinction and gave mobile robotics the benchmark test described later in this article.

The systematic sources you can fix through calibration include the following:

  • Effective wheel radius. The nominal radius from the datasheet assumes a particular tire condition and load. Pneumatic or foam-filled tires compress under payload. A rover carrying a sensor mast can have a smaller rolling radius than an empty one. A 2% radius error produces a 2% distance error on every straight segment, which is 20 cm over 10 m.
  • Effective track width. For skid-steer, this is often a dominant error. It absorbs much of the rotational slip into a single fitted constant. It is surface-dependent: the value calibrated on concrete may not be correct on loose gravel.
  • Left/right wheel radius ratio. Unequal tire pressure, uneven wear, or manufacturing tolerance makes one side travel slightly farther per encoder tick. This produces a slow curve during commanded straight-line motion.
  • Encoder resolution and gearbox ratio errors. Less common, but a transposed digit in a configuration file produces a clean, large, constant scale error that is easy to spot once you look for it.

The non-systematic sources you cannot calibrate away include wheel slip on loose or wet surfaces, impacts with obstacles, uneven terrain that changes the contact patch, and the variable slip that comes with different turn radii and speeds. These are exactly the kinds of effects that covariance modeling and sensor fusion are intended to handle, which is why correct covariance values matter as much as correct kinematic parameters.

How skid-steer differs from the ideal differential drive model

The standard differential drive forward kinematics are straightforward. Linear velocity is the average of left and right wheel velocities, and angular velocity is their difference divided by the track width:

v = (v_right + v_left) / 2
ω = (v_right - v_left) / b

where b is the track width. For a skid-steer rover, researchers commonly introduce a correction factor, sometimes called a slip factor or alpha. One possible convention is b_eff = b_geometric / alpha, where alpha is below 1 when the fitted effective track width is larger than the geometric one. Conventions vary, so it is important to document which definition your stack uses. The ROS 2 diff_drive_controller exposes wheel_separation and wheel_separation_multiplier, which makes it straightforward to represent the fitted effective separation.

How do you run the straight-line test to calibrate wheel radius?

Start with linear scale, because rotational calibration depends on it. The straight-line test isolates the wheel radius and helps reveal a left/right ratio error.

Here is the procedure, written to be reproducible on any skid-steer rover running ROS 2:

  1. Choose a flat, hard, uniform surface. Concrete or indoor flooring gives the cleanest baseline. Mark a straight reference line at least 5 m long, ideally 10 m, using a chalk line or tape measure.
  2. Place the rover at the start with a clearly identifiable reference point, for example a plumb bob hung from the center of the chassis or a marker taped to the frame.
  3. Reset odometry to zero, if your stack supports it, or record the starting odometry pose precisely.
  4. Command a straight-line motion at a moderate constant speed. Use something near the middle of the rover’s operating range, not the maximum, to avoid acceleration-dominated slip. Drive the full reference distance.
  5. Record the final odometry pose and measure the true final ground position: longitudinal distance along the reference line, and lateral deviation perpendicular to it.
  6. Repeat at least five times. Average the results.

From the averaged data, compute the linear scale correction:

radius_correction = true_distance / reported_distance

Multiply your configured wheel radius by this factor. If the rover reports 10.0 m but actually traveled 9.82 m, the correction is 0.982, so a nominal 0.0625 m radius becomes approximately 0.0614 m. Always use the wheel dimensions and encoder conventions for your specific rover and wheel variant rather than assuming a nominal value.

The lateral deviation can reveal a left/right radius mismatch. For a small, repeatable heading error theta accumulated over a straight-line distance D, the curvature is approximately theta / D. With the differential-drive approximation, the implied radius ratio depends on the signed heading error and the fitted effective track width. In practice, if the deviation is under roughly 1% of the distance traveled, most teams fold it into the covariance rather than correcting it, since it is often smaller than run-to-run variation. If it is consistently larger and repeatable, correct the per-side radius ratio and verify it with another set of runs.

A measurement script for logging odometry during tests

Running these tests by hand with a stopwatch invites errors. This ROS 2 Python node logs the odometry delta between a start and stop trigger, so you can compare it against your tape measure reading.

#!/usr/bin/env python3
# odom_delta_logger.py
import math
import rclpy
from rclpy.node import Node
from nav_msgs.msg import Odometry
from std_srvs.srv import Trigger


def yaw_from_quat(q):
siny = 2.0 * (q.w * q.z + q.x * q.y)
cosy = 1.0 - 2.0 * (q.y * q.y + q.z * q.z)
return math.atan2(siny, cosy)


class OdomDeltaLogger(Node):
def __init__(self):
super().__init__('odom_delta_logger')
self.start_pose = None
self.last = None
self.create_subscription(Odometry, '/odometry/wheels',
self.cb, 10)
self.create_service(Trigger, 'mark_start', self.on_start)
self.create_service(Trigger, 'mark_end', self.on_end)
self.get_logger().info('Call mark_start, drive, then mark_end.')

def cb(self, msg):
self.last = msg

def on_start(self, req, res):
if self.last is None:
res.success = False
res.message = 'No odometry received yet.'
return res
p = self.last.pose.pose
self.start_pose = (p.position.x, p.position.y,
yaw_from_quat(p.orientation))
res.success = True
res.message = 'Start marked at x=%.4f y=%.4f yaw=%.4f' % self.start_pose
self.get_logger().info(res.message)
return res

def on_end(self, req, res):
if self.start_pose is None or self.last is None:
res.success = False
res.message = 'Start not marked.'
return res
p = self.last.pose.pose
x1, y1 = p.position.x, p.position.y
yaw1 = yaw_from_quat(p.orientation)
x0, y0, yaw0 = self.start_pose
dx, dy = x1 - x0, y1 - y0
longitudinal = math.cos(yaw0) * dx + math.sin(yaw0) * dy
lateral = -math.sin(yaw0) * dx + math.cos(yaw0) * dy
chord = math.hypot(dx, dy)
dyaw = math.atan2(math.sin(yaw1 - yaw0), math.cos(yaw1 - yaw0))
res.success = True
res.message = ('longitudinal=%.4f m lateral=%.4f m chord=%.4f m '
'dyaw=%.4f rad (%.2f deg)'
% (longitudinal, lateral, chord,
dyaw, math.degrees(dyaw)))
self.get_logger().info(res.message)
return res


def main():
rclpy.init()
node = OdomDeltaLogger()
try:
rclpy.spin(node)
finally:
node.destroy_node()
rclpy.shutdown()


if __name__ == '__main__':
main()

Run it and trigger the markers from a second terminal:

ros2 run your_package odom_delta_logger
# terminal 2
ros2 service call /mark_start std_srvs/srv/Trigger
# ... drive the rover ...
ros2 service call /mark_end std_srvs/srv/Trigger

Adjust the topic name to match your stack. Check with ros2 topic list and confirm the message type with ros2 topic info. Important: subscribe to raw wheel odometry, not an EKF output, or you will be calibrating against a filtered signal that may already include IMU or other sensor corrections.

What is the UMBmark test and how do you run it on a skid-steer rover?

UMBmark (University of Michigan Benchmark) is a standard procedure for quantifying systematic odometry error in differential drive robots. The core insight is that driving a square path in both clockwise and counter-clockwise directions provides information that helps separate unequal wheel radii from incorrect wheel separation.

Driving the square in one direction only is not enough. An error in track width and an error in wheel radius ratio can produce similar final position offsets in a single direction, but their signatures differ when the direction reverses. Bidirectional testing is what makes the method useful.

Here is the procedure adapted for skid-steer wheel odometry calibration:

  1. Mark a square on the ground. A 4 m x 4 m square is a practical compromise between measurable error accumulation and space requirements. Larger squares amplify error and improve resolution.
  2. Place the rover at one corner, aligned with one side. Mark its exact ground position and heading.
  3. Drive the square clockwise: four straight segments of 4 m, each followed by a 90-degree turn. Use odometry feedback or a simple scripted controller so the commanded path is consistent between runs.
  4. Measure the final position error in x and y relative to the start, in a fixed world frame. Also note the final heading error.
  5. Repeat five times clockwise. Average the x and y errors to get the clockwise centroid.
  6. Repeat the entire procedure five times counter-clockwise. Average to get the counter-clockwise centroid.

The two centroids and the corresponding UMBmark equations can be used to estimate wheel-radius mismatch and wheel-separation error. On a skid-steer rover, interpret the results as fitted parameters for that payload, surface, speed, and turning method rather than as universal mechanical constants. Because skid-steer slip can be asymmetric and variable, a simple centroid pattern alone is not always sufficient to identify a single cause.

For practical skid-steer work, many teams simplify. Because rotational slip is often surface-dependent, the fastest path is often an in-place rotation test:

  1. Mark the rover’s heading precisely on the ground using two reference points on the chassis.
  2. Command a nominal sequence of multiple full rotations in place at a modest angular velocity, while recording wheel odometry yaw.
  3. Measure the actual accumulated rotation with an external reference, such as a motion-capture system, surveyed heading targets, or a carefully observed marked course.
  4. Compare the actual accumulated yaw against the odometry-reported accumulated yaw. The reported-to-true rotation ratio gives the wheel_separation_multiplier correction.

Multiple rotations amplify the error enough that a few degrees of measurement imprecision contributes relatively little to the final ratio. Run this both clockwise and counter-clockwise and compare the results: averaging is reasonable only when the difference is within the repeatability you expect for the surface and maneuver.

Applying corrections in ROS 2

If you use ros2_control with diff_drive_controller, the relevant parameters live in your controller YAML:

diff_drive_controller:
ros__parameters:
wheel_separation: 0.358 # example geometric value, in metres
wheel_radius: 0.0625 # example nominal value, in metres
wheel_separation_multiplier: 1.0 # <- tune from rotation test
left_wheel_radius_multiplier: 1.0
right_wheel_radius_multiplier: 1.0

Set wheel_separation_multiplier to the ratio of odometry-reported rotation to true rotation. If the rover reports 3620 degrees after 10 true rotations (3600 degrees), the odometry is over-reporting by a factor of 1.0056, so the effective separation should increase by that factor. Set the radius multipliers from the straight-line test, applying the ratio correction asymmetrically only if the lateral drift is repeatable.

Verify after every change. Re-run the rotation test and the straight-line test. Calibration is iterative: correcting wheel separation can affect a controller’s executed path, especially when its turns are closed-loop on odometry.

How should calibration results shape your EKF covariances?

Here is where wheel odometry calibration for skid-steer connects to the rest of your autonomy stack. After calibration, you know your residual error. That number belongs in the covariance matrix.

The common mistake is copying covariance values from a tutorial and never revisiting them. If the odometry covariance is too small, the EKF trusts wheel odometry over the IMU, and every slip event can inject an error the filter has little reason to reject. If it is too large, the filter effectively ignores odometry, and you lose a useful source of short-term linear velocity.

A defensible approach for a calibrated skid-steer rover follows these steps:

  • Consider fusing velocities rather than wheel-odometry poses. In robot_localization, a common configuration is to use wheel odometry for vx and, when justified, vyaw, rather than directly fusing its accumulated x, y, and yaw pose. Wheel odometry pose accumulates integration error. This is not the only valid configuration, but it avoids treating drifted wheel pose as an absolute observation.
  • Set vx variance from straight-line residuals. If your post-calibration straight-line runs show a standard deviation of roughly 1% of distance traveled, and you typically drive at 0.3 m/s, the velocity standard deviation is on the order of 0.003 m/s. Square it for variance, and inflate it by a safety factor of 2 to 4 for field surfaces.
  • Set vyaw variance generously on skid-steer. Rotational slip is often the least predictable component. If the IMU gyro is available and reasonably good, give wheel-derived yaw rate a large variance so the gyro dominates. Many skid-steer stacks disable wheel yaw rate entirely and rely on the IMU for short-term rotational motion.
  • Do not fuse a hard lateral-velocity constraint unless it is justified. A skid-steer rover can have lateral slip, but its basic kinematic model does not measure it. Leaving vy unfused avoids telling the filter that lateral velocity is an exact zero measurement.

A minimal robot_localization configuration fragment reflecting this looks like the following:

ekf_filter_node:
ros__parameters:
frequency: 50.0
two_d_mode: true
publish_tf: true
odom_frame: odom
base_link_frame: base_link
world_frame: odom

odom0: /odometry/wheels
odom0_config: [false, false, false,
false, false, false,
true, false, false,
false, false, false,
false, false, false]
odom0_differential: false
odom0_relative: false

imu0: /imu/data
imu0_config: [false, false, false,
false, false, false,
false, false, false,
false, false, true,
false, false, false]
imu0_differential: false
imu0_remove_gravitational_acceleration: true

This configuration takes forward velocity from the calibrated wheel odometry and yaw rate from the IMU. It is a conservative starting point for skid-steer platforms, and it makes the value of linear calibration explicit: the EKF’s short-term estimate of forward motion now relies on your wheel-radius calibration.

Publish covariances from your odometry source rather than relying on the EKF’s defaults. If your driver publishes zeros or an identity matrix, override them in a small republisher node using the standard deviations you measured.

When should you recalibrate, and what does calibration not fix?

Wheel odometry calibration for skid-steer is surface-specific and payload-specific. A configuration tuned indoors on smooth flooring can under-rotate outdoors on grass because the slip characteristics change.

Practical triggers for recalibration include the following situations:

  • You change tires, tire pressure, or wheel type.
  • You add or remove a payload heavy enough to change ride height, which changes effective rolling radius.
  • You move from your development surface to the deployment surface, especially from hard flooring to loose terrain.
  • Your EKF shows a consistent, direction-dependent heading bias during turns.

Calibration does not fix slip on wet grass, wheel lift on uneven ground, or the fundamental lack of direct lateral-velocity measurement in a basic skid-steer model. It does not make wheel odometry a global positioning source. What it does is reduce the systematic component of the error so that the remaining error is closer to the zero-mean uncertainty assumed by the filter model. An EKF given biased odometry and unrealistically small covariances will generally produce biased or inconsistent output no matter how carefully you tune everything downstream. For teams running experiments where repeatability matters, this is the difference between a trajectory you can analyze and one you cannot. Fictionlab documents more on these use cases under research applications.

Frequently asked questions

Q: Can I skip wheel odometry calibration if I have a good IMU and GNSS?

A: Not entirely. GNSS update rate and accuracy vary widely by receiver and correction service: uncorrected GNSS is often accurate to metres, while RTK GNSS can achieve centimetre-level accuracy under suitable conditions. An IMU gyro provides short-term angular-rate information, but it does not by itself provide a drift-free heading reference. Between global-position updates, the EKF often relies on wheel odometry and IMU propagation. A 5% scale error in wheel-derived velocity can produce visible artifacts in the fused trajectory and degrade local planner performance. Calibration is cheap compared to debugging that.

Q: How long does a full calibration take on a skid-steer rover?

A: Plan for two to three hours for a first-time full UMBmark procedure including setup, ten square runs, and measurement. The simplified version – a five-run straight-line test plus a multi-rotation test in both directions – typically fits in under an hour and captures much of the available improvement.

Q: Should I calibrate wheel separation or use a slip ratio parameter?

A: They can be mathematically equivalent when defined consistently. Use whichever your stack exposes. ROS 2’s diff_drive_controller provides wheel_separation_multiplier, which keeps the geometric value in the URDF intact and makes the calibration correction visible as a separate, documented number. That separation is useful when someone else reads your configuration a year later.

Q: My clockwise and counter-clockwise UMBmark centroids are far apart. What does that mean?

A: A strong difference between clockwise and counter-clockwise results is consistent with a wheel-separation error, asymmetric slip, or both. A common displacement in both directions can be consistent with unequal effective wheel radii. Use the full UMBmark correction equations and repeatability data rather than relying only on centroid appearance, especially on a skid-steer platform where slip is not perfectly symmetric.

Q: Does calibration change if I drive faster?

A: Yes, on skid-steer it often does. Higher angular velocities can increase lateral slip, so the fitted effective track width may grow with turn rate. Calibrate at a speed representative of your actual operating profile. If your rover operates across a wide speed range, calibrate at the midpoint and widen your yaw covariance to cover the spread.

Q: Can I automate the calibration instead of measuring with a tape?

A: Yes, if you have ground truth. Motion capture, a total station, or RTK GNSS with centimetre-level accuracy under suitable conditions all let you script the procedure and solve for parameters by least squares over many trajectories. Without ground truth, manual measurement on a marked course remains the reliable fallback and is often precise enough, since the test design amplifies the error you are measuring.

Q: Where do I put the resulting covariance values?

A: Ideally in the odometry driver, so every consumer of the topic sees them. If the driver does not expose them, a small republisher node that copies the message and fills the covariance arrays works well and keeps the calibration result in one place rather than duplicated across EKF configuration files.

If you are building an autonomy stack and want a documented, open-source skid-steer platform where wheel geometry, firmware, and ROS 2 packages are all accessible and modifiable, take a look at the Leo Rover.


Read more