HomeArduinoCore TutorialsExternal Interrupt in Arduino

Arduino External Interrupts Tutorial: attachInterrupt(), ISR, Modes & LED Example

When running code inside the loop(), it cannot react to an external event until the current iteration finishes. For a button press lasting 50 ms, that delay might be imperceptible. For a rotary encoder spinning at speed, a pulse counter, or a motion trigger that fires in microseconds, it is far too slow. External interrupts fix this by pausing the main program the instant a pin changes its state, running a dedicated function, and returning.

In this tutorial you’ll learn exactly what an interrupt is at the hardware level and why it is fundamentally different from polling. Which pins on Uno, Nano, Mega, Leonardo, and Zero support hardware interrupts. How to use attachInterrupt() and detachInterrupt() with all trigger modes. And how to write an Interrupt Service Routine.

This is part of the Arduino Core Tutorials series. Related tutorials are as follows:

For interrupt-based multitasking, the Arduino FreeRTOS tutorials build directly on what you’ll learn here.

Arduino External Interrupt Pins: A Beginner’s Guide

How External Interrupts Work

An interrupt is a hardware signal that pauses the main program, runs a special function called an ISR (Interrupt Service Routine), and then returns to exactly where the main program left off. The whole process happens in few microseconds.

Diagram showing Arduino main loop pausing when external interrupt fires on pin 2, ISR running, then returning to main loop

Without interrupts, you must poll a pin repeatedly in loop(). Polling misses brief signals if the loop is busy and wastes CPU cycles on constant checking. Interrupts eliminate both problems.

External interrupts are triggered by voltage changes on dedicated hardware pins. These pins are directly wired to the microcontroller’s interrupt logic. Only specific pins support this, not every digital pin qualifies.

Interrupt Pins by Board

BoardInterrupt PinsNotes
UNO / Nano / Pro Mini2, 3INT0 = pin 2, INT1 = pin 3
Mega 25602, 3, 18, 19, 20, 216 external interrupts
Leonardo / Micro0, 1, 2, 3, 75 external interrupts
DueAll digital pinsSAMD21 supports all pins
ESP32Almost all pinsVia GPIO matrix
Arduino Uno pinout diagram highlighting pin 2 (INT0) and pin 3 (INT1) as the two external hardware interrupt pins

To confirm which pins support interrupts on your board, use digitalPinToInterrupt(). It returns the interrupt number if the pin is valid, or -1 if not:

void setup() {
  Serial.begin(9600);
  delay(2000);

  Serial.print("Pin 2: "); Serial.println(digitalPinToInterrupt(2));  // 0 on UNO
  Serial.print("Pin 3: "); Serial.println(digitalPinToInterrupt(3));  // 1 on UNO
  Serial.print("Pin 4: "); Serial.println(digitalPinToInterrupt(4));  // -1 (not valid)
  Serial.print("Pin 5: "); Serial.println(digitalPinToInterrupt(5));  // -1 (not valid)
}

void loop() {}

The image below shows the output of the code on the serial monitor.

Serial Monitor showing pin 2 returns INT0, pin 3 returns INT1, pin 4 and pin 5 return -1 on Arduino UNO

Always use digitalPinToInterrupt(pin) inside attachInterrupt(), never hardcode the interrupt number directly. Interrupt numbers differ across boards. The function digitalPinToInterrupt() handles this automatically.

attachInterrupt() and Trigger Modes

Syntax

attachInterrupt(digitalPinToInterrupt(pin), ISR_function, mode);
  • pin — the interrupt-capable pin number
  • ISR_function — the function to run when the interrupt fires (no parameters, returns void)
  • mode — the signal condition that triggers the interrupt

To disable an interrupt:

detachInterrupt(digitalPinToInterrupt(pin));

After detachInterrupt(), the pin returns to a regular input. Re-enable it any time with attachInterrupt().


Trigger Modes

ModeTriggers WhenTypical Use
RISINGPin goes LOW → HIGHButton release, pulse start
FALLINGPin goes HIGH → LOWButton press (with INPUT_PULLUP)
CHANGEPin changes in either directionRotary encoders, pulse counting
LOWPin is continuously LOWLevel-triggered, less common

FALLING is the most common choice for buttons with INPUT_PULLUP. The pin remains at HIGH and goes LOW when pressed.

