Weekly Robotics logo
Weekly Robotics Beginner-friendly tutorials, every week
Wheel Encoders: Knowing Where You Are
Control Systems

Wheel Encoders: Knowing Where You Are

Encoders measure how far your wheels have turned, enabling odometry — dead reckoning navigation. Learn about quadrature encoders and how to read them.

Your robot can drive forward, but how does it know how far it actually went? Commanding the motors for two seconds doesn’t tell you the distance — a fresh battery, a slippery floor, or a bit of extra weight changes everything. Wheel encoders answer the question by measuring how much each wheel truly rotated, and that measurement is the foundation of a robot knowing its own position.

Why Odometry Matters

Odometry is the practice of estimating your position by adding up small movements over time, a technique sailors call dead reckoning. If you know each wheel’s diameter and count exactly how far each one turned, geometry tells you how far the robot traveled and how much it turned. From a known starting point, you can continuously update an estimate of where the robot is — no GPS, no external beacons, just the wheels themselves. Almost every wheeled robot that navigates does this, and the encoder is the sensor that makes it possible.

Incremental vs. Absolute Encoders

There are two broad families:

  • Incremental encoders output a stream of pulses as the shaft turns. They tell you change in position, not absolute position — you count pulses relative to wherever you started. They’re cheap, simple, and by far the most common on hobby robots.
  • Absolute encoders report the shaft’s exact angle at any instant, even right after power-on, using a unique code for each position. They’re more complex and more expensive, and you reach for them when a joint must know its true angle without first homing — robot arm joints, for example.

For wheels, incremental encoders are almost always the right tool, so that’s what we’ll focus on.

How Quadrature Works

A basic incremental encoder with a single output channel can count pulses, but it cannot tell you which direction the wheel turned — forward and backward produce identical pulses. Quadrature encoders fix this with two output channels, A and B, whose pulse trains are offset by 90 degrees (one-quarter of a cycle, hence “quadrature”).

Because the channels are out of phase, the order in which their edges arrive reveals direction:

  • When turning one way, channel A’s rising edge happens before channel B’s.
  • When turning the other way, the order flips: B leads A.

So at the moment channel A rises, you simply check the current state of channel B. If they differ, you’re going one direction; if they match, the other. That single comparison turns a blind pulse counter into a signed position tracker.

A B A rises while B is LOW 90° phase offset = "quadrature"
Channels A and B are offset by a quarter cycle. Sampling B at A's rising edge (dashed line) reveals direction: B LOW means one way, B HIGH means the other.

CPR vs. PPR

These two acronyms confuse everyone, so pin them down early:

  • PPR (Pulses Per Revolution) — how many pulses one channel produces in a full turn of the shaft.
  • CPR (Counts Per Revolution) — how many distinct count events you can extract in a full turn.

With full quadrature decoding, you count every rising and falling edge of both channels. Each channel cycle has 4 such edges, so:

CPR = 4 × PPR

A 360 PPR encoder gives 1440 CPR with full decoding — four times the resolution for free, just by watching more edges. The catch is that you must decide which scheme you’re using and be consistent, because your distance math depends on it. The simple example below counts on a single edge of one channel (so effectively 1× PPR); the popular library mentioned later does full 4× decoding for you.

Reading an Encoder with Arduino Interrupts

Encoder pulses can come fast — too fast to catch reliably by checking pins in loop(). The right tool is a hardware interrupt: the Arduino pauses whatever it’s doing the instant channel A changes and runs a short function called an ISR (Interrupt Service Routine).

On a classic Arduino Uno, only pins 2 and 3 support interrupts, so we wire channel A to pin 2 and channel B to a regular pin.

Encoder PinArduino Pin
Channel APin 2 (interrupt)
Channel BPin 4 (read inside ISR)
VCC5V
GNDGND
const int ENCODER_A = 2;  // must be an interrupt-capable pin
const int ENCODER_B = 4;

// 'volatile' tells the compiler this changes outside normal flow.
// 'long' so the count can grow large in both directions.
volatile long encoderCount = 0;

void setup() {
  Serial.begin(9600);
  pinMode(ENCODER_A, INPUT_PULLUP);
  pinMode(ENCODER_B, INPUT_PULLUP);

  // Run handleEncoder() on every RISING edge of channel A
  attachInterrupt(digitalPinToInterrupt(ENCODER_A), handleEncoder, RISING);
}

// The ISR: keep it short and fast.
void handleEncoder() {
  // When A just went HIGH, B's state tells us the direction.
  if (digitalRead(ENCODER_B) == LOW) {
    encoderCount++;   // one direction
  } else {
    encoderCount--;   // the other direction
  }
}

void loop() {
  // Copy the volatile value safely before printing
  noInterrupts();
  long count = encoderCount;
  interrupts();

  Serial.print("Count: ");
  Serial.println(count);
  delay(200);
}

Two details matter. First, encoderCount is volatile because it’s modified inside an interrupt — without that keyword the compiler might cache a stale copy and your reads would be wrong. Second, in loop() we briefly disable interrupts while copying the count, since a long is read in multiple steps on an 8-bit Arduino and an interrupt firing mid-read could hand you a garbled value.

Converting Counts to Distance

Counts are abstract until you turn them into centimeters. The wheel rolls one full circumference per revolution, and one revolution equals CPR counts, so:

distance = (count / CPR) × (π × wheel_diameter)

const float WHEEL_DIAMETER = 6.5;   // cm
const long  CPR = 360;              // counts per revolution for THIS setup
const float WHEEL_CIRCUMFERENCE = 3.14159 * WHEEL_DIAMETER;

float countsToDistance(long count) {
  return ((float)count / CPR) * WHEEL_CIRCUMFERENCE;
}

Measure your own wheel and confirm your CPR (remember the 1× vs 4× decision) — guessing here is the usual reason a robot thinks it traveled the wrong distance.

An Easier Path: the Encoder Library

Writing correct quadrature ISRs that handle both channels and all four edges is fiddly. Paul Stoffregen’s Encoder library (install it through the Arduino Library Manager) does full 4× decoding for you and is rock-solid:

#include <Encoder.h>

Encoder myEncoder(2, 3);  // both channels on interrupt pins for best speed

void setup() {
  Serial.begin(9600);
}

void loop() {
  long count = myEncoder.read();  // signed, direction-aware count
  Serial.println(count);
  delay(100);
}

For most projects this is the recommended starting point — write your own ISR mainly to understand what’s happening underneath.

The Catch: Odometry Drifts

Encoders measure wheel rotation, not robot motion, and those are not always the same thing. Every time a wheel slips on a smooth floor, spins on loose dirt, or skids during a sharp turn, the encoder counts movement that didn’t happen. Tiny errors in your wheel-diameter estimate add up too. Because odometry accumulates every small error, the position estimate slowly drifts, and after enough driving it can be wildly off — the longer the journey, the worse the guess.

This is exactly why real robots don’t rely on encoders alone. They blend encoder data with other sensors — an IMU, a LiDAR, a camera — in a process called sensor fusion, so that absolute references can correct the drifting dead-reckoning estimate. We’ll get there.

Next week we step up to the software that turns all of this into real navigation: the ROS 2 Nav2 stack.