# Wheel encoders for robots: counts, odometry and wiring

> PPR, CPR and the gearbox trap, a five-minute calibration for counts per revolution, and how wheel counts become distance and heading.

Canonical: https://roboticsengineering.in/robot-wheel-encoder-guide/ | Published: 2026-10-27 | Updated: 2026-10-28 | Author: Jignesh Panchal

**A wheel encoder turns wheel rotation into counts, and counts into distance — but only if you know how many counts one wheel revolution actually produces.** That number is not on the listing, and it is different for every gearbox.

## Two kinds of encoder, one job

An incremental encoder outputs two square waves, A and B, that are 90 degrees out of phase. The board counts the edges; the order of the edges gives direction. That is the whole idea behind every wheel odometry system on a hobby robot.

| Type | How it works | Typical use | Weakness |
| --- | --- | --- | --- |
| Magnetic (Hall) | A magnet ring on the motor shaft passes Hall sensors | TT gearmotors, cheap rover kits | Lower resolution; needs clean power |
| Optical | A slotted disc interrupts an LED/phototransistor pair | Higher-resolution motors, CNC | Dust and stray light cause false edges |
| Quadrature module on the output shaft | Encoder mounted after the gearbox | Precise wheel odometry | Costs more and takes space |

Most Indian student robots use the yellow TT gearmotor with a magnetic encoder board on the back. It is cheap, it works, and its published numbers are inconsistent — which is why the first job is to measure your own.

## PPR, CPR and the gearbox trap

Pulses per revolution (PPR) is the count from one channel for one turn of the *encoder’s* shaft. Counts per revolution (CPR) is what your code sees after decoding. In full 4x quadrature decoding, every edge of both channels is counted, so CPR is four times PPR.

The trap is where the encoder sits. On a TT motor it is on the motor shaft, before the gearbox. One turn of the wheel turns the motor shaft many times, so the wheel sees the encoder count multiplied by the gear ratio.

```
counts per wheel revolution = PPR x 4 x gear ratio

Example, 11 PPR encoder, 1:48 gearbox:
11 x 4 x 48 = 2112 counts per wheel revolution

Example, 20 PPR encoder, 1:48 gearbox:
20 x 4 x 48 = 3840 counts per wheel revolution
```

Both 11 PPR and 20 PPR TT motors are sold in India under the same photos, and the gear ratio varies between 1:48 and 1:120 depending on the seller. Do not trust the listing. Run the calibration below once and write the number into the code.

## Calibrate in five minutes

- **Mark the wheel** with a strip of tape so you can see exactly one revolution.
- **Print the raw count** to serial and reset it to zero.
- **Turn the wheel exactly one full turn by hand**, slowly, and read the count. Slow turning avoids missed edges.
- **Repeat three times.** If the numbers disagree by more than a few counts, the encoder is missing edges or the wiring is picking up noise.
- **Store the result** as `COUNTS_PER_REV` and derive everything else from it.

From that one number, distance per count is the wheel circumference divided by the counts per revolution. A 65 mm TT wheel has a circumference of about 204 mm, so 2112 counts gives roughly 0.097 mm per count — finer than the wheel actually rolls, given tyre slip and gear backlash.

| Wheel diameter | Circumference | mm per count at 2112 CPR | mm per count at 3840 CPR |
| --- | --- | --- | --- |
| 43 mm | 135 mm | 0.064 | 0.035 |
| 65 mm | 204 mm | 0.097 | 0.053 |
| 80 mm | 251 mm | 0.119 | 0.065 |
| 100 mm | 314 mm | 0.149 | 0.082 |

## Reading it on Arduino and ESP32

On an Arduino Uno only pins 2 and 3 have external interrupts, so wire channel A of the left wheel to pin 2 and channel A of the right wheel to pin 3. Count on the A rising edge and read B to get direction. That is 2x decoding in practice and it halves the resolution — adjust the calibration number to match.

```
volatile long leftCount = 0;

void leftISR() {
  if (digitalRead(4) == HIGH) leftCount++;
  else leftCount--;
}

void setup() {
  pinMode(2, INPUT_PULLUP);   // encoder A
  pinMode(4, INPUT_PULLUP);   // encoder B
  attachInterrupt(digitalPinToInterrupt(2), leftISR, RISING);
  Serial.begin(115200);
}

void loop() {
  static unsigned long last = 0;
  if (millis() - last >= 100) {
    last = millis();
    noInterrupts();
    long c = leftCount;
    interrupts();
    Serial.println(c);
  }
}
```

On an ESP32 every GPIO can interrupt, so the pin choice is free. For high count rates use the PCNT (pulse counter) peripheral instead of an interrupt per edge: at 3840 counts per revolution and 1.5 revolutions per second the ESP32 is handling over 5,000 interrupts per second per wheel, which starts to crowd the Wi-Fi stack.

