Tiempo sin trabarse
Programar sin colgarse: ticks, Timer y máquina de estados
Por qué el sleep cuelga la ESP32 y cómo hacer varias tareas a la vez en MicroPython con ticks_ms y ticks_diff, machine.Timer y máquinas de estados.
Por Prof. Rodrigo Faustino · Actualizado el · 12 min de lectura
Métricas de este artículo
- Palabras
- 2298
- Caracteres
- 13.212
- Tiempo de lectura
- ~12 min
Los primeros programas de placa suelen tener la misma forma: hace algo, sleep, hace otra cosa, sleep. Funciona muy bien mientras el programa hace una sola cosa. El día en que tiene que hacer parpadear un LED, actualizar una pantalla cada dos segundos, leer un sensor y además atender un botón, el sleep se convierte en el mayor enemigo: mientras duerme, la ESP32 no hace nada más. El botón pulsado durante el sueño simplemente no existió.
Este artículo muestra tres herramientas para escribir programas que no se cuelgan: el reloj de la placa con ticks_ms y ticks_diff, el machine.Timer, que llama a una función a la hora justa, y la máquina de estados, la manera de organizar el programa cuando crece. Ninguna exige bibliotecas: todo viene en el MicroPython de la ESP32 y funciona igual en el simulador.
El problema: el sleep es sordo
Empieza por un programa ingenuo: hacer parpadear un LED cada segundo y, en cada vuelta, contar las pulsaciones de un botón.
from machine import Pin
import time
led = Pin(18, Pin.OUT)
botao = Pin(4, Pin.IN, Pin.PULL_UP)
apertos = 0
for volta in range(15):
if botao.value() == 0:
apertos += 1
print('pulsación', apertos)
led.value(not led.value())
time.sleep(1) # el programa se queda PARADO aquíEjecútalo y haz tres pruebas, manteniendo pulsado el botón con el ratón:
- un toque rápido, de menos de un segundo: la mayoría de las veces no aparece nada. El programa estaba dentro del
sleep(1)y el botón se soltó antes de que volviera a mirar; - mantenlo pulsado tres segundos: aparecen tres pulsaciones, una por segundo. El programa no sabe que es la misma pulsación;
- cambia el
sleep(1)porsleep(5): el botón queda prácticamente sordo.
El Pin.irq, visto en el artículo sobre GPIO, resuelve el caso específico del botón: el pin avisa solo. Pero no resuelve el problema general: cómo hacer varias cosas en el tiempo, cada una con su ritmo, sin que la espera de una cuelgue a las otras.
La IA que recuerda tu vida y vive en tu ordenador
- Memoria a largo plazo con la fuente de cada respuesta
- Funciona sin conexión: tus datos se quedan en tu PC
- Lee PDF, diapositivas, fotos y grabaciones de reuniones
Plan Esencial gratis para siempre
El reloj: ticks_ms y ticks_diff
Piensa en quien cocina arroz y fríe un huevo al mismo tiempo. Nadie se queda veinte minutos mirando la olla del arroz: la persona mira el reloj de vez en cuando ("¿ya llegó la hora del arroz?") y, mientras no llega, atiende el huevo.
En el programa, la idea es idéntica. En vez de dormir, cada vuelta del bucle le pregunta al reloj si ya pasó el tiempo:
from machine import Pin
import time
led = Pin(18, Pin.OUT)
botao = Pin(4, Pin.IN, Pin.PULL_UP)
INTERVALO = 500 # ms entre un cambio del LED y el siguiente
ultimo = time.ticks_ms() # cuándo cambió el LED por última vez
anterior = 1 # lectura anterior del botón (1 = suelto)
while True:
agora = time.ticks_ms()
# tarea 1: parpadear el LED, sin esperar
if time.ticks_diff(agora, ultimo) >= INTERVALO:
led.value(not led.value())
ultimo = time.ticks_add(ultimo, INTERVALO)
# tarea 2: atender el botón (solo el flanco: suelto -> pulsado)
atual = botao.value()
if anterior == 1 and atual == 0:
print('pulsó en', agora, 'ms')
anterior = atual
time.sleep_ms(10) # pausa corta: unas 100 vueltas por segundoSi todavía no llegó la hora, el if es falso y el bucle sigue al instante con la tarea siguiente. Ninguna línea se queda parada mucho tiempo: el botón se lee cada 10 ms, sin perder toques. Y el botón ahora cuenta pulsaciones, no "vueltas con el botón pulsado": comparando la lectura actual con la anterior, el programa detecta el flanco (el paso de 1 a 0), y mantener pulsado el botón cuenta una sola vez.
sleep, el programa pasa el tiempo parado y pierde la pulsación rápida; con ticks_ms/ticks_diff, el bucle gira en vueltas cortas y cada tarea actúa a su hora.Por qué ticks_diff y no una resta
time.ticks_ms() cuenta milisegundos desde que la placa se encendió, pero el contador da la vuelta: después de 2^30 ms (poco más de doce días con la placa encendida) vuelve a empezar desde cero. Si el programa hiciera agora - ultimo justo en ese momento, el resultado sería un número negativo enorme, el if nunca volvería a ser verdadero y el LED dejaría de parpadear. Un bug que solo aparece después de doce días es de los peores que existen.
ticks_diff(fim, inicio) hace la cuenta correcta incluso en la vuelta del contador. La regla es absoluta: nunca compares ni restes ticks directamente. Usa ticks_diff para saber cuánto tiempo pasó y ticks_add para calcular una hora futura.
Por qué ticks_add(ultimo, INTERVALO) y no ultimo = agora
El bucle mira el reloj cada 10 ms, más o menos. Entonces el cambio del LED ocurre siempre un poquito después de la hora justa. Si el programa hiciera ultimo = agora, ese retraso entraría en la cuenta y se acumularía: cada ciclo quedaría unos milisegundos más largo, y el parpadeo se iría "deslizando". Con ultimo = ticks_add(ultimo, INTERVALO), la próxima hora se marca a partir de la hora justa anterior, y el ritmo medio queda exacto. Es la diferencia entre un reloj que se atrasa y uno que no.
¿Y la pausa corta del final?
El sleep_ms(10) al final del bucle no cuelga nada: solo limita la frecuencia de las vueltas. Sin él, el bucle giraría sin parar solo para descubrir que "todavía no llegó la hora", gastando energía en vano. Con él, el peor retraso para percibir cualquier cosa queda en torno a 10 ms, imperceptible para un botón. El sleep no está prohibido: solo no puede ser largo.
Varias tareas, cada una con su reloj
Lo que hace poderosa esta técnica es que escala. Cada tarea tiene su propia variable de "última vez" y su propio intervalo, y las tareas no se conocen entre sí:
from machine import Pin
import time
vermelho = Pin(18, Pin.OUT)
verde = Pin(19, Pin.OUT)
ultimo_vermelho = time.ticks_ms()
ultimo_verde = time.ticks_ms()
while True:
agora = time.ticks_ms()
if time.ticks_diff(agora, ultimo_vermelho) >= 250:
vermelho.value(not vermelho.value())
ultimo_vermelho = time.ticks_add(ultimo_vermelho, 250)
if time.ticks_diff(agora, ultimo_verde) >= 1000:
verde.value(not verde.value())
ultimo_verde = time.ticks_add(ultimo_verde, 1000)
time.sleep_ms(5)Con sleep, habría que calcular la línea de tiempo entera ("duerme 250, cambia el rojo, duerme 250, cambia el rojo… ahora cambia los dos") y rehacerlo todo con cada cambio de ritmo. Aquí, cambiar 250 por 300 o 1000 por 700 no exige tocar nada más. Añadir una tercera tarea (leer el DHT22 cada 2 s, redibujar el OLED cada 200 ms) es solo un bloque más igual.

