micropython-timers-hardware-machine-timer

Timers en MicroPython para ejecutar tareas periódicas

  • 5 min

Un timer es un mecanismo que ejecuta una función al vencer un intervalo de tiempo. Según el port de MicroPython, puede estar respaldado por hardware o gestionarse como un timer virtual.

Hasta ahora hemos aprendido a gestionar el tiempo de dos formas:

  1. time.sleep(): “Échate una siesta y no hagas nada más”. (Bloqueante).
  2. uasyncio: “Trabaja un poco, para, y deja que otro trabaje”. (Cooperativo).

¿Qué pasa si necesitamos ejecutar una tarea con una cadencia más regular que la obtenida mediante sleep()? ¿Y si queremos que un LED parpadee cada 100 ms mientras el programa principal sigue trabajando?

Para eso existen los timers.

Un timer de hardware usa un contador interno del microcontrolador. Cuando alcanza el valor configurado, genera un evento que termina ejecutando nuestro callback. El instante exacto en el que corre el código depende del port y del tipo de interrupción, así que no debemos confundir una cadencia estable con tiempo real estricto.

La clase machine.Timer

En MicroPython, controlar estos periféricos es muy sencillo gracias a la clase Timer.

Necesitamos definir tres cosas:

  1. Periodo (o Frecuencia): Cada cuánto tiempo debe saltar la alarma.
  2. Modo: ¿Debe sonar una sola vez (One Shot) o repetirse eternamente (Periodic)?
  3. Callback: ¿Qué función debe ejecutarse cuando suene la alarma?

Ejemplo: metrónomo con un timer periódico

Vamos a hacer parpadear un LED exactamente a 5 Hz (5 veces por segundo) usando un Timer, dejando el bucle principal (while True) totalmente libre.

from machine import Timer, Pin
import time

# 1. Definimos el hardware
led = Pin(2, Pin.OUT)

# 2. Definimos la función Callback (lo que hará el Timer)
# IMPORTANTE: La función debe aceptar un argumento (el propio timer)
def parpadear(timer):
    led.toggle()

# 3. Inicializamos el Timer
# Este ejemplo usa el timer 0 de un ESP32
tim = Timer(0)

# Configuramos: Frecuencia 5Hz, Modo Periódico
tim.init(freq=5, mode=Timer.PERIODIC, callback=parpadear)

print("Timer iniciado. El LED parpadea solo.")

# 4. Bucle principal
# Fíjate que aquí no hacemos NADA con el LED.
# Podríamos estar haciendo cálculos complejos y el LED seguiría a su ritmo.
contador = 0
while True:
    print(f"El programa principal sigue a lo suyo... {contador}")
    contador += 1
    time.sleep(1)
Copied!

Si ejecutas esto, verás el LED parpadeando frenéticamente mientras la consola imprime mensajes lentamente cada segundo. Son dos procesos independientes.

Ejemplo: cuenta atrás con ONE_SHOT

A veces no queremos algo repetitivo, sino algo diferido. “Enciende la bomba de agua y apágala automáticamente dentro de 10 segundos”.

Para esto usamos el modo Timer.ONE_SHOT.

from machine import Timer, Pin

rele = Pin(15, Pin.OUT)
tim_apagado = Timer(1) # Usamos otro ID de timer

def apagar_sistema(timer):
    rele.value(0)
    print("Sistema apagado por seguridad.")

# Encendemos
rele.value(1)
print("Encendiendo sistema... se apagará en 5 segundos.")

# Configuramos el timer para que salte una sola vez pasados 5000ms
tim_apagado.init(period=5000, mode=Timer.ONE_SHOT, callback=apagar_sistema)
Copied!

Diferencias entre placas (ESP32 y Pico)

Aquí MicroPython tiene algunas diferencias dependiendo del chip:

  • ESP32: Usa timers de hardware con identificadores desde 0. La cantidad disponible depende del modelo: puede haber uno, dos o cuatro. El port ESP32 ejecuta sus callbacks como interrupciones programadas (soft IRQ) y no admite timers virtuales.
  • Raspberry Pi Pico (RP2040): El port RP2 solo ofrece timers virtuales. Se inicializan con Timer(-1) o, en las versiones que permiten omitir el identificador, con Timer().
# Timer virtual para el port RP2
tim = Timer(-1)
tim.init(period=1000, mode=Timer.PERIODIC, callback=mi_funcion)
Copied!

Reglas de las ISR

El callback de un timer puede ejecutarse como interrupción dura o programada, según el port. En una interrupción dura no se puede reservar memoria; una programada permite hacerlo, pero introduce algo más de latencia y puede retrasarse por el recolector de basura.

Como el mismo código puede terminar en placas diferentes, conviene escribir callbacks breves y con pocas dependencias:

  1. Rapidez: La función debe ser muy breve. Togglear un LED, cambiar una variable global… y salir. Nada de time.sleep dentro.
  2. Memoria: En muchas placas, no puedes reservar memoria dentro del callback.
  • Evita crear listas, diccionarios o strings nuevos.
  • Evita usar print() si es posible (usa buffer), aunque en ESP32 suele funcionar por ser potente.
  • Si hay un error dentro del Timer, a veces el programa no dice nada y simplemente se para.

Si necesitas hacer algo complejo (como enviar un mensaje MQTT) cuando salte el Timer, no lo hagas en el callback. Usa el callback para levantar una “bandera” (variable global) y que el bucle principal (asyncio o while) procese esa tarea pesada.

Desactivar un timer

Si quieres detener esa locura de parpadeo, simplemente llama a deinit().

tim.deinit()
Copied!

¿Timer o uasyncio?

Es la gran duda. Ambos parecen hacer cosas a la vez.

  • Usa uasyncio para la lógica general del programa: leer sensores, esperar respuestas web, controlar flujos complejos. Es más fácil de depurar y no tiene las restricciones de memoria de las interrupciones.
  • Usa Timer cuando necesites una frecuencia estricta y garantizada (muestreo de audio, generación de señales, refresco de pantallas multiplexadas) o para tareas de seguridad tipo “Watchdog” que deben ocurrir aunque el programa principal se cuelgue.