An Arduino Interrupt Service Routine (ISR) is a special function that the microcontroller jumps to immediately when a hardware event occurs — without waiting for the current loop() iteration to finish. Mastering the arduino interrupt service routine isr is essential for any project that needs to react to events in microseconds: rotary encoders, pulse counting, emergency stop buttons, or communication protocols. This guide covers everything from the basics of attachInterrupt() to advanced ISR design rules that keep your firmware rock-solid.
Table of Contents
- What Is an Interrupt Service Routine?
- Using attachInterrupt()
- The volatile Keyword — Why It Matters
- Golden Rules for Writing Safe ISRs
- Debouncing Inside an ISR
- Timer Interrupts with TimerOne
- Real-World ISR Examples
- FAQ
What Is an Interrupt Service Routine?
In a typical Arduino sketch, the processor executes loop() sequentially — reading sensors, driving outputs, checking flags — in a continuous cycle. If an important event (a button press, a rising edge on a sensor pin, a timer tick) happens between two statements in loop(), the processor might not see it for milliseconds or even seconds, depending on how long other tasks take.
A hardware interrupt breaks this model. When the interrupt condition is met, the processor immediately:
- Saves its current execution context (program counter, registers) onto the stack
- Disables further interrupts (on AVR by default)
- Jumps to the ISR function you have registered
- On ISR return, restores the saved context and resumes
loop()exactly where it left off
This happens in hardware, typically within 3–5 clock cycles on AVR — that is sub-microsecond response time at 16 MHz. No polling loop can match this latency.
Using attachInterrupt()
The Arduino API exposes external interrupts through attachInterrupt(). The correct modern syntax is:
attachInterrupt(digitalPinToInterrupt(pin), ISR_function, mode);
Parameters:
- digitalPinToInterrupt(pin): converts a digital pin number to the interrupt number. Always use this macro — never hardcode interrupt numbers, as they differ across boards.
- ISR_function: the name of your ISR function (no parentheses).
- mode: one of
LOW,CHANGE,RISING, orFALLING.
Interrupt-capable pins by board:
- Uno / Nano / Mini: pins 2, 3
- Mega 2560: pins 2, 3, 18, 19, 20, 21
- Leonardo / Micro: pins 0, 1, 2, 3, 7
- Due / Zero / MKR series / Nano 33 IoT: all digital pins
Example — counting pulses from a Hall effect sensor on pin 2:
volatile unsigned long pulseCount = 0;
void countPulse() {
pulseCount++;
}
void setup() {
Serial.begin(115200);
pinMode(2, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(2), countPulse, RISING);
}
void loop() {
unsigned long localCount;
noInterrupts();
localCount = pulseCount;
interrupts();
Serial.println(localCount);
delay(500);
}
The volatile Keyword — Why It Matters
Any variable shared between an ISR and the main code must be declared volatile. Without it, the compiler may optimise the variable into a CPU register and never write back to RAM — meaning your ISR’s updates are invisible to loop(), and vice versa.
// CORRECT
volatile bool buttonPressed = false;
// WRONG — compiler may cache this in a register
bool buttonPressed = false;
volatile tells the compiler: “always read this variable from memory, never from a cached register.” It does not make the variable access atomic on AVR — for multi-byte types (int, long, float), you must also protect reads with noInterrupts() / interrupts() as shown in the example above.
Golden Rules for Writing Safe ISRs
ISRs have strict constraints. Violating them causes subtle, hard-to-debug bugs — timing glitches, frozen sketches, or corrupted data.
Rule 1: Keep ISRs as short as possible. An ISR blocks all other interrupts (on AVR). Long ISRs delay millis() updates, UART receive, and other interrupt-driven peripherals. The ideal ISR sets a flag and returns immediately — do the work in loop().
volatile bool eventFlag = false;
void onEvent() {
eventFlag = true; // Set flag only — do NOT process here
}
void loop() {
if (eventFlag) {
eventFlag = false;
// Do your processing here, safely
}
}
Rule 2: Never use delay() or millis() inside an ISR. delay() relies on timer interrupts which are disabled inside the ISR. millis() returns the same frozen value throughout ISR execution. Use micros() only for timestamping — it partially works inside an ISR but does not update on 8-bit AVR during ISR.
Rule 3: Never use Serial.print() inside an ISR. Serial transmission uses interrupts internally. Calling it from an ISR causes a deadlock — the ISR waits for a transmit interrupt that will never fire because interrupts are disabled.
Rule 4: Protect multi-byte variable reads in main code. On 8-bit AVR, reading a 16-bit or 32-bit variable is not atomic — it takes two or four read instructions. An interrupt between those instructions can corrupt the value. Always bracket multi-byte reads with noInterrupts():
noInterrupts();
unsigned long safeCopy = sharedLongVar;
interrupts();
// Use safeCopy, not sharedLongVar directly
Rule 5: Do not allocate memory in an ISR. malloc(), new, and String concatenation are not interrupt-safe and can corrupt the heap.
Debouncing Inside an ISR
Mechanical switches produce multiple rapid transitions (bounce) when pressed. A naive ISR increments a counter dozens of times per button press. Debouncing must be done carefully at ISR speed:
volatile unsigned long lastInterruptTime = 0;
volatile int buttonPresses = 0;
void buttonISR() {
unsigned long interruptTime = micros();
if (interruptTime - lastInterruptTime > 50000UL) { // 50ms debounce
buttonPresses++;
}
lastInterruptTime = interruptTime;
}
The 50 ms window filters mechanical bounce. micros() is safe enough inside a simple ISR for this purpose, even though it partially stalls on AVR. For production designs, a hardware RC filter (10 kΩ + 100 nF) on the button pin is even more reliable and removes the need for software debounce entirely.
Timer Interrupts with TimerOne
External pin interrupts are event-driven. Timer interrupts fire at a fixed periodic rate regardless of external events — perfect for tasks like PID control, sensor sampling at exactly 100 Hz, or LED PWM without using analogWrite.
The TimerOne library wraps AVR Timer1 (16-bit on Uno) for easy periodic ISR setup:
#include <TimerOne.h>
volatile bool sampleFlag = false;
void sampleISR() {
sampleFlag = true;
}
void setup() {
Timer1.initialize(10000); // 10,000 µs = 100 Hz
Timer1.attachInterrupt(sampleISR);
}
void loop() {
if (sampleFlag) {
sampleFlag = false;
// Read sensor at precise 100 Hz rate
}
}
The TimerOne library is available in the Arduino Library Manager. For the Mega, consider TimerThree to avoid conflicts with Timer1 used by the Servo library.
Real-World ISR Examples
Frequency Counter: Measure the frequency of a square wave by counting RISING edges per second using an ISR. Clear the count in loop() every 1000 ms:
volatile unsigned long freq_count = 0;
void freqISR() { freq_count++; }
void setup() {
Serial.begin(115200);
attachInterrupt(digitalPinToInterrupt(2), freqISR, RISING);
}
void loop() {
delay(1000);
noInterrupts();
unsigned long hz = freq_count;
freq_count = 0;
interrupts();
Serial.print(hz); Serial.println(" Hz");
}
Emergency Stop: Immediately halt all motor outputs when a safety pin goes LOW, regardless of what the main loop is doing:
const int MOTOR_PIN = 9;
const int ESTOP_PIN = 3;
volatile bool estopActive = false;
void estopISR() {
analogWrite(MOTOR_PIN, 0); // Direct hardware action — OK here
estopActive = true;
}
void setup() {
pinMode(MOTOR_PIN, OUTPUT);
pinMode(ESTOP_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ESTOP_PIN), estopISR, FALLING);
}
Note: Direct pin manipulation inside an ISR is acceptable for safety-critical immediate action — it is the processing-heavy tasks (Serial, String, delay) that must be avoided.
FAQ
How many external interrupts does Arduino Uno have?
The Arduino Uno (ATmega328P) has two external interrupt pins: digital pin 2 (INT0) and digital pin 3 (INT1). For more external interrupts, use Arduino Mega (6 interrupt pins) or Nano Every / boards with PCINT (Pin Change Interrupt) support which allows interrupts on any pin, though with less precision.
Can I use delay() inside an ISR?
No. delay() depends on Timer0 overflow interrupts to count milliseconds. Since interrupts are globally disabled while your ISR runs (on AVR), delay() inside an ISR will hang the program indefinitely. Similarly, millis() will not advance inside an ISR.
What is the difference between RISING, FALLING, and CHANGE modes?
RISING triggers when the pin transitions from LOW to HIGH. FALLING triggers on HIGH to LOW. CHANGE triggers on either transition. LOW continuously triggers the ISR as long as the pin is LOW (use with caution — it fires repeatedly, not once). For most buttons and encoders, RISING or FALLING is the correct choice.
What does volatile actually do at the compiler level?
volatile instructs the compiler to always generate a memory read/write instruction for that variable, never substituting a cached register value. Without it, the compiler’s optimiser legally assumes the variable cannot change between two reads in the same function and skips the second read — which breaks ISR communication entirely.
Can ISRs be nested on Arduino?
On AVR Arduinos (Uno, Mega, Nano), interrupts are globally disabled at the start of every ISR, so ISRs do not nest by default. You can re-enable interrupts inside an ISR with sei() to allow nesting, but this is advanced and risky — only do it if you fully understand the re-entrancy implications and have a compelling reason.
Ready to build faster, more responsive Arduino projects? Explore our full range of Arduino boards at Zbotic — from Uno to Mega to Nano Every, shipped fast across India.
Add comment