Solving Differential Drive Kinematics and Implementing It on a Custom Mobile Robot
- Karan Bhakuni
- 12 hours ago
- 6 min read
If a two-wheel robot can't drive straight, it feels like a shopping cart with a mind of its own. The good news is that differential drive kinematics gives me a clean way to predict, command, and debug that motion with just a few measurements.
I only need the wheel radius (r), the wheelbase (L) (distance between wheel contact centers), encoder ticks per revolution, and a loop time (dt). From there, I solve two practical problems: forward kinematics (wheel motion to robot motion) and inverse kinematics (desired robot motion to wheel speeds). After that, it's testing and tuning, not endless math.
This post ends with a working implementation plan I can drop into an Arduino, a Raspberry Pi, or a ROS2 robot. It's only a few equations, then lots of real-world fixes.
Solve differential-drive kinematics step by step (without getting lost in math)
I like to think of a diff-drive robot as two hands pushing a box. If both hands push equally, the box goes straight. If one hand pushes harder, the box turns. That's basically the model.
I'm assuming a standard setup: two driven wheels on the same axle line, rolling without slipping. If you want a deeper derivation and the assumptions spelled out clearly, Northeastern's notes on kinematics for wheeled mobile robots are a solid reference.
Forward kinematics, turning left and right wheel speeds into v and omega
Let the wheel angular speeds be phi_dot_R and phi_dot_L (rad/s) in rad/s. Wheel linear speeds are:
v_R = r * phi_dot_R
v_L = r * phi_dot_L
From there, robot linear velocity (v) (forward) and angular velocity (\omega) (yaw rate) are:
v = (r/2) * (phi_dot_R + phi_dot_L)
omega = (r/L) * (phi_dot_R - phi_dot_L)
Plain language: the average of the wheels sets forward speed, and the difference sets turning rate.
Sanity checks I use (with (omega > 0) meaning counterclockwise, turning left):
Same wheel speeds (phi_dot_R = phi_dot_L): (omega = 0), it drives straight.
Equal and opposite (phi_dot_R = -phi_dot_L): (v = 0), it spins in place.
Right wheel faster (phi_dot_R > phi_dot_L): (omega > 0), it turns left.

