ESP32/Interrupts and timing/17. Interrupts on a pin
Your chip
Your language
Interrupts and timing · 17 of 81

Interrupts on a pin

An interrupt runs your function the instant a pin changes, whatever else the board was doing. Two rules make it work and breaking either one is the usual cause of a board that reboots by itself.

/esp32/interrupts-on-a-pin · arduino · S3

One press, several interrupts

One button press, seen by attachInterrupt
6 counted
How long the contact chatters6 ms
Your debounce windownone
Edges on the wire
4
Presses counted
6
One press, 6 counted. Not a software bug — the contact really did open and close that many times. Record millis() in the ISR and ignore anything within 11 ms of the last edge. Never delay() inside an ISR: it needs a timer interrupt that an ISR has already blocked, so the board hangs.

The two rules

Keep it short. An ISR should set a volatile flag or increment a counter and return. No printing, no delay, no String, no allocation, no I2C. All of those either block or need an interrupt that yours has blocked.

Mark it IRAM_ATTR. The handler must live in RAM. If it is in flash and an interrupt arrives while the flash is being written, the chip cannot fetch the code and it crashes — rarely, and never while you are watching.

Which edge

  • RISING / FALLING — one edge only. Use these for counting.
  • CHANGE — both. Use for reading an encoder, and expect twice the events.
  • ONLOW / ONHIGH — level triggered. These fire continuously while the level holds, which is almost never what you want and is a very effective way to hang a board.

When not to use one

If the thing you are watching changes more than a few thousand times a second, an interrupt stops being free — see counting pulses. And if you are polling a pin every 10 ms anyway, polling is simpler and has no sharp edges.

On your S3
ChipXtensa LX7 · 2 × 240 MHz
Board settingESP32S3 Dev Module
Default I2CSDA 8 · SCL 9
Watch out forThe port vanishes after upload

The code

The ISR does two things and returns. Everything else - printing, maths, deciding what it means - happens in loop, where it is allowed to take time.

counter.ino
const int PIN = 4;
volatile unsigned long count = 0;
volatile unsigned long lastUs = 0;

void IRAM_ATTR onEdge() {
  unsigned long now = micros();
  if (now - lastUs < 5000) return;    // 5 ms debounce
  lastUs = now;
  count++;
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN, INPUT_PULLUP);
  attachInterrupt(PIN, onEdge, FALLING);
}

void loop() {
  noInterrupts();
  unsigned long c = count;
  interrupts();
  Serial.println(c);
  delay(500);
}

IRAM_ATTR puts the handler in RAM rather than flash. Without it, an interrupt that fires while the flash is busy crashes the board, and it will not be reproducible.

When it does not work

One press counts as three or four

Contact bounce, and the interrupt is fast enough to see all of it. Ignore edges within a few milliseconds of the last one - the figure above shows how much window you need.

The board reboots and blames the watchdog

Something in the ISR is taking too long, or calling delay. delay needs a timer interrupt that your ISR has already blocked, so the board hangs. Set a flag and return.

Serial.println inside the ISR crashes it

Printing allocates, blocks and touches the flash. None of those are safe in interrupt context. Print from loop.

The counter reads a nonsense value occasionally

You read a multi-byte variable while the ISR was updating it. Copy it with interrupts briefly disabled, or use a FreeRTOS queue.

Where this goes next

The handler fires. Getting what it learned back to the main loop is where this stops being obvious.

Sharing data with an ISR

Edit this page — content/esp32/interrupts-on-a-pin.mdx

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse ESP32 on the forum