Keep the interrupt routine to a single increment. Printing, floating-point maths or `delay()` inside an ISR will drop counts. Copy the counter into a normal variable with interrupts briefly disabled and do the maths outside. The [interrupts and rotary encoder guide](https://roboticsengineering.in/arduino-interrupts-rotary-encoder/) covers debouncing, volatile variables and the mistakes that make counts jump.

## Turning counts into a position

For a differential-drive robot, each wheel’s distance in a time step gives the robot’s movement. Average the two for forward travel and use the difference for rotation:

```
dL = dCountsLeft  * DIST_PER_COUNT;
dR = dCountsRight * DIST_PER_COUNT;

dDistance = (dL + dR) / 2.0;
dTheta    = (dR - dL) / WHEELBASE;

x += dDistance * cos(theta + dTheta / 2.0);
y += dDistance * sin(theta + dTheta / 2.0);
theta += dTheta;
```

This is dead reckoning, and it drifts. Every count that is missed, every wheel that slips and every millimetre of error in the wheelbase accumulates. A robot that is 2% off will be a metre away from where it thinks it is after 50 m. That is normal, and it is why encoders are paired with a compass, an IMU or a line sensor rather than trusted alone. The [PID guide](https://roboticsengineering.in/pid-control-line-follower-explained/) shows how the same counts become a speed measurement for a control loop.

## Wiring and noise

- **Supply:** most encoder boards take 3.3–5 V. Check before connecting a 5 V board to a 3.3 V ESP32 pin.
- **Pull-ups:** Hall outputs are usually open-collector. Many boards include 10 kΩ resistors; bare sensors need them added.
- **Twist A/B with ground** and keep the pair away from motor leads. Encoder noise shows up as counts that only appear when the motors run.
- **Do not add a long debounce delay.** Encoders need every edge; a 5 ms debounce silently deletes counts at speed.
- **Test with the motors off** first. If counts are clean by hand and jumpy under power, the problem is electrical, not code.

## Common questions

**How many counts per revolution does a TT motor encoder have?**

It depends on the encoder and the gearbox. Common units use 11 PPR or 20 PPR on the motor shaft with a 1:48 gearbox, giving 2112 or 3840 counts per wheel revolution in 4x decoding. Measure your own unit once rather than trusting the listing.

**What is the difference between PPR and CPR?**

PPR is pulses from one channel per revolution of the encoder shaft. CPR is the total count after decoding, which is four times PPR in full quadrature. The gearbox multiplies both again at the wheel.

**Can I use one encoder channel only?**

Yes, but you lose direction unless you also read the second channel, and you halve the resolution. For odometry, use both channels.

**Why do my encoder counts jump when the motors run?**

Electrical noise from the motor leads, missing pull-ups, or a shared ground that is not solid. Twist the encoder pair, keep it away from motor wiring and check the ground.

**Do I need an encoder for a line follower?**

Not strictly. A line follower can work on sensor input alone. Encoders help with speed control, distance travelled and recovering after the robot loses the line.

Related: [Arduino interrupts and rotary encoders](https://roboticsengineering.in/arduino-interrupts-rotary-encoder/), [PID for line followers](https://roboticsengineering.in/pid-control-line-follower-explained/), [TB6612FNG motor driver](https://roboticsengineering.in/components/tb6612fng-motor-driver/).

SLAM fuses lidar with odometry, and [wheel encoders](https://roboticsengineering.in/slam-for-beginners/) explains the wheel-side half of that equation.

Encoder housings and wheel hubs are often printed; see the [design rules for printed robot parts](https://roboticsengineering.in/3d-printed-robot-parts-design/) first.

Encoders give distance but not tilt, so pair them with [an IMU angle from a complementary filter](https://roboticsengineering.in/complementary-filter-imu-arduino/).

Odometry on a steered robot uses the bicycle model described in [Ackermann steering geometry](https://roboticsengineering.in/ackermann-steering-robot-design/).

Encoder type and resolution are part of drive selection, which the [servo drive selection guide](https://roboticsengineering.in/servo-drive-selection-guide/) treats alongside current ratings.

Turning counts into a pose is the next step, covered in the [odometry guide](https://roboticsengineering.in/differential-drive-odometry-guide/).

## Sources and further reading

- [Arduino language reference](https://docs.arduino.cc/language-reference/) — `attachInterrupt()`, `digitalPinToInterrupt()` and interrupt-safe variables.
- [Incremental encoder](https://en.wikipedia.org/wiki/Incremental_encoder) — quadrature decoding and resolution definitions.

Last reviewed: October 2026. Encoder resolutions vary by supplier; the calibration method is the reliable part. Prices are Indian retail ranges. Not bench-tested by this site.
