Tempo sem travar
Programar sem travar: ticks, Timer e máquina de estados
Por que o sleep trava a ESP32 e como fazer várias tarefas ao mesmo tempo em MicroPython com ticks_ms e ticks_diff, machine.Timer e máquinas de estados.
Por Prof. Rodrigo Faustino · Atualizado em · 12 min de leitura
Os primeiros programas de placa costumam ter a mesma forma: faz alguma coisa, sleep, faz outra, sleep. Funciona muito bem enquanto o programa faz uma coisa só. No dia em que ele precisa piscar um LED, atualizar um display a cada dois segundos, ler um sensor e ainda atender um botão, o sleep vira o maior inimigo: enquanto dorme, a ESP32 não faz mais nada. O botão apertado durante o sono simplesmente não existiu.
Este artigo mostra três ferramentas para escrever programas que não travam: o relógio da placa com ticks_ms e ticks_diff, o machine.Timer, que chama uma função na hora certa, e a máquina de estados, o jeito de organizar o programa quando ele cresce. Nenhuma delas exige bibliotecas: tudo vem no MicroPython da ESP32 e funciona igual no simulador.
O problema: o sleep é surdo
Comece por um programa ingênuo: piscar um LED a cada segundo e, a cada volta, contar os apertos de um botão.
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('aperto', apertos)
led.value(not led.value())
time.sleep(1) # o programa fica PARADO aquiRode e faça três testes, segurando o botão com o mouse:
- um toque rápido, de menos de um segundo: na maioria das vezes nada aparece. O programa estava dentro do
sleep(1)e o botão foi solto antes de ele olhar de novo; - segure por três segundos: aparecem três apertos, um por segundo. O programa não sabe que é o mesmo aperto;
- troque o
sleep(1)porsleep(5): o botão fica praticamente surdo.
O Pin.irq, visto no artigo sobre GPIO, resolve o caso específico do botão — o pino avisa sozinho. Mas ele não resolve o problema geral: como fazer várias coisas no tempo, cada uma com o seu ritmo, sem que a espera de uma trave as outras?
Da ementa ao semestre planejado
- Plano de ensino e calendário montados sozinhos
- Diário de aulas pronto para colar no SIGA
- Slides e listas de exercícios com gabarito
Versão Community grátis · Windows
O relógio: ticks_ms e ticks_diff
Pense em quem cozinha arroz e frita um ovo ao mesmo tempo. Ninguém fica vinte minutos olhando a panela do arroz: a pessoa olha o relógio de vez em quando — "já deu a hora do arroz?" — e, enquanto não deu, cuida do ovo.
No programa, a ideia é idêntica. Em vez de dormir, cada volta do laço pergunta ao relógio se já passou o tempo:
from machine import Pin
import time
led = Pin(18, Pin.OUT)
botao = Pin(4, Pin.IN, Pin.PULL_UP)
INTERVALO = 500 # ms entre uma troca do LED e a próxima
ultimo = time.ticks_ms() # quando o LED trocou pela última vez
anterior = 1 # leitura anterior do botão (1 = solto)
while True:
agora = time.ticks_ms()
# tarefa 1: piscar o LED, sem esperar
if time.ticks_diff(agora, ultimo) >= INTERVALO:
led.value(not led.value())
ultimo = time.ticks_add(ultimo, INTERVALO)
# tarefa 2: atender o botão (só a borda: solto -> apertado)
atual = botao.value()
if anterior == 1 and atual == 0:
print('apertou em', agora, 'ms')
anterior = atual
time.sleep_ms(10) # pausa curta: umas 100 voltas por segundoSe ainda não deu a hora, o if é falso e o laço segue na hora para a próxima tarefa. Nenhuma linha fica parada por muito tempo: o botão é lido a cada 10 ms, sem perder toques. E o botão agora conta apertos, não "voltas com o botão apertado": comparando a leitura atual com a anterior, o programa detecta a borda — a passagem de 1 para 0 —, e segurar o botão conta uma vez só.
sleep, o programa passa o tempo parado e perde o toque rápido; com ticks_ms/ticks_diff, o laço gira em voltas curtas e cada tarefa age na sua hora.Por que ticks_diff e não uma subtração
time.ticks_ms() conta milissegundos desde que a placa ligou, mas o contador dá a volta: depois de 2^30 ms — pouco mais de doze dias com a placa ligada — ele recomeça do zero. Se o programa fizesse agora - ultimo bem nessa hora, o resultado seria um número negativo enorme, o if nunca mais seria verdadeiro e o LED pararia de piscar. Um bug que só aparece depois de doze dias é dos piores que existem.
ticks_diff(fim, inicio) faz a conta certa mesmo na volta do contador. A regra é absoluta: nunca compare nem subtraia ticks diretamente. Use ticks_diff para saber quanto tempo passou e ticks_add para calcular uma hora futura.
Por que ticks_add(ultimo, INTERVALO) e não ultimo = agora
O laço olha o relógio a cada 10 ms, mais ou menos. Então a troca do LED acontece sempre um pouquinho depois da hora certa. Se o programa fizesse ultimo = agora, esse atraso entraria na conta e se acumularia: cada ciclo ficaria alguns milissegundos mais longo, e o pisca iria "escorregando". Com ultimo = ticks_add(ultimo, INTERVALO), a próxima hora é marcada a partir da hora certa anterior, e o ritmo médio fica exato. É a diferença entre um relógio que atrasa e um que não atrasa.
E a pausa curta do fim?
O sleep_ms(10) no fim do laço não trava nada: ele só limita a frequência das voltas. Sem ele, o laço giraria sem parar só para descobrir que "ainda não deu a hora", gastando energia à toa. Com ele, o pior atraso para perceber qualquer coisa fica em torno de 10 ms — imperceptível para um botão. O sleep não é proibido: ele só não pode ser longo.
Várias tarefas, cada uma com o seu relógio
O que torna essa técnica poderosa é que ela escala. Cada tarefa tem a sua própria variável de "última vez" e o seu próprio intervalo, e as tarefas não sabem uma da outra:
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)Com sleep, seria preciso calcular a linha do tempo inteira ("dorme 250, troca o vermelho, dorme 250, troca o vermelho… agora troca os dois") e refazer tudo a cada mudança de ritmo. Aqui, mudar 250 para 300 ou 1000 para 700 não exige mexer em mais nada. Acrescentar uma terceira tarefa — ler o DHT22 a cada 2 s, redesenhar o OLED a cada 200 ms — é só mais um bloco igual.

