Learning Goals 5 min
So far you've commanded motors and hoped. With encoders, you measure the wheels actually turning — counts per second tell you RPM; total counts tell you distance. PID controllers using encoder feedback are how real robots achieve precise speed and position. By the end of this lesson you will:
- Wire an optical or Hall-effect encoder on a TT motor (or use a hobby motor with built-in encoder).
- Use interrupts to count pulses without missing edges.
- Implement a closed-loop speed controller: target RPM in, measured RPM measured, motor duty adjusted by PID.
Warm-Up 10 min
Hardware:
- TT motor with encoder (the "yellow gearbox + encoder" combo is common in robot kits).
- L298N motor driver.
- UNO + power.
What an encoder gives you
Two types in hobby motors:
- Optical: a slotted disc on the motor shaft + an IR LED + phototransistor that pulses each slot.
- Hall-effect: small magnets on the shaft + Hall-effect sensor that pulses each magnet.
Either way: a digital signal that pulses N times per shaft revolution. Common values: 6 pulses/rev (raw motor) × 1:120 gearbox = 720 pulses per output-wheel revolution. Different motors vary.
Single vs quadrature encoders
Two flavours:
- Single channel: one pulse signal. Counts speed but not direction.
- Quadrature: two signals A and B, 90° out of phase. Counts speed AND direction (the A-vs-B order tells which way).
For line-followers / robots driving one direction at a time, single is fine. For self-balancing robots, quadrature is essential.
New Concept · Pulse counting + speed control 25 min
Counting pulses with interrupts
const int ENCODER_PIN = 2; // INT0
volatile unsigned long pulseCount = 0;
void onPulse() {
pulseCount++;
}
void setup() {
pinMode(ENCODER_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_PIN), onPulse, RISING);
}Every rising edge on D2 triggers the interrupt; pulseCount increments. The variable must be volatile because it changes outside the main flow.
Computing RPM
const int PULSES_PER_REV = 720; // measure your motor
unsigned long lastSample = 0;
unsigned long lastCount = 0;
void loop() {
unsigned long now = millis();
if (now - lastSample >= 100) { // sample every 100 ms
noInterrupts();
unsigned long c = pulseCount;
interrupts();
unsigned long deltaP = c - lastCount;
unsigned long deltaT = now - lastSample;
lastSample = now;
lastCount = c;
float pulsesPerSec = (deltaP * 1000.0) / deltaT;
float rpm = pulsesPerSec * 60.0 / PULSES_PER_REV;
Serial.print("RPM: "); Serial.println(rpm);
}
}Sample period of 100 ms gives 10 RPM updates per second — plenty for PID at a few Hz.
Quadrature: knowing direction
const int ENC_A = 2, ENC_B = 4;
volatile long position = 0;
void onA() {
if (digitalRead(ENC_B) == HIGH) position++;
else position--;
}
void setup() {
pinMode(ENC_A, INPUT_PULLUP);
pinMode(ENC_B, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENC_A), onA, RISING);
}When A rises, the state of B tells you direction. position is signed — positive = forward, negative = reverse. For high-resolution / high-speed encoders, the "1× decoding" here is enough for hobby use.
Closed-loop speed control
#include "PID.h"
#include "motor.h"
const Motor m = {9, 8, 5}; // IN1, IN2, ENA
PID speedPID(0.8, 0.4, 0.02);
float targetRPM = 60.0;
void loop() {
static unsigned long lastTick = 0;
unsigned long now = millis();
if (now - lastTick < 100) return;
lastTick = now;
// (compute current RPM from pulse delta, as above)
float rpm = ...;
float u = speedPID.update(targetRPM, rpm);
int duty = constrain((int)u, 0, 255);
motorSpeed(m, duty);
}Now the chassis runs at exactly 60 RPM regardless of load. Going uphill? PID drives more duty. Going downhill? Less. Battery sagging? Compensated automatically.
Position control
For "move exactly N centimetres forward", integrate pulses. With 720 pulses per wheel revolution and a wheel circumference of 21 cm (≈ 6.7 cm radius), 1 cm of travel = 720 / 21 ≈ 34 pulses. Drive forward until position increments by N × 34.
Worked Example · Speed-controlled wheel 25 min
Step 1 — find PULSES_PER_REV
Mark the wheel with tape. Spin it by hand exactly one revolution. Watch the pulse counter; subtract before/after = pulses per revolution. Common values: 360, 540, 720 for TT motors with built-in encoders.
Step 2 — sketch
// L04-27 · Closed-loop speed control
#include "PID.h"
#include "motor.h"
const int ENC_PIN = 2;
const int PULSES_PER_REV = 720;
const Motor motorA = {9, 8, 5}; // IN1, IN2, ENA
PID speedPID(0.8, 0.4, 0.02);
volatile unsigned long pulses = 0;
unsigned long lastCount = 0;
unsigned long lastSample = 0;
float targetRPM = 60.0;
void onPulse() { pulses++; }
void setup() {
Serial.begin(115200);
pinMode(ENC_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENC_PIN), onPulse, RISING);
motorInit(motorA);
}
void loop() {
unsigned long now = millis();
if (now - lastSample < 100) return;
noInterrupts();
unsigned long c = pulses;
interrupts();
unsigned long deltaP = c - lastCount;
unsigned long deltaT = now - lastSample;
lastCount = c;
lastSample = now;
float pps = (deltaP * 1000.0) / deltaT;
float rpm = pps * 60.0 / PULSES_PER_REV;
float u = speedPID.update(targetRPM, rpm);
int duty = constrain((int)u, 0, 255);
motorSpeed(motorA, duty);
Serial.print(rpm); Serial.print(" "); Serial.println(duty);
}Step 3 — run + plot
Lift the wheel off the floor. Open Serial Plotter. RPM should climb and lock onto 60. Touch the wheel to slow it; PID bumps duty up to compensate.
Step 4 — tune
- Kp = 0.5, Ki = 0, Kd = 0 → slow approach to setpoint, persistent steady-state offset.
- Add Ki = 0.3 → eliminates offset.
- Kd only if you see overshoot when target RPM changes.
Step 5 — position control
Modify to count pulses to a target:
long targetPulses = 720 * 2; // 2 full revolutions
void loop() {
// ... compute current pulses ...
long error = targetPulses - (long)pulses;
if (abs(error) < 5) { motorSpeed(motorA, 0); return; } // arrived
int duty = constrain((int)(error * 0.5), -255, 255);
motorSpeed(motorA, duty);
}Robot moves exactly the requested distance and stops. With encoders, this works regardless of surface friction or battery state.
Try It Yourself 15 min
Goal: Drive the chassis exactly 50 cm forward, then stop. Use the cm-to-pulses formula from §3.
Goal: Two motors, two encoders, two PIDs. Both wheels at the same target RPM regardless of load mismatch. Chassis drives perfectly straight.
Goal: "Drive forward 1 m, turn 90° right, drive 1 m, repeat to draw a square". Use position control + the wheel-track-width formula to convert 90° to a pulse count differential.
Mini-Challenge · Two-wheel calibration 10 min
- Measure your wheel diameter precisely. Compute circumference.
- Drive the chassis forward 1 metre using encoder counts only. Measure how far it actually went.
- Adjust the cm-per-pulse constant. Re-test. Aim for < 2 cm error per metre.
- Note: with encoders, this is precision-machined-tool good. Without, you'd be ±15 cm.
Recap 5 min
Encoders close the loop on motor speed and position. Interrupts count pulses; PID adjusts duty to hit target RPM or pulse count. Quadrature gives direction. The combination of L298N + TT motors + encoders + PID + IMU (tomorrow) is the standard self-balancing-robot recipe. Tomorrow: meet the MPU6050.
- Encoder
- A sensor that pulses with rotation. Optical or Hall-effect varieties.
- Single-channel encoder
- One pulse output. Counts speed; no direction.
- Quadrature encoder
- Two pulse outputs 90° out of phase. Direction-aware.
- Pulses per revolution (PPR)
- Encoder resolution. Higher = more precise. Combined with gearbox ratio for output-shaft PPR.
- Interrupt service routine (ISR)
- Function called by hardware on an event (e.g. pin edge). Must be brief; reads / writes guarded variables.
- volatile
- Keyword telling the compiler the variable may change asynchronously. Required for ISR-touched variables.
- noInterrupts / interrupts
- Disable / re-enable global interrupts. Use briefly to safely read multi-byte volatile values.
- Closed-loop speed control
- Measure RPM → compare to setpoint → adjust duty. The encoder-based PID pattern.
- Position control
- Integrate pulses to track position. Drive until position matches the target.
Homework 5 min
- Build the §4 speed-controlled wheel. Plot RPM vs duty.
- Drive a precise 50 cm and 100 cm. Measure error.
- Read ahead to ARD-L04-28 (The MPU6050 IMU). Bring an MPU6050 module.