CHANGE doubles the resolution of rotary encoders by catching both edges of each pulse.

LOW fires continuously while the pin is held low, not just on the transition — use it with care to avoid an interrupt loop.


Configuring the Pin — INPUT vs INPUT_PULLUP

Before attaching an interrupt, you must configure the pin as an input. This tells the Arduino that the pin will receive a signal from an external device, like a button or sensor.

You can set a pin as:

  • INPUT — for reading an external signal with your own pull-up or pull-down resistor.
  • INPUT_PULLUP — enables Arduino’s internal pull-up resistor, keeping the pin HIGH by default and detecting a LOW signal when pressed.

Example:

pinMode(2, INPUT_PULLUP); // Configure pin 2 as input with internal pull-up

Using INPUT_PULLUP is convenient because you don’t need an external resistor. The pin is automatically pulled to the VDD, hence it also avoids the risk of damaging the MCU with High Voltage input.


detachInterrupt() — Disable an Interrupt

Sometimes, you may want to temporarily disable an interrupt, for example, during a critical section of your program or to avoid false triggers. To stop an interrupt from responding, use the detachInterrupt() function.

detachInterrupt(digitalPinToInterrupt(pin));

Example:

detachInterrupt(digitalPinToInterrupt(2)); // Disable interrupt on pin 2

After calling this function, the interrupt will no longer trigger until you re-enable it with attachInterrupt() again. This feature is handy when handling noisy inputs or when you need more control over timing-sensitive tasks.

Button Toggle LED Example

This example uses a push button on pin 2 to toggle an LED on pin 13. The ISR sets a flag. The main loop reads the flag, debounces, and toggles the LED. This is the correct pattern for interrupt-driven input handling.

Wiring

  • Button: one leg to pin 2, other leg to GND. INPUT_PULLUP keeps the pin HIGH by default — pressing the button pulls it LOW.
  • LED: anode to pin 13 via a 220 Ω resistor, cathode to GND. The UNO’s built-in LED is already on pin 13.
Arduino Uno wiring diagram for external interrupt LED toggle — push button to pin 2 (INT0) with INPUT_PULLUP, LED to pin 13 via 220Ω resistor

Code

const int buttonPin = 2;   // INT0 — external interrupt pin
const int ledPin    = 13;  // LED output pin

volatile bool irqFlag = false;      // set inside ISR, read in loop()
unsigned long lastToggle  = 0;
const unsigned long debounceMs = 50;

void setup() {
  pinMode(ledPin,    OUTPUT);
  pinMode(buttonPin, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(buttonPin), handleButtonISR, FALLING);

  Serial.begin(9600);
  Serial.println("Ready — press the button.");
}

void loop() {
  if (irqFlag) {
    irqFlag = false;

    unsigned long now = millis();
    if (now - lastToggle >= debounceMs) {
      digitalWrite(ledPin, !digitalRead(ledPin));  // toggle LED
      lastToggle = now;
    }
  }
}

// ISR — keep it short. Only set the flag.
void handleButtonISR() {
  irqFlag = true;
}

volatile bool irqFlag is the bridge between the ISR and loop(). The ISR only sets it to true — no logic, no Serial, no delay. The main loop checks it on every iteration, clears it, and then does the actual work: debouncing and toggling the LED.

The debounce check compares the current millis() against lastToggle. If less than 50 ms has passed since the last toggle, the press is ignored — this filters out the multiple rapid transitions a mechanical button generates when physically pressed.

Result

The GIF below shows the simulation for this project on the TinkerCAD simulator.

Animated GIF showing TinkerCAD simulation of Arduino Uno — LED on pin 13 toggling on each button press at pin 2 using external interrupt

Each button press fires one interrupt, sets the flag, and the LED changes state exactly once. The debounce logic ensures a single press never produces multiple toggles.

ISR Rules and Troubleshooting

ISR Rules

Keep ISRs short. The ISR interrupts everything — including other time-sensitive code. A long ISR delays both the main program and any other pending interrupts.

Never use delay() inside an ISR. delay() depends on timer interrupts to count time. Since interrupts are disabled during an ISR, delay() will hang indefinitely.

Never use Serial.print() inside an ISR. Serial transmission uses interrupts internally. Calling it from an ISR causes unpredictable behaviour and potential lock-ups.

