Interrupts and Timers Explained
Why Interrupts Exist
loop() runs sequentially — if you're doing a delay(1000) somewhere, the Arduino is completely unresponsive to everything else for that full second. Interrupts let specific events (a pin changing state, a timer reaching a count) immediately pause whatever loop() is doing and jump to a dedicated handler function, then resume exactly where it left off.
External (Pin Change) Interrupts
volatile int pulseCount = 0; void setup() { pinMode(2, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(2), onPulse, FALLING); } void onPulse() { pulseCount++; } void loop() { // pulseCount updates in the background, unaffected by whatever // else loop() is doing }On an Uno/Nano, only pins 2 and 3 support external interrupts natively — this is a hardware limitation of the ATmega328P, not a software one. Mega has more (pins 2, 3, 18, 19, 20, 21).
The volatile Keyword Matters
Any variable modified inside an interrupt handler and read in loop() must be declared volatile — this tells the compiler not to cache the value in a register, since it can change asynchronously at any point. Skipping this is a classic bug that causes intermittent, hard-to-diagnose "it works sometimes" behavior.
Keep Interrupt Handlers Tiny
No delay(), no Serial.print(), no long computation inside an interrupt handler — other interrupts are blocked while one is executing, and a slow handler can cause you to miss subsequent pulses entirely. Set a flag or increment a counter, then handle the actual logic back in loop().
Hardware Timers (More Advanced)
Beyond pin interrupts, the ATmega328P has 3 hardware timers (Timer0 used internally by millis()/delay(), Timer1 and Timer2 free for your use) that can trigger an interrupt at a precise, configurable interval — independent of anything else the chip is doing. Libraries like TimerOne wrap the raw register manipulation into a usable API:
#include <TimerOne.h> void setup() { Timer1.initialize(500000); // microseconds — fires every 0.5s Timer1.attachInterrupt(onTimer); } void onTimer() { // runs every 0.5s, precisely, regardless of what loop() is doing }When You Actually Need This
Rotary encoders, frequency counting, precise PWM generation beyond what analogWrite() offers, and any "must not miss this event even if loop() is busy" scenario (button debouncing done properly, pulse counting from a flow sensor) are the classic real-world use cases.
Related Guides
- Rotary Encoders for Makers: Incremental vs Absolute, Debouncing, and Reading with Arduino/ESP32
- How to Program Addressable LED Strips: WS2812B Patterns, Effects, and Power Design
- How to Use the ESP32-CAM: Video Streaming, Motion Detection, and Time-Lapse
- I2C vs SPI vs UART: How to Choose and Use Serial Communication Protocols
- How to Use Sensors with Arduino and ESP32: Temperature, Distance, Load, Current, and Hall Effect
- How to Control Motors with Arduino and ESP32: Stepper, DC, and Servo Drivers
- Correcting Beam Astigmatism in High-Power Diodes
- Implementing Material Docking and Pre-heating on CNC-Laser Hybrids