Inverse kinematics, converting a target v and omega into wheel commands
Inverse kinematics is what I use to drive the robot on purpose. If I want a target (v) and (omega), the wheel angular speed targets become:
phi_dot_R = (2v + L*omega) / (2r)
phi_dot_L = (2v - L*omega) / (2r)
This maps nicely to joystick control or a planner output like ROS-style cmd_vel (linear.x and angular.z). I treat (v) as "how fast forward" and (omega) as "how fast turning."
Units matter, but the math doesn't change. I can work in rad/s, RPM, or ticks/s as long as I convert cleanly.
Here's the compact unit cheat sheet I keep nearby:
Quantity | Common unit | Quick conversion |
Wheel angular speed | rad/s | RPM = rad/s * (60 / 2π) |
Encoder speed | ticks/s | rad/s = ticks/s * (2π / ticks_per_revolution) |
Wheel linear speed | m/s | v = r * phi_dot |
Odometry updates, how I turn encoder ticks into x, y, and heading
Odometry is where the robot starts telling me what it actually did. Each loop:
I read encoder tick deltas Delta_ticks_R , Delta_ticks_L.
I convert them to wheel angle changes: [Delta_phi = 2π * (Delta_ticks / ticks_per_revolution)].
I estimate wheel angular speeds: [phi_dot ≈ Delta_phi / dt].
I compute (v) and (omega) using forward kinematics.
I integrate pose:
[ x = x + v cos(theta) dt
y = y + v sin(theta) dt
theta = theta + omega * dt]
I keep (dt) small, usually 10 to 20 ms, and I make timing consistent. Drift is normal, especially on slick floors.
If my odometry is "wrong," I first assume wheel slip or bad (r) and (L), not bad trigonometry.
Implement it on a custom robot, from measurements to a stable control loop
Once the math is set, the real work begins. My typical hardware assumptions are simple: two driven wheels, a caster (optional), encoders on each motor or wheel, a motor driver, and a battery that can actually supply current.
If I want a classic reference on the Instantaneous Center of Curvature idea (ICC), Columbia's short notes are handy: Differential Drive Kinematics (PDF). I don't copy the derivation into my code, but the concept helps when debugging turns.
Measure r and L the right way, because small errors wreck my odometry
A small wheel radius error turns into a big distance error. So I measure "effective" values, not just what the CAD says.
What works for me:
Wheel radius (r): I do a roll test under load. I mark the tire, roll one full wheel turn, and measure distance. Then (r = d / (2π)).
Wheelbase (L): I measure center-to-center between wheel contact patches, not the motor mounts.
Then I validate with two calibration tests:
Straight-line test: command equal wheel speeds. If it curves, I adjust left/right scaling (or re-check wheel radii).
Spin test: command equal and opposite speeds for a full 360-degree spin. If the angle is off, (L) is usually the culprit.
The goal is boring motion. Boring means predictable, and predictable is gold.
A control loop that actually works, encoder reading, speed estimate, then PID per wheel
I run my motor loop at 50 to 200 Hz. Faster helps, but only if encoder reads are clean.
My loop structure stays the same across platforms:
Read encoder ticks and compute measured wheel speed (ticks/s or rad/s).
Convert the desired body command ((v,\omega)) into wheel targets with inverse kinematics.
Run PID per wheel to hit those targets.
Output PWM (or voltage command) to the motor driver.
I always tune left and right PIDs separately because motors never match perfectly. In addition, I smooth measured speed with a tiny moving average (3 to 5 samples). That knocks down tick noise without making the robot feel laggy.

Common gotchas on DIY diff-drive robots, and quick fixes I use
Wheel slip: I reduce acceleration, add grippier tires, or fuse an IMU for heading.
Motor deadband: I build a minimum PWM map so small commands still move.
Battery sag: I monitor voltage and scale max PWM as voltage drops.
Tick noise at low speed: I average speed over a few samples and avoid ultra-low setpoints.
Math overflow or truncation: I use floats for kinematics, even if PWM is integer.
Hook it into ROS2 or a simple teleop stack, so I can drive, log, and improve fast
Kinematics feels abstract until I can drive the robot, log data, and replay what happened. That's why I keep a clean interface: inputs are (v) and (omega), outputs are wheel targets, and feedback is odometry.
When I test, I log four signals first: commanded (v,omega), measured wheel speeds, and battery voltage. That combo exposes most problems within minutes.
If I use ROS2, where the diff-drive math lives (and what I need to configure)
In ROS2 (as of March 2026), I usually lean on ros2_control with a diff_drive_controller. It accepts cmd_vel style commands and can publish odometry from encoders. I configure wheel radius, wheel separation (L), encoder resolution, update rates, and frame IDs (often odom and base_link). I also set reasonable covariances early so downstream filters don't assume perfect sensors.
If I skip ROS2, a clean "v and omega" interface still keeps my code organized
Even without ROS2, I keep the same contract. One function converts ((v,omega)) to wheel targets (inverse kinematics). Another converts encoder deltas to ((x,y,theta)) (forward kinematics and integration). If I want smoother motion, I ramp (v) and (omega) to limit jerk.
When inverse kinematics feels "jumpy," I compare my sign and unit choices with real examples, like this discussion on smooth differential-drive inverse kinematics.
Conclusion
When I build a custom differential-drive robot, I follow the same loop every time: measure (r) and (L), read encoders, run forward kinematics for odometry, run inverse kinematics for wheel targets, then use per-wheel PID to track speed. After that, I calibrate using simple real tests and small adjustments.
Start simple and log everything. Tune one thing at a time, and trust the data. My favorite next step is a straight-line test and a 360-degree spin test, then I adjust (r) and (L) until reality matches the math.

Comments