If you are building a reactive embedded system, polling a pin in your loop() is a bottleneck. The direct answer for handling Arduino interruptions (hardware interrupts) on the standard Arduino Uno R3 is to use pins D2 (INT0) and D3 (INT1) paired with the attachInterrupt() function and volatile variables. This allows your ATmega328P microcontroller to instantly pause its main routine, execute an Interrupt Service Routine (ISR), and resume without missing a beat.

Project Difficulty: Intermediate | Time Required: 45 Minutes | Target Board: Arduino Uno R3 (ATmega328P, 16MHz)

The Physics and Logic of Arduino Interruptions

Think of your microcontroller's main loop as a car driving down a single-lane highway. Polling is like the driver constantly checking the rearview mirror to see if an ambulance is behind them. It works, but it distracts from driving and wastes cycles. An interrupt, conversely, is the ambulance hitting its siren. The driver immediately pulls over (pauses the main loop), lets the ambulance pass (executes the ISR), and then merges back into traffic (resumes the loop).

In the ATmega328P silicon, hardware interruptions are triggered by voltage transitions on specific pins. When a FALLING edge (5V to 0V) hits Pin D2, the CPU finishes its current machine instruction (which takes a maximum of 62.5 nanoseconds at 16MHz), pushes the current program counter onto the hardware stack, and jumps to the interrupt vector address.

This is non-negotiable for high-speed signals. If you are reading a rotary encoder spinning at 3,000 RPM with 20 detents, you are generating over 1,000 pulses per second. A standard loop() running a delay(10) will miss 90% of those pulses. Hardware interruptions guarantee you catch every single edge, provided your ISR executes fast enough.

Test Rig Parts List and Pin Mapping

To demonstrate reliable interrupt handling, we will build a debounced pushbutton counter. Mechanical switches suffer from 'contact bounce'—the metal contacts physically rattle for 1 to 5 milliseconds before settling, which the 16MHz CPU reads as dozens of rapid presses.

ComponentSpecificationPurpose
MicrocontrollerArduino Uno R3 (ATmega328P)Main processing unit, 5V logic
Switch6x6mm SPST Tactile PushbuttonTrigger source for the interrupt
Capacitor0.1µF X7R Ceramic (50V)Hardware debounce (low-pass filter)
Resistor10kΩ Carbon Film (1/4W)External pull-up (optional if using internal)
Wiring22 AWG Solid Core Jumper WiresBreadboard connections

Pin Mapping Table

Arduino Uno R3 PinATmega328P PortConnection TargetNotes
D2 (INT0)PD2Pushbutton NO ContactHardware interrupt pin 0
5VVCCPushbutton NC ContactProvides HIGH signal
GNDGNDCapacitor Leg 2Reference ground for filter
D13PB5Onboard LEDVisual feedback (optional)
Bench Tip: While the Uno R3 has dedicated external interrupt pins (D2 and D3), the ATmega328P also supports Pin Change Interrupts (PCINT) on almost all other digital and analog pins. However, PCINTs only trigger on *any* logic change, not specific rising/falling edges, requiring you to read the pin state manually inside the ISR.

The Complete Interrupt and Debounce Code

The following C++ code targets the Arduino Uno R3. It uses a hybrid debouncing approach: a 0.1µF capacitor across the switch handles the high-frequency microsecond ringing, while a software timer inside the ISR ignores any secondary bounces that occur within a 50-millisecond window.

#include <Arduino.h>

// Pin Definitions
#define INTERRUPT_PIN 2
#define LED_PIN 13

// 50ms debounce window in microseconds
#define DEBOUNCE_US 50000 

// Variables shared between ISR and main loop MUST be volatile
volatile unsigned long last_interrupt_time = 0;
volatile int button_count = 0;

void setup() {
  // Initialize serial for debugging
  Serial.begin(115200);
  while (!Serial) { ; } // Wait for serial port (Leonardo/Micro, harmless on Uno)
  
  // Configure pins
  pinMode(INTERRUPT_PIN, INPUT_PULLUP); // Uses internal 20k pull-up
  pinMode(LED_PIN, OUTPUT);
  
  // Attach the interrupt handler
  // digitalPinToInterrupt() translates pin 2 to INT0 automatically
  attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), handleButtonPress, FALLING);
  
  Serial.println('System ready. Waiting for interruptions...');
}

void loop() {
  // Main loop remains free for heavy processing
  if (button_count > 0) {
    // Critical section: disable interrupts briefly to read multi-byte volatile safely
    noInterrupts();
    int local_count = button_count;
    button_count = 0; // Reset counter
    interrupts();
    
    Serial.print('Button pressed count: ');
    Serial.println(local_count);
    
    // Toggle LED state
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
  }
}

// Interrupt Service Routine (ISR)
void handleButtonPress() {
  unsigned long current_time = micros();
  
  // Software debounce check
  if ((current_time - last_interrupt_time) > DEBOUNCE_US) {
    button_count++;
    last_interrupt_time = current_time;
  }
}

