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.
One press, several interrupts
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.
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.
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.
MicroPython schedules the handler rather than running it in true interrupt context, which makes it more forgiving - but the same discipline applies.
from machine import Pin
import time
count = 0
last = 0
def on_edge(pin):
global count, last
now = time.ticks_us()
if time.ticks_diff(now, last) < 5000:
return
last = now
count += 1
Pin(4, Pin.IN, Pin.PULL_UP).irq(trigger=Pin.IRQ_FALLING, handler=on_edge)
while True:
print(count)
time.sleep(0.5)Allocating memory inside a handler raises MemoryError in hard-interrupt mode. Keep it to arithmetic on variables that already exist.
When it does not work
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.
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.
Printing allocates, blocks and touches the flash. None of those are safe in interrupt context. Print from loop.
You read a multi-byte variable while the ISR was updating it. Copy it with interrupts briefly disabled, or use a FreeRTOS queue.
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.