Interrupts vs polling: the same pulses, counted both ways
- 192parts on the bench
- 25boards running now
- 1.00xreal time, on every board
There are two ways for a sketch to notice that a pin changed. It can poll: read the pin in loop() and compare it with the last reading. Or it can ask the hardware for an interrupt: the chip watches the pin on its own and, the moment the edge arrives, stops whatever loop() was doing to run a short function you gave it. Most of the time polling is fine. This page shows the case where it is not, with numbers.
The circuit above connects a signal generator to pin 2 of an Uno. It sends a 0 to 5 V square wave at 50 Hz with a 2 percent duty cycle, so every pulse is 0.4 milliseconds wide and they arrive every 20 milliseconds. The sketch counts the pulses twice at once, by polling and with an interrupt on INT0, and it gives loop() some other work to do, a 10 millisecond delay standing in for reading a sensor or redrawing a display. Once a second it prints both counts.
When we ran it the serial monitor showed "last second: interrupt 50, polling 0" for second after second, with the occasional "polling 7" when the timing happened to line up. The interrupt counted every pulse. Polling counted almost none.
Why polling misses them
Polling only sees the pin at the instant digitalRead runs. In this sketch that is once every pass through loop(), roughly every 10 milliseconds because of the work in the middle. A pulse that starts and ends between two reads is simply never seen. With 0.4 ms pulses and a read every 10 ms, a read lands inside a pulse only when the timing happens to line up.
That is also why the polled count comes in bursts rather than trickling in. The loop runs at a little over 10 ms a pass and the pulses come every 20 ms, so the moment of each read drifts slowly past the pulses. For several seconds every read falls between pulses and the count is 0; then for a second or so the reads walk across the pulses and it catches several. The same effect, called aliasing, is why a camera can make a spinning wheel look as if it stands still.
Open the sketch in the editor, change WORK_MS from 10 to 0 and press Run. Now loop() reads the pin tens of thousands of times a second and polling catches every pulse too. That is the trade in one line: polling works exactly as well as your loop is fast.
How the interrupt version works
attachInterrupt(digitalPinToInterrupt(2), onRise, RISING) tells the ATmega328P to watch pin 2 for a rising edge. On an Uno only pins 2 and 3 can do this, as INT0 and INT1, and digitalPinToInterrupt turns the pin number into the interrupt number for you. When the edge arrives the chip finishes the instruction it is on, saves where it was, runs onRise, and goes back. The delay() in loop() is interrupted too and carries on afterwards as if nothing happened.
The handler itself is one line: pulses++. Everything else happens back in loop(), which takes a copy of the count and prints it.
volatile unsigned long pulses = 0;
void onRise() {
pulses++; // the interrupt: every edge
}
void loop() {
int level = digitalRead(SIGNAL_PIN);
if (level == HIGH && lastLevel == LOW) {
polled++; // polling: only if we happen to look
}
lastLevel = level;
delay(WORK_MS); // the rest of the loop's work
// ... once a second, print both counts
}Rules for interrupt handlers
Keep the handler short. While it runs, other interrupts wait, including the timer interrupt that makes millis() tick. Set a flag or bump a counter and get out. Never call delay() in a handler, and avoid Serial.print there too, since it can wait on the very interrupts that are held off.
Mark shared variables volatile. pulses is changed by the handler and read by loop(); volatile tells the compiler it can change at any moment, so it must be read from memory every time rather than kept in a register.
Read multi-byte values with interrupts off. An unsigned long is four bytes and the ATmega328P reads one byte at a time. If the interrupt fires halfway through loop() reading it, you get two old bytes and two new ones: a number that never existed. The sketch wraps the read in noInterrupts() and interrupts() for exactly this reason. It is a bug that shows up once in a few thousand reads, which makes it very hard to find on a real bench.
When to poll anyway
Polling is the right choice more often than not. A pushbutton pressed by a person stays down for tens of milliseconds, far longer than any sensible loop takes, and polling it keeps all the logic in one place. It also avoids the bounce problem that makes an interrupt on a mechanical switch fire several times per press, which the button debounce page shows happening.
Reach for an interrupt when the event is shorter than your loop, when you must not miss one (counting pulses from a flow meter or a rotary encoder), or when you need to react within microseconds rather than whenever loop() comes round. Keep loop() free of long delays and you will need interrupts less often, which is what the blink without delay page is about.
Questions
Which Arduino pins support interrupts?
On an Uno or Nano, pins 2 and 3 through attachInterrupt. A Mega has six, and boards like the ESP32 and the Raspberry Pi Pico can attach an interrupt to almost any GPIO. The ATmega328P also has pin-change interrupts on every pin, but those need register-level code or a library.
Why does my interrupt count go up by more than one per button press?
Contact bounce. A mechanical switch opens and closes several times in the first few milliseconds, and an interrupt faithfully counts every edge. Debounce in software by ignoring edges for a few milliseconds after the first, or add a small capacitor across the switch.
Does millis() work inside an interrupt handler?
You can read it, but it does not advance while the handler runs, because the timer interrupt that updates it is waiting. That is another reason to keep handlers short.
What is the difference between RISING, FALLING and CHANGE?
Which edge fires the handler: low to high, high to low, or both. LOW fires continuously while the pin is low, which is rarely what you want in a sketch.
Build this for real
Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.