Debugging Interrupt Failures: The First Three Things to Check

When your interrupt code fails, it rarely fails silently. It either locks up the board, misses counts, or throws compiler warnings. If your build is failing, check these three things in order:

1. The Clobbered Variable Warning (Missing Volatile)

Exact Error String: warning: variable 'button_count' might be clobbered by 'longjmp' or 'vfork' [-Wclobbered]

The Cause: The GCC compiler optimizes code by caching variables in CPU registers. If a variable is modified inside an ISR but not marked volatile, the main loop will read the cached register value and never see the update.

The Fix: Add the volatile keyword to any variable touched by both the ISR and the main loop. Furthermore, when reading multi-byte variables (like a 32-bit unsigned long) in the main loop, wrap the read in noInterrupts() and interrupts() to prevent the ISR from firing mid-read and corrupting the data.

2. The Watchdog Timeout (ISR Taking Too Long)

Exact Error String: Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1) (Common when porting to ESP32)

The Cause: You put blocking code inside the ISR. Functions like delay(), millis() (which relies on the Timer0 interrupt), and Serial.print() rely on interrupts to function. If you call them from inside an ISR, you create a deadlock or trigger the hardware watchdog timer.

The Fix: An ISR must be ruthlessly brief. Set a flag, update a counter, or record a micros() timestamp, then immediately return. Let the main loop() handle the heavy lifting, math, and serial printing.

3. Switch Bounce and False Triggering

Symptom: One physical button press registers as 15 counts.

The Cause: Mechanical contact bounce. The CPU is vastly faster than the physical switch.

The Fix: Implement the hybrid debounce shown in the code above. If software debouncing fails due to extreme EMI environments, add a hardware RC low-pass filter (a 10kΩ resistor in series with the signal, and a 0.1µF capacitor to ground) or use a Schmitt trigger IC like the 74HC14.

How to Extend or Simplify Your Interrupt Build

To Simplify: If you are breadboarding a quick prototype, drop the external 10kΩ pull-up resistor. The ATmega328P has internal 20kΩ pull-up resistors that can be activated via pinMode(pin, INPUT_PULLUP). Wire one side of the button to D2 and the other directly to GND. Set the interrupt to trigger on FALLING. This reduces your part count and wiring complexity instantly.

To Extend: If you need to read a rotary encoder, you need two interrupt pins to track quadrature decoding (Phase A and Phase B). Since the Uno R3 only has two dedicated external interrupt pins (D2 and D3), you are maxed out. To extend this, you have two options:

  1. Use Pin Change Interrupts (PCINT): Libraries like EnableInterrupt allow you to assign ISRs to almost any pin on the Uno, though you must manually check which pin triggered the event inside the handler.
  2. Upgrade the Silicon: Move to an ESP32 DevKit v1. The ESP32 has 32 GPIO pins, and almost all of them support full external interrupts with specific edge triggering. (Note: ESP32 requires the IRAM_ATTR attribute on the ISR function to keep it in fast RAM).
Safety Caveat: Never wire mains voltage (120V/240V AC) directly to a microcontroller interrupt pin, even through a massive resistor divider. Always use an optocoupler (like the PC817) or a dedicated zero-crossing detector IC to galvanically isolate the high voltage from your 5V logic.

Frequently Asked Questions About Arduino Interruptions

Can I use Serial.print() inside an Arduino interruption handler?

No. Serial.print() relies on the UART hardware buffer and background interrupts to transmit data byte-by-byte. If you call it from within an ISR, you risk deadlocking the CPU or corrupting the serial buffer. Always use the ISR to set a volatile boolean flag, and let the main loop() check that flag and execute the Serial.print() command.

Which pins support hardware interruptions on the Arduino Uno R3?

On the standard Arduino Uno R3 (and Nano v3), only Digital Pin 2 (INT0) and Digital Pin 3 (INT1) support dedicated external hardware interrupts via attachInterrupt(). If you are using an Arduino Mega 2560, you gain four additional dedicated pins: D18, D19, D20, and D21.

How do I fix the 'ISR not in IRAM' error when moving to ESP32?

When porting interrupt code from an AVR-based Uno to an ESP32, you will often encounter cache errors if the ISR is loaded from slow flash memory. You must prefix your ISR function definition with the IRAM_ATTR macro. For example: void IRAM_ATTR handleButtonPress() { ... }. This forces the compiler to place the function in the ESP32's fast internal RAM.

What is the maximum execution time for an Arduino ISR?

There is no hard hardware limit, but practically, an ISR on a 16MHz Uno should execute in under 5 to 10 microseconds. If your ISR takes longer, you risk missing subsequent interrupts, delaying the millis() timer (which causes timing drift in your main loop), and dropping incoming serial data. If your interrupt logic requires complex math or I2C sensor reads, you are using the wrong architecture; use a polling state machine or a dedicated timer interrupt instead.