micropython-interrupciones-irq-gpio

External Interrupts (IRQ) in MicroPython

  • 5 min

An interrupt is a hardware notification that executes a function when an event occurs.

So far, if we wanted to know if a button had been pressed, we did it inside a while True loop, repeatedly checking the pin state. This technique is called Polling.

Polling has a serious problem: it is inefficient. The processor spends 99% of its time asking “ready? ready? ready?”, and if our loop has other heavy tasks (like reading a slow sensor or connecting to WiFi), we could miss the press exactly at the moment it occurs.

The usual solution is Interrupts (IRQs). Instead of constantly asking, we let the hardware notify us.

It’s the difference between looking out the window every 5 seconds waiting for the mailman, or installing a doorbell and going about your life until it rings.

What is an interrupt?

An interrupt is a signal sent by the hardware to the processor telling it: “Stop everything you are doing and attend to me right now!”.

When an event occurs on a pin (like a voltage change from 0V to 3.3V), the main program flow pauses, a special function called an ISR (Interrupt Service Routine) or Handler is executed, and upon completion, the main program continues exactly where it left off.

Configuring IRQ in MicroPython

In MicroPython, the Pin class has an .irq() method that allows us to configure these interrupts very easily.

We need to define two things:

  1. The trigger: What event should activate the interrupt?
  2. The handler: What function should be executed when it occurs?

The triggers

We can configure the interrupt to fire on different electrical signal states:

  • Pin.IRQ_RISING: Activates when the signal goes from low to high (rising edge).
  • Pin.IRQ_FALLING: Activates when the signal goes from high to low (falling edge).
  • Pin.IRQ_RISING | Pin.IRQ_FALLING: Activates on both changes.

Basic example

Let’s configure a button on GPIO 14 to toggle an internal LED each time it is pressed.

from machine import Pin
import time

# LED and Button configuration
led = Pin(2, Pin.OUT)
boton = Pin(14, Pin.IN, Pin.PULL_UP)

# Define the handling function (ISR)
def mi_interrupcion(pin):
    led.toggle()
    print("Interrupt detected on:", pin)

# Associate the interrupt with the button pin
# It will execute when the button is pressed (Falling edge if using Pull-up)
boton.irq(trigger=Pin.IRQ_FALLING, handler=mi_interrupcion)

print("Main program running...")

# Main loop empty or doing other things
while True:
    time.sleep(1)
    print("Doing important things...")
Copied!

If you run this code, you will see the message “Doing important things…” appear every second. But if you press the button, immediately the interrupt message will be printed and the LED will change, without waiting for the sleep in the main loop.

Rules for writing an ISR

Interrupts are powerful but come with great responsibility. When you are inside an ISR, the rest of the system is “frozen”. Therefore, there are strict rules:

  1. Be fast: The handler function should last microseconds. Change a variable, set a flag, or turn on an LED. Never put a time.sleep() inside an interrupt.
  2. Watch out for memory: In many MicroPython implementations, you cannot allocate memory inside an interrupt. This means you should not create new lists, complex objects, or strings. If you try to do something that consumes dynamic memory (heap), you will get an error.
  3. Avoid print: Although we used it in the example, print uses a buffer and can block or corrupt data if an interrupt occurs while the main program was also printing. Use it only for debugging.

To debug interrupts, it is very useful to execute this command at the start of the script: micropython.alloc_emergency_exception_buf(100). This reserves memory to be able to display error messages if something fails inside the ISR.

micropython.schedule: the safe way

If you need to do something “heavy” when an interrupt occurs (like printing a long log, writing to a file, or complex calculations), do not do it in the ISR.

The correct way is to use micropython.schedule. This schedules a function to be executed “as soon as possible”, but outside the strict context of the interrupt, avoiding memory problems.

import machine
import micropython

# Lightweight function that runs "later"
def tarea_pesada(arg):
    print("Processing complex data...", arg)

# ISR: Only schedule the task
def interrupcion_rapida(pin):
    # Schedule the heavy task to run as soon as the CPU can
    micropython.schedule(tarea_pesada, 123)

btn = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)
btn.irq(trigger=machine.Pin.IRQ_FALLING, handler=interrupcion_rapida)
Copied!

The bounce problem (Debounce)

If you test the button example with a real physical pushbutton, you will notice something odd: sometimes, a single press triggers the interrupt multiple times.

This is because the metallic contacts of the button physically “bounce” before stabilizing, generating a burst of Rising and Falling signals within milliseconds.

To solve this within an interrupt, we need to filter by time:

import machine
import time

led = machine.Pin(2, machine.Pin.OUT)
btn = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)

ultimo_tiempo = 0

def interrupcion_con_debounce(pin):
    global ultimo_tiempo
    tiempo_actual = time.ticks_ms()
    
    # If less than 200ms have passed since the last time, ignore
    if time.ticks_diff(tiempo_actual, ultimo_tiempo) > 200:
        led.toggle()
        print("Valid press")
        ultimo_tiempo = tiempo_actual

btn.irq(trigger=machine.Pin.IRQ_FALLING, handler=interrupcion_con_debounce)
Copied!

Critical sections

Sometimes, in our main code, we are doing something so delicate (like communicating with a sensor with very strict timing like DHT11 or NeoPixel) that we do not want to be interrupted.

To handle this, we can temporarily disable interrupts:

import machine

# Disable interrupts (critical section)
estado = machine.disable_irq()

# ... sensitive code ...

# Restore interrupts to previous state
machine.enable_irq(estado)
Copied!

Interrupts are fundamental for reactive and low-power systems. They allow us to respond instantly to external events. However, always remember: enter, do the bare minimum, and exit. If you follow this rule, your programs will fly.