Declare shared variables as volatile. Without volatile, the compiler may cache the variable in a register and never re-read it from memory. volatile forces a fresh memory read on every access, ensuring the main loop sees the value the ISR wrote.

volatile bool irqFlag = false;    // correct
volatile int  pulseCount = 0;     // correct for counters too

Disable interrupts when reading multi-byte volatile variables in loop(). A 16-bit or 32-bit variable can be read mid-update by the main loop if the ISR modifies it between reads. Use noInterrupts() / interrupts() to guard the read:

noInterrupts();
unsigned long count = pulseCount;  // atomic copy
interrupts();

Button Bounce and False Triggers

A mechanical button does not produce a clean transition. When pressed, the contacts bounce for 1–10 ms, generating dozens of rapid HIGH/LOW transitions. Each transition fires a new interrupt, causing multiple ISR calls from a single press.

The software debounce in the example above handles this. For hardware debounce, add a 100 nF capacitor across the button contacts (one leg to pin, one leg to GND). The capacitor absorbs the bounce transients before they reach the pin.

LED Toggles Multiple Times per Press

The debounce window is too short. Increase debounceMs from 50 to 100. If it still triggers multiple times, add a hardware capacitor.

Interrupt Never Fires

  • Check that you are using an interrupt-capable pin. Run the digitalPinToInterrupt() checker above — it must not return -1.
  • Confirm pinMode() is called before attachInterrupt().
  • Check the trigger mode matches the signal. With INPUT_PULLUP and a button to GND, use FALLING.

millis() Drifts or Freezes

You are calling delay() or blocking code inside the ISR. Remove it. millis() is driven by Timer0’s overflow interrupt — if your ISR runs too long or disables interrupts, Timer0 does not update.

Using Interrupts vs Polling

Use interrupts when you need to catch brief signals (under 1 ms), count pulses from encoders or sensors, or respond to events while the main loop is busy with other work.

Use polling when the signal changes slowly (buttons pressed for 100+ ms), you already have a tight loop that checks inputs frequently, or the added complexity of ISR flag management is not justified.

Arduino External Interrupts FAQ

Conclusion

External interrupts are the most fundamental upgrade you can make to how Arduino responds to the physical world. Polling burns CPU cycles checking states that may not have changed. Interrupts let the hardware do that work — the moment the signal changes, the ISR fires, the flag is set, and your main loop handles it in the next cycle without missing anything.

In this tutorial you covered the complete picture: how the ATmega328P hardware interrupt controller works, which pins carry INT0 and INT1 on Uno/Nano versus the expanded sets on Mega and Leonardo, how to configure attachInterrupt() with all four trigger modes, how to write a correct ISR using the volatile flag pattern that keeps the handler to a single line, and how to add millis()-based debounce in the main loop to handle the mechanical noise of a real push button.

From here, interrupts feed naturally into more advanced concurrency. The Arduino FreeRTOS Interrupts tutorial shows how to signal RTOS tasks from an ISR using binary semaphores — the same flag pattern you used here, but with proper task scheduling behind it. The IR Sensor counter and PIR motion detector tutorials both use interrupt-driven pulse counting in practice. Download the full project above and explore the Arduino Core tutorials for the next step.

Download Arduino External Interrupt Project Files

Complete Arduino project with digitalPinToInterrupt() pin checker sketch and full LED toggle example using attachInterrupt() on INT0 (pin 2), volatile flag ISR pattern, and millis()-based debounce. No external libraries required. Compatible with Uno, Nano, Mega, and Leonardo (check board interrupt pin map). Free to download — support the work if it helped you.

ISR Flag Pattern millis() Debounce Uno / Nano / Mega

Browse More Arduino Core Tutorials

1 2
About the Author
Arun Rawat
Arun Rawat
Embedded Systems Engineer · Founder, ControllersTech

Arun is an embedded systems engineer with 10+ years of experience in STM32, ESP32, and AVR microcontrollers. He created ControllersTech to share practical tutorials on embedded software, HAL drivers, RTOS, and hardware design — grounded in real industrial automation experience.

Subscribe
Notify of

0 Comments
Newest
Oldest Most Voted
×

Don’t Miss Future STM32 Tutorials

Join thousands of developers getting free guides, code examples, and updates.