ESP32 Tutorial: External Interrupt Pin usage
Abstract
Standard microcontroller programs use the loop() function to constantly check, or poll, the status of input pins. This is inefficient and can miss fast-changing events. External Interrupts provide an event-driven alternative, allowing the ESP32 to pause its main program execution, run a specific function (Interrupt Service Routine or ISR) immediately upon detecting a change on a pin, and then return to the main loop. This tutorial focuses on configuring a GPIO pin on the ESP32 to trigger an external interrupt using attachInterrupt().
1. Introduction
In the polling method (digitalRead() in the loop()), if a button press lasts only a microsecond, and your loop() is delayed by a network request or a sensor reading that takes milliseconds, you will miss the event.
Interrupts solve this by using hardware. When the voltage on a designated pin changes (e.g., from LOW to HIGH), the hardware generates an interrupt signal that causes the CPU to immediately stop its current task and jump to the pre-defined ISR function.
ESP32 Interrupt Features
- Flexibility: Almost any GPIO pin on the ESP32 can be configured as an external interrupt source.
- Dual-Core: Because the ESP32 is a Dual-Core processor, interrupt handlers (ISRs) are automatically set up to run on the non-primary core (Core 1), ensuring that the main tasks running on Core 0 are interrupted for the absolute minimum time.
2. Core Function for ESP32 Interrupts
Setting up an interrupt requires three main steps: defining the pin mode, defining the ISR function, and attaching the ISR to the pin.
2.1 Defining the Interrupt Service Routine (ISR)
The ISR is the function that is executed when the interrupt is triggered.
- Rule 1: ISRs must be fast. Do not use delay(), print(), or perform long, complex operations inside an ISR. These actions can interfere with other critical timing in the microcontroller.
- Rule 2: Use volatile Any variable modified within the ISR and used in the main loop must be declared with the volatile keyword. This tells the compiler that the variable’s value can change unexpectedly (by the ISR) and must be read from memory every time it is accessed.
2.2 Attaching the Interrupt: attachInterrupt(pin, ISR, mode)
This function links a physical pin to the ISR function and specifies the condition under which the interrupt is triggered.
- pin: The GPIO pin number to monitor (e.g., GPIO 15).
- ISR: The name of the function to call when the interrupt is triggered (the Interrupt Service Routine).
- mode: The condition for triggering the interrupt:
| Mode | Trigger Condition | Common Use |
|---|---|---|
| CHANGE | Triggers when the pin changes from HIGH to LOW or LOW to HIGH (any change). | Toggling a state. |
| RISING | Triggers only when the pin changes from LOW to HIGH (rising edge). | Button Press (Pull-Down config). |
| FALLING | Triggers only when the pin changes from HIGH to LOW (falling edge). | Button Press (Pull-Up config). |
3. Hands-On Lab: Fading an LED with PWM
This lab uses an interrupt to accurately count how many times a button has been pressed, even if the presses are very fast.
Wiring
- Connect the Pushbutton between a chosen GPIO pin (e.g., GPIO 15) and GND.
- Connect the onboard LED (typically GPIO 2) for visual feedback.
- Ensure you use the INPUT_PULLUP mode for the button pin, so the pin is HIGH by default, and a press generates a FALLING edge (HIGH to LOW).
ESP32 External Interrupt Code Example
#define BTN_PIN 15 // GPIO pin for the button
#define LED_PIN 2 // GPIO pin for the onboard LED
// Global variable to store the last time the interrupt was triggered
volatile unsigned long last_interrupt_time = 0;
const unsigned long DEBOUNCE_DELAY = 50; // 50ms debounce time
// Variables shared between loop() and the ISR must be declared 'volatile'
volatile int buttonPressCount = 0;
volatile bool ledState = LOW;
// The Interrupt Service Routine (ISR) - Must be fast!
void IRAM_ATTR buttonISR() { // IRAM_ATTR is recommended for ESP32 ISRs
unsigned long current_time = millis();
// Debouncing logic: check if enough time has passed since the last valid interrupt
if ((current_time - last_interrupt_time) > DEBOUNCE_DELAY) {
ledState = !ledState; // Toggle the LED state
// Update the last interrupt time
last_interrupt_time = current_time;
// For simplicity, we just change the variables here:
buttonPressCount++;
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
// Set the button pin as an input with internal pull-up enabled
pinMode(BTN_PIN, INPUT_PULLUP);
// Attach the interrupt:
// Trigger on a FALLING edge (Button pressed from HIGH to LOW)
attachInterrupt(BTN_PIN, buttonISR, FALLING);
}
void loop() {
// 1. Core Logic: Use the volatile variable to control the LED
digitalWrite(LED_PIN, ledState);
// 2. Monitoring: Print the count in the main loop
Serial.print("Total Button Presses: ");
Serial.println(buttonPressCount);
// The delay is long, but the interrupt still works instantly when the button is pressed.
delay(1000);
}
Execution Steps
- Upload the sketch to your ESP32 board.
- Open the Serial Monitor and ensure the baud rate is set to 115200.
- Press the button and observe the behavior:
4. Lab Recap
You’ve learned how to implement event-driven programming on the ESP32:
- Interrupts are necessary for efficiently capturing fast or time-sensitive events, preventing missed signals due to long delays in the main loop().
- Variables shared between the ISR and loop() must be declared as volatile.
- Interrupt Service Routines (ISRs) must be kept extremely brief, avoiding functions like delay() and print().
- The function attachInterrupt(pin, ISR, mode) is used to configure the pin and the trigger condition (RISING, FALLING, or CHANGE).
- In the provided example, the FALLING mode was used because the button was wired with an INPUT_PULLUP


