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 speedsResulting motion
vL = vRStraight line, omega = 0, infinite radius
vL = -vRSpin on the spot about the centre, v = 0
vL = 0, vR > 0Pivot about the left wheel, R = L / 2
vR > vL > 0Curve to the left
vL > vR > 0Curve 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.

Inverse kinematics with per-wheel PI speed control using encoders
// 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.