Esse estilo se chama multitarefa cooperativa: cada tarefa faz a sua parte rápido e devolve a vez. A regra que sustenta tudo é que nenhuma tarefa pode travar — nada de sleep longo, nada de laço esperando algo acontecer. Uma tarefa lenta atrasa todas: um measure() do DHT22 leva uns 272 ms, e durante esse tempo nenhum LED troca. Por isso tarefas lentas devem ser raras e as rápidas, curtas.
machine.Timer: o relógio que chama você
Com ticks_diff, é o programa que pergunta "já deu a hora?". Com um Timer, a lógica se inverte: você diz de quanto em quanto tempo e qual função chamar, e a ESP32 chama sozinha.
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()) # coisa rápida: troca o LED
segundos += 1
mudou = True # o resto fica para o programa principal
relogio = Timer(0)
relogio.init(period=1000, mode=Timer.PERIODIC, callback=a_cada_segundo)
while True: # mantém o programa vivo
if mudou:
mudou = False
print('passaram', segundos, 's')
if segundos >= 10:
break
time.sleep_ms(10)
relogio.deinit() # desliga o Timer antes de terminarOs argumentos do init:
period: o intervalo em milissegundos (também existefreq, em hertz);mode:Timer.PERIODICrepete para sempre;Timer.ONE_SHOTdispara uma vez, como um despertador;callback: a função, que recebe o próprio Timer como argumento.
No MicroPython da ESP32, esses argumentos vão sempre com nome. Timer(0, 1000) não funciona: dá TypeError: extra positional arguments given. E deinit() desliga o Timer.
Quais números de Timer existem
A ESP32 tem 4 timers de hardware: Timer(0) a Timer(3), e cada número devolve sempre o mesmo objeto. Timer(4) dá ValueError: Timer(4) doesn't exist, there are only 4 hardware timers. Um número negativo, como Timer(-1), cria um timer virtual, feito por software no sistema da ESP32; cada chamada cria um novo, e não há limite de quatro. Para a maioria dos projetos de aula, os quatro de hardware sobram.
Uma curiosidade da v1.29.0 que o simulador reproduz: o print de um Timer periódico mostra mode=ONE_SHOT, porque o código C da versão troca os dois nomes ao imprimir. O Timer funciona certo; só o texto sai trocado. É o tipo de detalhe que mostra por que o simulador copia o comportamento da placa em vez de "fazer o que parece certo".
Callback curto: por quê
O callback do Timer é atendido do mesmo jeito que o do Pin.irq: o aviso entra numa fila e a função roda logo depois, entre uma instrução e outra do programa principal ou durante um sleep. Enquanto o callback roda, o programa principal espera. E a fila tem 8 lugares: se os avisos chegam mais rápido do que os callbacks terminam, a fila enche e avisos se perdem — o seu relógio "pula" segundos sem você saber.
Daí a regra de ouro: no callback, só o que é rápido — trocar um pino, somar num contador, levantar uma bandeira (mudou = True). O que é demorado — print, desenhar no OLED, ler o DHT22, publicar no MQTT — fica no laço principal, que olha a bandeira. Como a função muda variáveis de fora, precisa do global. O MicroPython da ESP32 nem aceita timers "duros" (hard=True dá ValueError: hard Timers are not implemented): todo callback é desse tipo agendado.
Mantenha o programa vivo
Na placa real, um Timer de hardware pode continuar chamando o callback depois que o main.py termina, enquanto a placa está no REPL (é um comportamento que ainda vai ser conferido numa placa do laboratório). No simulador, não: os callbacks só rodam enquanto o programa roda. Por isso os exemplos mantêm um laço vivo no fim. Se o programa acabar com um Timer ligado, a aba Avisos mostra timer-apos-fim, lembrando que o comportamento depende da placa.
Timer ou ticks?
As duas técnicas resolvem o mesmo problema, e a escolha é de estilo e de precisão:
ticks_ms/ticks_diff | machine.Timer | |
|---|---|---|
| Quem decide a hora | o laço pergunta | a placa chama |
| Precisão | depende da pausa do laço (~10 ms) | o alarme acontece na hora; o callback roda no próximo ponto seguro |
| Quantas tarefas | quantas quiser | 4 de hardware (mais os virtuais) |
| Trabalho pesado | direto no laço | só no laço, guiado por bandeiras |
| Depurar | fácil: tudo é sequencial | mais difícil: a função roda "de fora" |
Para começar, ticks é mais simples de entender e depurar. O Timer brilha quando a hora precisa ser exata e o laço principal tem trabalho variável.
A IA que lembra da sua vida e mora no seu computador
- Memória de longo prazo com a fonte de cada resposta
- Funciona offline: seus dados ficam no seu PC
- Lê PDFs, slides, fotos e gravações de reunião
Plano Essencial grátis para sempre
Máquina de estados: organizando o programa
Um semáforo, uma máquina de lavar, um elevador: todos passam o tempo num estado (verde, enxaguando, subindo) e mudam de estado quando acontece alguma coisa (o tempo acabou, a porta fechou, alguém apertou o botão). Esse jeito de pensar se chama máquina de estados, e é o que mantém programas de placa organizados quando eles crescem.
São três ingredientes:
- estados: o que o sistema está fazendo agora. Uma variável guarda o estado atual;
- eventos: o que pode acontecer — um aperto, um tempo que passou, um sensor que mudou;
- transições: em cada estado, qual evento leva a qual estado.
sleep.O exemplo a seguir tem três estados. Cada aperto do botão leva ao próximo: o LED apagado, aceso, piscando e apagado de novo.
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 # o evento "aperto"
anterior = atual
if estado == 'APAGADO':
led.off()
if apertou:
estado = 'ACESO'
print(estado)
elif estado == 'ACESO':
led.on()
if apertou:
estado = 'PISCANDO'
ultimo = agora
print(estado)
elif estado == 'PISCANDO':
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)O laço tem sempre a mesma forma, e é ela que vale aprender:
- ler as entradas uma vez e descobrir os eventos (
apertou); - fazer o que o estado atual manda — um bloco
if/elifpor estado; - trocar de estado quando um evento pede, anotando a hora se o novo estado tiver tempo marcado;
- pausa curta e volta.
Nenhum estado espera parado. O PISCANDO pisca com ticks_diff, então o botão continua sendo lido a cada 10 ms e o aperto que desliga é atendido na hora.
Estados com tempo marcado
Muitos estados duram um tempo fixo: o amarelo do semáforo, o enxágue da máquina de lavar. O padrão é anotar quando o estado começou e conferir a cada volta:
if estado == 'AMARELO':
if time.ticks_diff(agora, inicio) >= 1000: # 1 s no amarelo
estado = 'VERMELHO'
inicio = agoraCom isso, um semáforo completo com botão de pedestre — que precisa atender o botão a qualquer momento, inclusive no meio do verde — vira um programa curto e legível, sem um único sleep longo.
Por que estados em texto
Os estados do exemplo são textos ('APAGADO', 'ACESO'). Números ocupariam menos memória, mas textos deixam o print e a aba Variáveis do simulador autoexplicativos: dá para pausar o programa e ver em que estado ele está. Se a memória apertar num projeto grande, o MicroPython oferece const() no módulo micropython para criar constantes inteiras com nome.
Erros comuns ao abandonar o sleep
A troca do sleep pelo relógio costuma tropeçar sempre nos mesmos pontos:
- comparar ticks com
>ou subtrair: funciona por doze dias e falha depois. Useticks_diff; - esquecer de atualizar o
ultimo: oiffica verdadeiro em todas as voltas e a tarefa roda sem parar; - uma tarefa com
sleepescondido: uma função "inocente" que espera um segundo trava todas as outras. Procuresleepe laçoswhilede espera dentro das tarefas; - ler o botão em vários lugares: cada leitura pode dar um valor diferente e a borda se perde. Leia as entradas uma vez no começo da volta e use a variável;
- trabalho pesado no callback do Timer: o callback fica longo, a fila de 8 enche e eventos somem. Levante uma bandeira e faça o trabalho no laço;
- programa que termina com o Timer ligado: no simulador, os callbacks param; o aviso timer-apos-fim lembra disso.
A aba Pinos do simulador é a melhor aliada nessa transição: ela mostra, em escala de tempo, quando cada pino mudou — dá para medir na tela se o LED troca mesmo a cada 250 ms e se o ritmo escorrega com o tempo.
Juntando tudo
As três ferramentas se combinam. Um projeto típico de fim de curso — um termômetro com display, alarme e botão — fica assim:
- uma máquina de estados decide o modo (mostrando, alarme, configurando);
- cada tarefa periódica (ler o sensor a cada 2 s, redesenhar a tela a cada 500 ms, piscar o alarme) tem o seu bloco de
ticks_diff; - se uma tarefa precisa de hora exata, um Timer levanta uma bandeira que o laço atende;
- os eventos de borda (apertos, sensor de presença) vêm da comparação com a leitura anterior ou de um
Pin.irqcom debounce.
O que nenhum desses programas tem é um sleep longo no meio. Essa é a mudança de mentalidade que separa os primeiros exemplos dos projetos de verdade.
Na trilha Tempo sem travar, cada parte deste artigo vira uma lição com circuito pronto, e os desafios cobram exatamente isso: piscar e atender o botão ao mesmo tempo, dois ritmos independentes, um relógio com Timer e o semáforo com pedestre. O último artigo da série, sobre Wi-Fi, HTTP e MQTT, mostra por que essa forma de programar é ainda mais importante quando a placa conversa pela rede.
Para praticar no PyPlaca
Trilhas
Lições
- Por que o sleep() trava o programa
- Já deu a hora? ticks_ms e ticks_diff
- Várias tarefas, cada uma com o seu relógio
- machine.Timer: o relógio que chama você
- Máquina de estados