Handling Asynchronous Events with Arduino Interrupts
Learn how to use Interrupt Service Routines (ISRs) in Arduino to capture high-speed hardware events without polling, including the critical use of volatile variables and atomic access.
02 Jun 2026, 15:07 UTC

The Problem: Polling Latency and Missed Events
When your Arduino code relies on a loop() to check the state of a button or sensor (polling), it can only detect a change when the execution flow reaches that specific digitalRead() call. If your code is executing a delay() or a complex calculation, a fast pulse or a critical button press can occur and disappear before the processor ever checks the pin. This results in missed triggers and an unresponsive user interface.
The solution is the Interrupt Service Routine (ISR). An interrupt tells the microcontroller to pause its current task immediately, execute a small piece of critical code, and then return to exactly where it left off. This ensures that hardware events are captured in real-time, regardless of what the main loop is doing.
Implementing Hardware Interrupts
To use interrupts, you use the attachInterrupt() function. This requires three parameters: the interrupt pin, the function to run (the ISR), and the trigger mode.
Trigger Modes Defined
- LOW: Triggered whenever the pin is low.
- CHANGE: Triggered whenever the pin changes value (high to low or low to high).
- RISING: Triggered when the pin goes from low to high.
- FALLING: Triggered when the pin goes from high to low.
Worked Example: Precision Event Counter
In this example, we track how many times a button is pressed using an Arduino Uno. We use pin 2, which is one of the two hardware interrupt pins available on the ATmega328P.
// Pin 2 is used for the interrupt on the Uno
const byte interruptPin = 2;
// 'volatile' tells the compiler this variable can change outside the normal loop flow
volatile unsigned int eventCount = 0;
void setup() {
Serial.begin(9600);
// Set pin as input with internal pull-up resistor to prevent floating
pinMode(interruptPin, INPUT_PULLUP);
// Attach the ISR: Pin 2, function 'countPulse', trigger on FALLING edge
attachInterrupt(digitalPinToInterrupt(interruptPin), countPulse, FALLING);
}
void loop() {
// We use a local copy to avoid race conditions during printing
noInterrupts(); // Temporarily disable interrupts to read the multi-byte variable safely
unsigned int currentCount = eventCount;
interrupts(); // Re-enable interrupts
Serial.print("Events detected: ");
Serial.println(currentCount);
delay(1000); // The interrupt will still work during this delay
}
// The ISR function: Must be fast and avoid complex logic
void countPulse() {
eventCount++;
}
Critical Engineering Constraints
Interrupts are powerful but introduce specific risks that can lead to erratic system behavior or hard-to-debug crashes.
The 'volatile' Keyword
The Arduino compiler optimizes code by assuming a variable doesn't change unless the current line of code changes it. Because an ISR changes a variable "behind the scenes," the compiler might ignore the update in the loop(). Marking a variable as volatile forces the processor to read the value from RAM every single time it is accessed.
Atomic Access and Race Conditions
On 8-bit microcontrollers like the Uno, reading a multi-byte variable (such as an int or long) takes multiple clock cycles. If an interrupt occurs halfway through the loop() reading those bytes, the resulting value will be corrupted. To prevent this, wrap the read operation in noInterrupts() and interrupts() to ensure the read is atomic (uninterrupted).
ISR Restrictions
Inside an ISR, you must follow these rules to avoid system instability:
- No
delay(): This function relies on timer interrupts, which are disabled while another ISR is running. It will simply not work. - No
Serial.print(): Serial communication uses interrupts. Calling it inside an ISR can cause the microcontroller to hang or crash. - Keep it Short: An ISR should ideally only set a flag (a boolean) or increment a counter. Complex logic should be handled in the
loop()based on that flag.
Limitations and Verification
| Constraint | Detail | Verification Method |
|---|---|---|
| Pin Availability | Only specific pins support attachInterrupt() (e.g., 2 and 3 on Uno). |
Check digitalPinToInterrupt() mapping for your specific board. |
| Switch Bounce | Mechanical buttons create "noise" (multiple rapid triggers). | Observe the counter; if one press increments the count by 5, you need a debouncing capacitor or software timer. |
| Execution Time | Long ISRs block other system timers. | Use an oscilloscope to measure the time between the trigger edge and the resulting action. |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.