Interrupts

Lesson 93: Using attachInterrupt() External Interrupts

Learn how attachInterrupt() links hardware pin events to ISR callbacks for fast event-driven input handling.

Progress indicator

Lesson 93 of 93

Learning Objectives

  • Understand what attachInterrupt() does and why it is used for event-driven input handling.
  • Learn attachInterrupt() syntax and required ISR function structure.
  • Understand trigger modes like RISING, FALLING, CHANGE, and LOW.
  • Use digitalPinToInterrupt() correctly for board-safe interrupt mapping.
  • Avoid common ISR mistakes that cause unstable or missed interrupt behavior.
  • Build a practical model for ISR-to-loop data flow using volatile variables and snapshots.

Concept Explanation

What is attachInterrupt()

attachInterrupt() connects a hardware interrupt source (usually a pin event) to a function called an ISR (Interrupt Service Routine).

When the selected trigger event happens, the CPU pauses normal loop code briefly and runs the ISR quickly.

This gives faster response than polling in loop(), especially for short pulses or quick button events that might be missed by slow loop timing.

attachInterrupt() Syntax

Basic syntax: attachInterrupt(digitalPinToInterrupt(pin), isrFunction, mode);

  • digitalPinToInterrupt(pin): maps pin to interrupt number.
  • isrFunction: ISR callback to run on event.
  • mode: trigger type (RISING/FALLING/CHANGE/LOW).

The ISR function should be a fast function with no parameters and no return value.

How External Interrupts Work

  1. Hardware detects edge/level change on configured pin.
  2. Interrupt controller notifies CPU.
  3. CPU runs ISR immediately (or very quickly).
  4. After ISR, CPU returns to normal code execution.

Think of it as a high-priority callback path from hardware to your code.

Interrupt Trigger Modes

  • RISING: LOW to HIGH edge.
  • FALLING: HIGH to LOW edge.
  • CHANGE: both edges.
  • LOW: low-level trigger (board-dependent behavior).

For button with INPUT_PULLUP, FALLING is common (pressed state often goes LOW).

Wrong trigger mode can cause double counts, missed presses, or unstable behavior.

ISR (Interrupt Service Routine) Basics

ISR should be very short and fast. Usually update a flag/counter only.

Avoid delay(), heavy Serial printing, dynamic allocation, or long loops inside ISR.

Do the heavy work in loop() after reading ISR-updated flags.

digitalPinToInterrupt() Explained

This helper converts a board pin number into the correct interrupt mapping value.

It keeps code portable and avoids hardcoded interrupt numbers.

This is important when moving code between boards where interrupt mapping differs.

When to Use attachInterrupt()

  • Fast button press capture without polling delay.
  • Encoder pulse counting and speed measurements.
  • Edge-triggered event logging and timing tasks.
  • Responsive input where loop polling might miss events.

Real-life examples include rotary encoders, pulse sensors, and event counters.

Example Code

This example toggles LED state and increments an interrupt counter on button edge events.

const int BUTTON_PIN = 0;
const int LED_PIN = 2;
volatile bool toggleLed = false;
volatile unsigned long interruptCount = 0;

void IRAM_ATTR onButtonFallingEdge() {
  toggleLed = !toggleLed;
  interruptCount = interruptCount + 1;
}

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(115200);

  attachInterrupt(
    digitalPinToInterrupt(BUTTON_PIN),
    onButtonFallingEdge,
    FALLING
  );
}

void loop() {
  bool localToggle;
  unsigned long localCount;

  noInterrupts();
  localToggle = toggleLed;
  localCount = interruptCount;
  interrupts();

  digitalWrite(LED_PIN, localToggle ? HIGH : LOW);

  Serial.print("Interrupt count: ");
  Serial.print(localCount);
  Serial.print(" | LED state: ");
  Serial.println(localToggle ? "ON" : "OFF");

  delay(200);
}

Example Code Explanation

  1. ISR toggles a volatile flag and increments a volatile counter.
  2. setup() configures input/output and attaches interrupt using FALLING mode.
  3. loop() copies volatile values in a short critical section.
  4. LED state is updated using the copied local value.
  5. Serial monitor prints counter and LED state for verification.
  6. Snapshot variables keep output consistent even if ISR fires during loop execution.
  7. ISR remains lightweight, so interrupt latency stays low.

What Happens Inside

  1. Button edge triggers external interrupt hardware logic.
  2. CPU jumps into ISR and updates shared volatile data.
  3. CPU returns to loop() after ISR exits.
  4. loop() safely snapshots shared data under short interrupt pause.
  5. Application uses stable snapshot to update outputs and logging.
Internal flow: hardware edge -> ISR updates volatile state -> loop snapshots safely -> loop processes snapshot -> UI/output reflects latest stable event state.

Common Mistakes with attachInterrupt()

  • Using heavy logic, delay(), or blocking code inside ISR.
  • Forgetting volatile on ISR-shared variables.
  • Choosing wrong trigger mode for button wiring logic.
  • Skipping digitalPinToInterrupt() and using wrong interrupt mapping.
  • Reading shared data in loop() without safe copy protection.

Best Practices for attachInterrupt()

  • Keep ISR minimal: set flags/counters only.
  • Use volatile for all shared ISR variables.
  • Use digitalPinToInterrupt() for safe board mapping.
  • Copy shared data in short critical sections before processing.
  • Use debouncing strategy for noisy mechanical buttons.

Try it now

Open the simulator workspace and step through external interrupt edge events, ISR updates, and safe snapshot logic.

Run in Simulator