Este estilo se llama multitarea cooperativa: cada tarea hace su parte rápido y devuelve el turno. La regla que sostiene todo es que ninguna tarea puede colgarse: nada de sleep largo, nada de bucles esperando que algo ocurra. Una tarea lenta retrasa a todas: un measure() del DHT22 tarda unos 272 ms, y durante ese tiempo ningún LED cambia. Por eso las tareas lentas deben ser raras y las rápidas, cortas.
machine.Timer: el reloj que te llama
Con ticks_diff, es el programa el que pregunta "¿ya llegó la hora?". Con un Timer, la lógica se invierte: tú dices cada cuánto tiempo y qué función llamar, y la ESP32 llama sola.
from machine import Pin, Timer
import time
led = Pin(2, Pin.OUT)
segundos = 0
mudou = False
def a_cada_segundo(t):
global segundos, mudou
led.value(not led.value()) # cosa rápida: cambia el LED
segundos += 1
mudou = True # el resto queda para el programa principal
relogio = Timer(0)
relogio.init(period=1000, mode=Timer.PERIODIC, callback=a_cada_segundo)
while True: # mantiene vivo el programa
if mudou:
mudou = False
print('pasaron', segundos, 's')
if segundos >= 10:
break
time.sleep_ms(10)
relogio.deinit() # apaga el Timer antes de terminarLos argumentos de init:
period: el intervalo en milisegundos (también existefreq, en hercios);mode:Timer.PERIODICse repite para siempre;Timer.ONE_SHOTse dispara una sola vez, como un despertador;callback: la función, que recibe el propio Timer como argumento.
En el MicroPython de la ESP32, estos argumentos van siempre con nombre. Timer(0, 1000) no funciona: da TypeError: extra positional arguments given. Y deinit() apaga el Timer.
Qué números de Timer existen
La ESP32 tiene 4 timers de hardware: Timer(0) a Timer(3), y cada número devuelve siempre el mismo objeto. Timer(4) da ValueError: Timer(4) doesn't exist, there are only 4 hardware timers. Un número negativo, como Timer(-1), crea un timer virtual, hecho por software en el sistema de la ESP32; cada llamada crea uno nuevo, y no hay límite de cuatro. Para la mayoría de los proyectos de clase, los cuatro de hardware sobran.
Una curiosidad de la v1.29.0 que el simulador reproduce: el print de un Timer periódico muestra mode=ONE_SHOT, porque el código C de la versión intercambia los dos nombres al imprimir. El Timer funciona bien; solo el texto sale cambiado. Es el tipo de detalle que muestra por qué el simulador copia el comportamiento de la placa en vez de "hacer lo que parece correcto".
Callback corto: por qué
El callback del Timer se atiende igual que el de Pin.irq: el aviso entra en una cola y la función se ejecuta poco después, entre una instrucción y otra del programa principal o durante un sleep. Mientras el callback se ejecuta, el programa principal espera. Y la cola tiene 8 lugares: si los avisos llegan más rápido de lo que terminan los callbacks, la cola se llena y los avisos se pierden: tu reloj "salta" segundos sin que lo sepas.
De ahí la regla de oro: en el callback, solo lo que es rápido: cambiar un pin, sumar a un contador, levantar una bandera (mudou = True). Lo que es lento (print, dibujar en el OLED, leer el DHT22, publicar en MQTT) queda en el bucle principal, que mira la bandera. Como la función cambia variables de fuera, necesita el global. El MicroPython de la ESP32 ni siquiera acepta timers "duros" (hard=True da ValueError: hard Timers are not implemented): todo callback es de este tipo agendado.
Mantén vivo el programa
En la placa real, un Timer de hardware puede seguir llamando al callback después de que el main.py termine, mientras la placa está en el REPL (es un comportamiento que todavía se va a comprobar en una placa del laboratorio). En el simulador, no: los callbacks solo se ejecutan mientras el programa corre. Por eso los ejemplos mantienen un bucle vivo al final. Si el programa acaba con un Timer encendido, la pestaña Avisos muestra timer-apos-fim, recordando que el comportamiento depende de la placa.
¿Timer o ticks?
Las dos técnicas resuelven el mismo problema, y la elección es de estilo y de precisión:
ticks_ms/ticks_diff | machine.Timer | |
|---|---|---|
| Quién decide la hora | el bucle pregunta | la placa llama |
| Precisión | depende de la pausa del bucle (~10 ms) | la alarma ocurre a la hora; el callback corre en el próximo punto seguro |
| Cuántas tareas | las que quieras | 4 de hardware (más los virtuales) |
| Trabajo pesado | directo en el bucle | solo en el bucle, guiado por banderas |
| Depurar | fácil: todo es secuencial | más difícil: la función corre "desde fuera" |
Para empezar, ticks es más simple de entender y depurar. El Timer brilla cuando la hora tiene que ser exacta y el bucle principal tiene un trabajo variable.
Del programa al semestre planificado
- Plan de enseñanza y calendario montados solos
- Diario de clases listo para pegar en el SIGA
- Diapositivas y listas de ejercicios con solucionario
Versión Community gratis · Windows
Máquina de estados: organizar el programa
Un semáforo, una lavadora, un ascensor: todos pasan el tiempo en un estado (verde, enjuagando, subiendo) y cambian de estado cuando ocurre algo (se acabó el tiempo, se cerró la puerta, alguien pulsó el botón). Esta manera de pensar se llama máquina de estados, y es lo que mantiene organizados los programas de placa cuando crecen.
Son tres ingredientes:
- estados: lo que el sistema está haciendo ahora. Una variable guarda el estado actual;
- eventos: lo que puede ocurrir: una pulsación, un tiempo que pasó, un sensor que cambió;
- transiciones: en cada estado, qué evento lleva a qué estado.
sleep.El ejemplo siguiente tiene tres estados. Cada pulsación del botón lleva al siguiente: el LED apagado, encendido, parpadeando y apagado otra vez.
from machine import Pin
import time
led = Pin(18, Pin.OUT)
botao = Pin(4, Pin.IN, Pin.PULL_UP)
estado = 'APAGADO'
anterior = 1
ultimo = time.ticks_ms()
print(estado)
while True:
agora = time.ticks_ms()
atual = botao.value()
apertou = anterior == 1 and atual == 0 # el evento "pulsación"
anterior = atual
if estado == 'APAGADO':
led.off()
if apertou:
estado = 'ENCENDIDO'
print(estado)
elif estado == 'ENCENDIDO':
led.on()
if apertou:
estado = 'PARPADEANDO'
ultimo = agora
print(estado)
elif estado == 'PARPADEANDO':
if time.ticks_diff(agora, ultimo) >= 200:
led.value(not led.value())
ultimo = time.ticks_add(ultimo, 200)
if apertou:
estado = 'APAGADO'
print(estado)
time.sleep_ms(10)El bucle tiene siempre la misma forma, y es ella la que vale la pena aprender:
- leer las entradas una vez y descubrir los eventos (
apertou); - hacer lo que manda el estado actual: un bloque
if/elifpor estado; - cambiar de estado cuando un evento lo pide, anotando la hora si el nuevo estado tiene tiempo marcado;
- pausa corta y vuelta.
Ningún estado espera parado. El PARPADEANDO parpadea con ticks_diff, así que el botón se sigue leyendo cada 10 ms y la pulsación que lo apaga se atiende al instante.
Estados con tiempo marcado
Muchos estados duran un tiempo fijo: el amarillo del semáforo, el enjuague de la lavadora. El patrón es anotar cuándo empezó el estado y comprobarlo en cada vuelta:
if estado == 'AMARILLO':
if time.ticks_diff(agora, inicio) >= 1000: # 1 s en amarillo
estado = 'ROJO'
inicio = agoraCon eso, un semáforo completo con botón de peatón (que tiene que atender el botón en cualquier momento, incluso en medio del verde) se convierte en un programa corto y legible, sin un solo sleep largo.
Por qué estados en texto
Los estados del ejemplo son textos ('APAGADO', 'ENCENDIDO'). Los números ocuparían menos memoria, pero los textos dejan el print y la pestaña Variables del simulador autoexplicativos: se puede pausar el programa y ver en qué estado está. Si la memoria aprieta en un proyecto grande, MicroPython ofrece const() en el módulo micropython para crear constantes enteras con nombre.
Errores comunes al abandonar el sleep
El cambio del sleep por el reloj suele tropezar siempre en los mismos puntos:
- comparar ticks con
>o restar: funciona durante doce días y falla después. Usaticks_diff; - olvidar actualizar el
ultimo: elifqueda verdadero en todas las vueltas y la tarea corre sin parar; - una tarea con
sleepescondido: una función "inocente" que espera un segundo cuelga a todas las otras. Buscasleepy bucleswhilede espera dentro de las tareas; - leer el botón en varios lugares: cada lectura puede dar un valor distinto y el flanco se pierde. Lee las entradas una vez al comienzo de la vuelta y usa la variable;
- trabajo pesado en el callback del Timer: el callback se alarga, la cola de 8 se llena y los eventos desaparecen. Levanta una bandera y haz el trabajo en el bucle;
- programa que termina con el Timer encendido: en el simulador, los callbacks se detienen; el aviso timer-apos-fim lo recuerda.
La pestaña Pines del simulador es la mejor aliada en esta transición: muestra, en escala de tiempo, cuándo cambió cada pin, y se puede medir en la pantalla si el LED cambia de verdad cada 250 ms y si el ritmo se desliza con el tiempo.
Juntándolo todo
Las tres herramientas se combinan. Un proyecto típico de fin de curso, un termómetro con display, alarma y botón, queda así:
- una máquina de estados decide el modo (mostrando, alarma, configurando);
- cada tarea periódica (leer el sensor cada 2 s, redibujar la pantalla cada 500 ms, parpadear la alarma) tiene su bloque de
ticks_diff; - si una tarea necesita hora exacta, un Timer levanta una bandera que el bucle atiende;
- los eventos de flanco (pulsaciones, sensor de presencia) vienen de la comparación con la lectura anterior o de un
Pin.irqcon debounce.
Lo que ninguno de estos programas tiene es un sleep largo en medio. Ese es el cambio de mentalidad que separa los primeros ejemplos de los proyectos de verdad.
En la ruta Tiempo sin trabarse, cada parte de este artículo se convierte en una lección con circuito listo, y los desafíos cobran exactamente eso: parpadear y atender el botón al mismo tiempo, dos ritmos independientes, un reloj con Timer y el semáforo con peatón. El último artículo de la serie, sobre Wi-Fi, HTTP y MQTT, muestra por qué esta forma de programar es todavía más importante cuando la placa conversa por la red.
Para practicar en PyPlaca
Rutas
Lecciones
- Por qué el sleep() traba el programa
- ¿Ya es la hora? ticks_ms y ticks_diff
- Varias tareas, cada una con su reloj
- machine.Timer: el reloj que te llama
- Máquina de estados