TL;DR:
Polling checks a pin over and over from the main loop, so it can miss a pulse that comes and goes between checks.
An interrupt makes the chip pause the main code, run a short handler, and then resume exactly where it was.
Keep handlers very short, share data through volatile variables, and protect multi-byte reads.
A wheel encoder sends a pulse every time the wheel turns by a tiny angle. Your program must count every pulse, or the robot will think it travelled less than it did. But the same program also has to read sensors, update motors and print messages. How do you count pulses without watching the pin every moment?
Polling versus interrupts
Polling means the main loop reads the pin every time round and compares it with the last reading. It is simple and fine for slow signals such as a button. Its weakness is that the check happens only once per loop. If one trip around the loop takes 5 ms and a pulse lasts 1 ms, the pulse can come and go between two checks.
An interrupt reverses the arrangement. The hardware watches the pin. When a chosen event happens, such as a rising edge, the chip finishes its current instruction, saves its place, runs a small function called an interrupt service routine (ISR or handler), and then goes back to what it was doing. No pulse is missed, as long as the handler gets to run before the next one arrives.
Worked example: counting encoder ticks with an interrupt
A wheel has a radius of 0.03 m and an encoder that gives 360 ticks per wheel revolution, after any gearing. First, what does one tick mean in distance?
Circumference:
2π × 0.03 ≈ 0.1885 m.Distance per tick:
0.1885 / 360 ≈ 0.000524 m, about 0.52 mm.At 1 revolution per second the tick rate is 360 per second, so a tick arrives every
1 / 360 ≈ 2.78 ms. At that speed the robot moves0.1885 m/s.If the main loop takes 5 ms per pass (for example, because of printing and sensor reads), polling checks the pin about every 5 ms, which is longer than the 2.78 ms between ticks. It would miss about half of them.
Now count with an interrupt. This sketch is for an Arduino Uno-style board, where pin 2 supports interrupts.
const int ENC_PIN = 2;
volatile unsigned long ticks = 0;
void onTick() {
ticks++; // keep it this short
}
void setup() {
Serial.begin(115200);
pinMode(ENC_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENC_PIN), onTick, RISING);
}
unsigned long lastTicks = 0;
unsigned long lastTime = 0;
void loop() {
if (millis() - lastTime >= 100) {
noInterrupts();
unsigned long t = ticks; // safe copy
interrupts();
unsigned long dt = t - lastTicks;
lastTicks = t;
lastTime += 100;
float revPerSec = dt / 360.0 / 0.1;
Serial.println(revPerSec);
}
}Check the speed math. At 1 rev/s, a 100 ms window should hold 360 × 0.1 = 36 ticks. The code turns 36 ticks into 36 / 360 / 0.1 = 1.0 rev/s, which matches. The speed in meters per second is that number times 0.1885.
A simple encoder has one channel and cannot tell forward from backward. A quadrature encoder has two channels offset by a quarter of a cycle; reading the second channel inside the handler tells you the direction, so the count goes up or down.
Keeping handlers short
While a handler runs, other interrupts normally wait, so a long handler delays everything else and can make you miss the next tick. The ISR above takes a few microseconds, far below the 2.78 ms gap. The rules are:
Do the minimum: set a flag, add to a counter, store a timestamp. Do the real work in the main loop.
Never call
delay(), and avoidSerial.printinside a handler. They wait on other interrupts and can hang.Mark any variable shared with the handler as
volatile, so the compiler re-reads it each time instead of caching it.On an 8-bit chip, a 16-bit or 32-bit variable is read in several steps. If an interrupt fires in the middle, you get a torn value. Copy it with interrupts briefly off, as in the sketch.
Keep interrupts off for as short a time as possible, so you do not miss pulses.
Timing with timers
Timers are the other half of timing. A hardware timer counts clock cycles on its own and can raise an interrupt at a fixed interval, which is how a control loop runs steadily at 100 Hz or 1 kHz. Using millis() to check the elapsed time in the main loop, as the sketch does, is the simple version. Avoid delay() for anything that must stay responsive, because the chip does nothing else while waiting.
Where it goes wrong
Contact bounce. A mechanical switch makes several fast edges for one press, so the count jumps. Debounce in hardware or by ignoring edges for a few milliseconds.
Electrical noise. Motor wiring can inject false pulses. Use short wires, shielding or a filter.
Too many interrupts. If the tick rate is too high for the handler, ticks pile up and are lost.
Wrong edge. Counting both rising and falling edges doubles the count.
Forgetting
volatile. The main loop may keep reading a stale value.
Safety: Test encoder and motor code with the robot's wheels lifted off the ground and your fingers clear of the wheels. A bug in speed control can make a motor run at full power.
How real robots use it
Encoder counting, motor control timing, serial data arrival and sensor readiness are all driven by interrupts in real robot firmware. Speed control built on encoder ticks pairs naturally with PWM, controlling speed with fast switching. The input side of the story, how a pin reads a signal at all, is in reading a sensor with a microcontroller.
Try it yourself
Wire a button to pin 2 with the internal pull-up and count presses in two ways: by polling in a loop that includes a delay(50), and with an interrupt. Press quickly and compare. Then add debounce logic to the interrupt version and see the count match your presses.
Frequently asked questions
Are interrupts always better than polling?
No. Polling is simpler and fine for slow, non-critical inputs. Use interrupts for fast pulses or events that cannot wait.
How many interrupt pins do I get?
It depends on the chip and board. Check your board's documentation for which pins support external interrupts.
