Differential Drive Kinematics: From Wheel Speeds to Robot Motion
Most small mobile robots, including every 2WD chassis in this series and many vacuum-cleaning robots, use differential drive: two independently driven wheels on a common axle, plus a caster for balance. The robot steers only by making one wheel faster than the other. Kinematics is the set of equations that connects the two wheel speeds to the motion of the whole robot.
Once you know these equations you stop thinking in terms of left PWM and right PWM and start commanding what you actually want: a forward speed in metres per second and a turning rate in radians per second. This is exactly how ROS 2 mobile robots receive commands, so it is an important step towards advanced robotics. This tutorial derives the equations, works through numerical examples and implements them with closed-loop wheel speed control.
Forward Kinematics
Let vL and vR be the linear speeds of the left and right wheels at the ground, in metres per second, and L the wheelbase, the distance between the wheels' contact points. The robot's centre moves with linear speed v and rotates with angular speed omega.
v = (vR + vL) / 2 linear speed of the robot centre (m/s) omega = (vR - vL) / L angular speed, positive = counter-clockwise (rad/s) Turning radius of the centre: R = v / omega = (L / 2) * (vR + vL) / (vR - vL)
| Wheel speeds | Resulting motion |
|---|---|
| vL = vR | Straight line, omega = 0, infinite radius |
| vL = -vR | Spin on the spot about the centre, v = 0 |
| vL = 0, vR > 0 | Pivot about the left wheel, R = L / 2 |
| vR > vL > 0 | Curve to the left |
| vL > vR > 0 | Curve to the right |
Inverse Kinematics
Inverse kinematics answers the practical question: given the v and omega I want, what speed should each wheel run at? Solve the two equations above for vL and vR.
vR = v + omega * L / 2 vL = v - omega * L / 2 Wheel angular speed (rad/s) = wheel linear speed / wheel radius r Wheel RPM = wheel linear speed / (pi * D) * 60
Worked example
Robot: L = 0.13 m, wheel diameter D = 0.065 m Command: v = 0.20 m/s, follow a circle of radius R = 0.50 m to the left omega = v / R = 0.20 / 0.50 = 0.40 rad/s vR = 0.20 + 0.40 * 0.065 = 0.226 m/s vL = 0.20 - 0.40 * 0.065 = 0.174 m/s RPM right = 0.226 / (3.1416 * 0.065) * 60 = 66.4 rpm RPM left = 0.174 / (3.1416 * 0.065) * 60 = 51.1 rpm Time for one full circle = 2 * pi / omega = 15.7 s
Check the answer against the motor's no-load speed. If a wheel would need more than the motor can deliver, you must scale both wheel speeds down by the same factor, which keeps the turning radius and only slows the robot. Clipping one wheel alone would change the path.
Commanding v and omega on Arduino
Open-loop PWM does not give an exact wheel speed, because speed changes with battery voltage and load. The sketch below combines inverse kinematics with a PI speed controller per wheel, using the encoder wiring from the odometry tutorial (encoder A on D2 and D3, B on D11 and D12) and the TB6612FNG on D4 to D10.
// Differential drive: command (v, omega), PI speed control per wheel
const int L_A = 2, L_B = 11, R_A = 3, R_B = 12; // encoders
const int PWMA = 5, AIN1 = 7, AIN2 = 8; // left motor
const int PWMB = 6, BIN1 = 9, BIN2 = 10, STBY = 4; // right motor
const float TICKS_PER_REV = 700.0; // measure on your robot
const float WHEEL_D = 0.065; // m
const float L = 0.13; // wheelbase, m
const float V_MAX = 0.40; // max wheel speed your motors reach, m/s
const float M_PER_TICK = PI * WHEEL_D / TICKS_PER_REV;
const float KP = 400.0, KI = 1500.0; // PWM per (m/s) error; tune for your motors
const float DT = 0.02; // 50 Hz control loop
volatile long lt = 0, rt = 0;
void lISR() { if (digitalRead(L_B)) lt--; else lt++; }
void rISR() { if (digitalRead(R_B)) rt++; else rt--; }
float targetL = 0, targetR = 0, iL = 0, iR = 0;
long pl = 0, pr = 0;
unsigned long last = 0;
void motor(int pwmPin, int in1, int in2, int s) {
s = constrain(s, -255, 255);
digitalWrite(in1, s > 0 ? HIGH : LOW);
digitalWrite(in2, s < 0 ? HIGH : LOW);
analogWrite(pwmPin, abs(s));
}
// Inverse kinematics with proportional scaling if a wheel would saturate
void setVelocity(float v, float omega) {
float vR = v + omega * L / 2.0;
float vL = v - omega * L / 2.0;
float biggest = max(fabs(vR), fabs(vL));
if (biggest > V_MAX) {
vR *= V_MAX / biggest;
vL *= V_MAX / biggest;
}
targetL = vL; targetR = vR;
}
void speedControl() {
noInterrupts(); long l = lt, r = rt; interrupts();
float vL = (l - pl) * M_PER_TICK / DT; // measured wheel speeds
float vR = (r - pr) * M_PER_TICK / DT;
pl = l; pr = r;
float eL = targetL - vL, eR = targetR - vR;
iL = constrain(iL + eL * DT, -0.2, 0.2); // integral with anti-windup
iR = constrain(iR + eR * DT, -0.2, 0.2);
motor(PWMA, AIN1, AIN2, (int)(KP * eL + KI * iL));
motor(PWMB, BIN1, BIN2, (int)(KP * eR + KI * iR));
}
void setup() {
int outs[] = {PWMA, AIN1, AIN2, PWMB, BIN1, BIN2, STBY};
for (int p : outs) pinMode(p, OUTPUT);
digitalWrite(STBY, HIGH);
pinMode(L_A, INPUT_PULLUP); pinMode(L_B, INPUT_PULLUP);
pinMode(R_A, INPUT_PULLUP); pinMode(R_B, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(L_A), lISR, RISING);
attachInterrupt(digitalPinToInterrupt(R_A), rISR, RISING);
}
void loop() {
// Demo: 0.5 m radius left circle for 8 s, then stop for 3 s
unsigned long t = millis() % 11000;
if (t < 8000) setVelocity(0.20, 0.40);
else setVelocity(0.0, 0.0);
if (millis() - last >= 20) { // fixed 20 ms control period
last = millis();
speedControl();
}
}
How the Pieces Fit
setVelocity is the inverse kinematics; it can be called from anywhere, such as a Bluetooth command parser or a serial link from a Raspberry Pi. speedControl runs every 20 ms, measures each wheel's speed from encoder ticks and adjusts PWM with a PI controller so the wheel reaches its target speed despite battery and load changes. The integral term matters here, unlike in the line follower, because a motor needs a steady non-zero PWM just to hold a constant speed.
With only 20 ms of ticks per measurement, low speeds are measured coarsely. If the speed reading is noisy, average it over a few periods or use a longer control period for slow robots.
Common Mistakes
- Mixing units: keep everything in metres, seconds and radians, and convert to RPM or degrees only for display.
- Measuring wheelbase to the wheel's outer edge instead of the centre of the tyre contact patch.
- Clipping only the saturated wheel, which changes the turning radius.
- Robot turns the wrong way for positive omega: the convention is counter-clockwise positive when viewed from above; check motor and encoder signs.
Summary
Forward kinematics turns wheel speeds into v and omega; inverse kinematics turns the v and omega you want into wheel speeds through vR = v + omega L / 2 and vL = v - omega L / 2. Scale both wheels together when one saturates, and close the loop with encoder-based PI speed control to make the robot actually follow the commanded motion.