micropython-interrupciones-irq-gpio

Interrupciones externas (IRQ) en MicroPython

  • 5 min

Una interrupción es un aviso del hardware que ejecuta una función cuando ocurre un evento.

Hasta ahora, si queríamos saber si se había pulsado un botón, lo hacíamos dentro de un bucle while True, comprobando el estado del pin repetidamente. A esta técnica se le llama Polling (o sondeo).

El polling tiene un problema grave: es ineficiente. El procesador se pasa el 99% del tiempo preguntando “¿ya? ¿ya? ¿ya?”, y si nuestro bucle tiene otras tareas pesadas (como leer un sensor lento o conectarse al WiFi), podríamos perdernos la pulsación justo en el momento en que ocurre.

La solución habitual son las Interrupciones (IRQ). En lugar de preguntar constantemente, dejamos que el hardware nos avise.

Es la diferencia entre mirar por la ventana cada 5 segundos esperando al cartero, o instalar un timbre y seguir haciendo tu vida hasta que suene.

¿Qué es una interrupción?

Una interrupción es una señal que envía el hardware al procesador para decirle: “¡Para todo lo que estás haciendo y atiéndeme ahora mismo!”.

Cuando ocurre un evento en un pin (como un cambio de voltaje de 0V a 3.3V), el flujo del programa principal se pausa, se ejecuta una función especial llamada ISR (Interrupt Service Routine) o Manejador, y al terminar, el programa principal continúa exactamente donde se quedó.

Configuración de IRQ en MicroPython

En MicroPython, la clase Pin tiene un método .irq() que nos permite configurar estas interrupciones de forma muy sencilla.

Necesitamos definir dos cosas:

  1. El trigger (disparador): ¿Qué evento debe activar la interrupción?
  2. El handler (manejador): ¿Qué función se debe ejecutar cuando ocurra?

Los disparadores

Podemos configurar la interrupción para que salte en diferentes estados de la señal eléctrica:

  • Pin.IRQ_RISING: Se activa cuando la señal pasa de bajo a alto (flanco de subida).
  • Pin.IRQ_FALLING: Se activa cuando la señal pasa de alto a bajo (flanco de bajada).
  • Pin.IRQ_RISING | Pin.IRQ_FALLING: Se activa en ambos cambios.

Ejemplo básico

Vamos a configurar un pulsador en el GPIO 14 para que encienda un LED interno cada vez que se pulse.

from machine import Pin
import time

# Configuración del LED y el Botón
led = Pin(2, Pin.OUT)
boton = Pin(14, Pin.IN, Pin.PULL_UP)

# Definimos la función de manejo (ISR)
def mi_interrupcion(pin):
    led.toggle()
    print("¡Interrupción detectada en:", pin)

# Asociamos la interrupción al pin del botón
# Se ejecutará cuando el botón se pulse (Flanco de bajada si usamos Pull-up)
boton.irq(trigger=Pin.IRQ_FALLING, handler=mi_interrupcion)

print("Programa principal corriendo...")

# Bucle principal vacío o haciendo otras cosas
while True:
    time.sleep(1)
    print("Haciendo cosas importantes...")
Copied!

Si ejecutas este código, verás que el mensaje “Haciendo cosas importantes…” sale cada segundo. Pero si pulsas el botón, inmediatamente se imprimirá el mensaje de la interrupción y el LED cambiará, sin esperar al sleep del bucle principal.

Reglas para escribir una ISR

Las interrupciones son poderosas, pero conllevan una gran responsabilidad. Cuando estás dentro de una ISR, el resto del sistema está “congelado”. Por tanto, hay reglas estrictas:

  1. Sé rápido: La función manejadora debe durar microsegundos. Cambiar una variable, activar un flag o encender un LED. Nunca pongas un time.sleep() dentro de una interrupción.
  2. Cuidado con la memoria: En muchas implementaciones de MicroPython, no puedes reservar memoria dentro de una interrupción. Esto significa que no debes crear listas nuevas, ni objetos complejos. Si intentas hacer algo que consuma memoria dinámica (heap), obtendrás un error.
  3. Evita print: Aunque lo hemos usado en el ejemplo, print usa buffer y puede bloquearse o corromper datos si ocurre una interrupción mientras el programa principal también estaba imprimiendo. Úsalo solo para depurar.

Para depurar interrupciones, es muy útil ejecutar este comando al inicio del script: micropython.alloc_emergency_exception_buf(100). Esto reserva memoria para poder mostrar los mensajes de error si algo falla dentro de la ISR.

micropython.schedule: la forma segura

Si necesitas hacer algo “pesado” cuando ocurre una interrupción (como imprimir un log largo, escribir en un archivo o cálculos complejos), no lo hagas en la ISR.

La forma correcta es usar micropython.schedule. Esto programa una función para que se ejecute “lo antes posible”, pero fuera del contexto estricto de la interrupción, evitando los problemas de memoria.

import machine
import micropython

# Función ligera que se ejecuta "después"
def tarea_pesada(arg):
    print("Procesando datos complejos...", arg)

# ISR: Solo programa la tarea
def interrupcion_rapida(pin):
    # Programamos la tarea pesada para que se ejecute en cuanto la CPU pueda
    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!

El problema de los rebotes (Debounce)

Si pruebas el ejemplo del botón con un pulsador físico real, notarás algo raro: a veces, una sola pulsación dispara la interrupción varias veces.

Esto se debe a que los contactos metálicos del botón “rebotan” físicamente antes de estabilizarse, generando una ráfaga de señales Rising y Falling en cuestión de milisegundos.

Para solucionarlo dentro de una interrupción, necesitamos filtrar por tiempo:

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()
    
    # Si han pasado menos de 200ms desde la última vez, ignoramos
    if time.ticks_diff(tiempo_actual, ultimo_tiempo) > 200:
        led.toggle()
        print("Pulsación válida")
        ultimo_tiempo = tiempo_actual

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

Secciones críticas

A veces, en nuestro código principal, estamos haciendo algo tan delicado (como comunicarse con un sensor con tiempos muy estrictos tipo DHT11 o NeoPixel) que no queremos ser interrumpidos.

Para ello podemos deshabilitar temporalmente las interrupciones:

import machine

# Desactivar interrupciones (zona crítica)
estado = machine.disable_irq()

# ... código sensible ...

# Restaurar interrupciones al estado anterior
machine.enable_irq(estado)
Copied!

Las interrupciones son fundamentales para sistemas reactivos y de bajo consumo. Nos permiten responder al instante a eventos externos. Sin embargo, recuerda siempre: entra, haz lo mínimo indispensable, y sal. Si sigues esa regla, tus programas volarán.