Learn

ESP32 interrupts with attachInterrupt

Start building free

Or see every lesson.

  • 455parts on the bench
  • 43boards running now
  • 1.00xreal time, on every board
ESP32 DevKit: a button on GPIO 15live0.000 s 0.00x
Hold the button
The button pulls GPIO 15 to ground, so a press is a FALLING edge.

An interrupt lets an ESP32 notice a pin change the moment it happens, whatever loop() is busy with. You write a short function, mark it IRAM_ATTR, and hand it to attachInterrupt() with the pin and the edge to watch. Every GPIO on an ESP32 can trigger one. The two things that go wrong are the button, which bounces, and the variable the handler shares with loop(), which has to be volatile.

The circuit above is an ESP32 DevKit with a button from GPIO 15 to ground. Below, a counter on that pin pressed three times, with the button’s bounce switched off, then at the typical setting, then at its worst, and the debounce that brought all three back to 3.

attachInterrupt, IRAM_ATTR and volatile

pinMode(15, INPUT_PULLUP) holds the pin high until the button pulls it low, and attachInterrupt(digitalPinToInterrupt(15), onPress, FALLING) runs onPress() on each high-to-low edge. The handler only adds one to a counter; loop() does the printing. On an ESP32, digitalPinToInterrupt(15) is just 15, but writing it keeps the sketch portable to an Uno.

IRAM_ATTR puts the handler in internal RAM. On a real ESP32 most code runs from flash through a cache, and the cache is switched off while the flash is being written, for example by Preferences or WiFi. A handler left in flash that fires then crashes the board. In Mokxi all code already runs from RAM, so the mark changes nothing here, but write it every time.

volatile tells the compiler that the variable can change behind its back. A loop that waited with while (presses == 0) { } on a plain int never ended here, through three presses: the compiler read the variable once and kept the copy. With volatile int presses the same loop printed "pressed" at the first press.

Counting presses with an interrupt
volatile int presses = 0;

void IRAM_ATTR onPress() {
  presses++;
}

void setup() {
  Serial.begin(115200);
  pinMode(15, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(15), onPress, FALLING);
}

void loop() {
  static int shown = 0;
  if (presses != shown) {
    shown = presses;
    Serial.print("presses: ");
    Serial.println(shown);
  }
}

Why one press counts more than once

A push button does not close once. Its contacts bounce for a few milliseconds when they meet and again when they part, and an interrupt is fast enough to catch every bounce. Mokxi’s push button bounces the way a real one does, with a setting for how badly. Pressed three times, the sketch above counted 3 with bounce off, 14 at the typical setting and 46 at the worst.

The usual fix in tutorials is a time check: ignore a FALLING edge within 50 milliseconds of the last one counted. It counted 6 for 3 presses, at both settings. The press is counted once, but the release bounces too, and some of its edges fall. They come 200 milliseconds after the press, long after the 50 are up.

A debounce that counts one per press

The handler below watches both edges with CHANGE and reads the pin each time. When it reads HIGH it notes the time; when it reads LOW it counts a press only if the pin had been high for more than 30 milliseconds. A bounce during the press or the release never stays high that long, and a real press always follows a long high. It counted 3 for 3 presses with bounce off, typical and worst.

Keep the handler this short. No Serial.print(), no delay() and nothing that waits: on a real ESP32 a handler that runs too long trips the interrupt watchdog and the board resets. Set a flag or a count, and let loop() do the slow part. detachInterrupt(15) stops watching the pin; a sketch that called it once the count reached 2 stayed at 2 through the third press.

A debounced press counter
volatile int presses = 0;
volatile unsigned long highSince = 0;

void IRAM_ATTR onEdge() {
  unsigned long now = millis();
  if (digitalRead(15) == HIGH) {
    highSince = now;
  } else if (now - highSince > 30) {
    presses++;
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(15, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(15), onEdge, CHANGE);
}

void loop() {
  static int shown = 0;
  if (presses != shown) {
    shown = presses;
    Serial.print("presses: ");
    Serial.println(shown);
  }
}

ESP32 interrupt pins, and the ones that need a resistor

Every GPIO can interrupt, but a pin with nothing holding it high or low floats, and a floating pin interrupts on noise. With pinMode(15, INPUT) instead of INPUT_PULLUP and the button released, the pin read 0 and the counter reached 141 in two seconds, with three real presses among them.

GPIO 34 to 39 are input only and have no internal pull-up at all, so INPUT_PULLUP on them does nothing. The same button on GPIO 34 with INPUT_PULLUP counted 142. With a 10 k resistor from GPIO 34 to 3.3 V the pin read 1 at rest and counted the three presses, 3. Use those pins for interrupts only with a resistor, or with a sensor that drives its output both ways.

For several buttons with one handler, attachInterruptArg(pin, handler, &data, FALLING) passes a pointer to the handler; with a struct for the button it counted 3 for 3 presses. Avoid GPIO 6 to 11, which are the flash, and the strapping pins 0, 2, 5, 12 and 15 for anything held low at power on. GPIO 15 is fine for a button that is open at boot.

Questions

Which ESP32 pins can be used for interrupts?

Every GPIO. GPIO 34 to 39 have no internal pull-up, so they need a resistor or a sensor that drives the pin, and GPIO 6 to 11 are the flash.

Why is my ESP32 attachInterrupt not working?

Most often the pin floats (no INPUT_PULLUP or resistor), the shared variable is not volatile, the pin is 34 to 39 without a pull-up, or the handler prints or delays.

What does IRAM_ATTR do on an ESP32?

It puts the handler in internal RAM, so it can run while the flash cache is off during a flash write. Without it a handler that fires at that moment crashes the board.

How do I debounce a button interrupt on an ESP32?

Watch CHANGE, note when the pin goes high, and count a low only after it has been high for about 30 ms. A plain 50 ms gap after FALLING still counts the release bounce.

Can I use Serial.print in an ESP32 interrupt?

No. Printing waits for the UART, and a slow handler can trip the interrupt watchdog. Set a variable in the handler and print it from loop().

Build this for real

Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.