Every usable GPIO on the ESP32 can function as an ESP32 interrupt pin, but they are not created equal. While the chip boasts 40 physical pins, only 34 are user-accessible GPIOs. All 34 support hardware interrupts via the attachInterrupt() function, but GPIOs 34 through 39 are strictly input-only and lack internal pull-up or pull-down resistors. If you wire a switch to GPIO 34 without an external 10kΩ resistor, your interrupt will trigger randomly from floating noise. Furthermore, the ESP32 operates at 3.3V logic; feeding 5V into any interrupt pin will permanently destroy the silicon.
ESP32 GPIO Interrupt Capability Matrix
Before wiring sensors or switches, you must select the right pin. The ESP32's GPIO matrix routes peripheral signals internally, but physical pin limitations still apply. Use this reference table to select safe interrupt pins for your next build.
| GPIO Pin(s) | Interrupt Support | Internal Pull-Up/Down | I/O Direction | Hardware Notes & Restrictions |
|---|---|---|---|---|
| GPIO 4, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27 | Yes | Yes (Both) | Input / Output | Safest pins for general-purpose interrupts and outputs. No boot-strapping conflicts. |
| GPIO 0, 2, 5, 15 | Yes | Yes (Both) | Input / Output | Strapping pins. Avoid using for interrupts if the pin state during boot affects your circuit (e.g., GPIO 0 must be HIGH to boot normally). |
| GPIO 12, 13, 14 | Yes | Yes (Both) | Input / Output | Default JTAG / MTCK / MTDI pins. Using these for interrupts will disable hardware debugging via JTAG. |
| GPIO 32, 33 | Yes | Yes (Both) | Input / Output | Connected to the external 40MHz crystal oscillator on some DevKit variants. Safe for interrupts, but avoid high-frequency PWM here. |
| GPIO 34, 35, 36, 39 | Yes | NO | Input Only | Requires external 10kΩ pull-up or pull-down resistors. Cannot drive outputs. Excellent for reading sensors without wasting output-capable pins. |
Project Build: Dual-Button ISR with Hardware Debouncing
This build demonstrates how to handle two interrupts simultaneously: one on a standard I/O pin with internal pull-ups, and one on an input-only pin requiring external biasing. We will use hardware debouncing (an RC filter) to prevent the ISR from firing multiple times per physical button press.
Parts List
- Microcontroller: ESP32-WROOM-32 DevKit V1 (30-pin variant)
- Switches: 2x 6x6mm Tactile Pushbuttons (SPST-NO)
- Resistors: 1x 10kΩ 1/4W carbon film (for GPIO 34 external pull-up)
- Capacitors: 2x 0.1µF (100nF) ceramic disc capacitors (for hardware debounce)
- Wiring: 22 AWG solid core jumper wires
Pin Mapping Table
| Component | ESP32 Pin | Wiring Details |
|---|---|---|
| Button 1 (Standard) | GPIO 4 | Switch to GND. 0.1µF cap across switch pins. Internal pull-up enabled in code. |
| Button 2 (Input-Only) | GPIO 34 | Switch to GND. 0.1µF cap across switch pins. 10kΩ external resistor to 3.3V. |
Wiring Steps
- De-energize the board. Unplug the USB cable before inserting components into the breadboard.
- Wire Button 1: Insert the tactile switch. Connect one leg to ESP32 GPIO 4 and the opposite leg to the GND rail. Place the 0.1µF capacitor in parallel across the switch legs.
- Wire Button 2: Insert the second switch. Connect one leg to GPIO 34 and the other to GND. Place the second 0.1µF capacitor in parallel.
- Add the external pull-up: Insert the 10kΩ resistor. Connect one end to the GPIO 34 switch leg and the other end to the 3.3V rail.
- Verify connections: Use a multimeter in continuity mode to ensure the switch legs are not shorted and that GPIO 34 reads ~3.3V when the button is open.
Complete Compilable ISR Code (ESP32-WROOM-32)
The following code targets the ESP32-WROOM-32 DevKit V1 using the Arduino IDE (ESP32 Core v2.0.x or v3.0.x). It uses the IRAM_ATTR directive to place the Interrupt Service Routine (ISR) in fast execution RAM, preventing cache-miss delays that trigger watchdog resets.
// Target Board: ESP32-WROOM-32 DevKit V1
// Core: ESP32 Arduino Core v2.0.14+
#define PIN_BTN_STANDARD 4
#define PIN_BTN_INPUT_ONLY 34
// Volatile flags to communicate between ISR and main loop
volatile bool btn1_flag = false;
volatile bool btn2_flag = false;
// Timestamps for software debounce fallback (if hardware cap is missing)
volatile unsigned long last_btn1_time = 0;
volatile unsigned long last_btn2_time = 0;
const unsigned long debounce_delay = 50; // milliseconds
// ISR for Button 1 (GPIO 4)
void IRAM_ATTR isr_btn1() {
unsigned long current_time = millis();
if (current_time - last_btn1_time > debounce_delay) {
btn1_flag = true;
last_btn1_time = current_time;
}
}
// ISR for Button 2 (GPIO 34)
void IRAM_ATTR isr_btn2() {
unsigned long current_time = millis();
if (current_time - last_btn2_time > debounce_delay) {
btn2_flag = true;
last_btn2_time = current_time;
}
}
void setup() {
Serial.begin(115200);
while(!Serial) { ; } // Wait for serial monitor
Serial.println("ESP32 Interrupt Pin Test Starting...");
// Configure GPIO 4 with internal pull-up
pinMode(PIN_BTN_STANDARD, INPUT_PULLUP);
// Configure GPIO 34 as standard input (external pull-up handles bias)
pinMode(PIN_BTN_INPUT_ONLY, INPUT);
// Attach interrupts (FALLING edge because buttons pull to GND)
attachInterrupt(digitalPinToInterrupt(PIN_BTN_STANDARD), isr_btn1, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_BTN_INPUT_ONLY), isr_btn2, FALLING);
Serial.println("ISRs attached successfully.");
}
void loop() {
// Handle Button 1 flag
if (btn1_flag) {
btn1_flag = false; // Clear flag immediately
Serial.println("[EVENT] Button 1 (GPIO 4) pressed.");
// Execute non-blocking task here
}
// Handle Button 2 flag
if (btn2_flag) {
btn2_flag = false; // Clear flag immediately
Serial.println("[EVENT] Button 2 (GPIO 34) pressed.");
// Execute non-blocking task here
}
// Main loop remains free for other tasks (WiFi, MQTT, etc.)
delay(10); // Yield to RTOS background tasks
}
Debugging ISR Failures: Errors and the First Three Checks
When working with Espressif's GPIO API under the Arduino wrapper, ISR mistakes crash the RTOS instantly. If your ESP32 reboots randomly or locks up, check the serial output for these exact error strings.
Exact Error: Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1)
Ranked Causes:
- Blocking code inside the ISR: You used
delay(),Serial.print(), orWire.requestFrom()inside theIRAM_ATTRfunction. The Watchdog Timer (WDT) expects the ISR to finish in microseconds. - Missing
IRAM_ATTR: The function is stored in flash memory. When an interrupt fires, the CPU halts to fetch the instruction from flash, causing a cache miss that trips the WDT. - Memory Allocation: You used
Stringobjects ormalloc()inside the ISR, which requires thread-safe mutex locks that deadlock in interrupt context.
Exact Error: E (142) gpio: gpio_install_isr_service(219): GPIO isr service already installed
Ranked Causes:
- Redundant Initialization: The Arduino core automatically installs the ISR service when you call
attachInterrupt(). If you also call the raw ESP-IDFgpio_install_isr_service(0)in your setup, it throws this warning. - Hot-swapping pins: Calling
attachInterrupt()repeatedly in theloop()without callingdetachInterrupt()first.
Note: This specific error is technically a warning (Level 'E' but non-fatal). The interrupt will still work, but it clutters your serial monitor.
- Verify
IRAM_ATTRand Volatile: Ensure the ISR function has theIRAM_ATTRprefix and all variables shared with the main loop are declaredvolatile. - Strip the ISR to a Flag: Remove all logic from the ISR except setting a boolean flag to
true. Move allSerial.print()and state-change logic to theloop(). - Measure the Pin Voltage: Put a multimeter on the interrupt pin. If it reads between 0.8V and 2.0V when the button is open, your pull-up resistor is missing or broken, and the pin is floating.
Extending and Simplifying the Build
Interrupts are powerful, but they are not always the correct tool for the job. According to the Arduino attachInterrupt documentation, overusing interrupts can lead to race conditions in complex RTOS environments.
How to Extend This Build
- Rotary Encoders: Use two interrupt pins (e.g., GPIO 4 and GPIO 5) to read quadrature signals. You will need to read the state of Pin B inside the ISR triggered by Pin A to determine rotation direction.
- Flow Sensors & Anemometers: These output high-frequency pulses (up to 2kHz). Use an ISR to increment a
volatile uint32_tpulse counter, then calculate the flow rate in the main loop every 1 second. - Wake from Deep Sleep: If you want the ESP32 to wake up only when a button is pressed, use
esp_sleep_enable_ext0_wakeup(). Note that ext0 wake-up only supports RTC GPIOs (like GPIO 4, 25, 26, 27, 32-39), not standard digital pins.
How to Simplify (When to Drop Interrupts Entirely)
If your input event happens slower than 50Hz (like a human pressing a button or a slow-moving limit switch), drop the interrupt and use polling. Polling with millis() eliminates the need for hardware debounce capacitors, removes the risk of WDT panics, and simplifies your codebase. Interrupts should be reserved for events that are faster than your main loop cycle time or events that must wake the CPU from a low-power sleep state.






