Pular para o conteúdo

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 aqui

Rode e faça três testes, segurando o botão com o mouse:

  1. 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;
  2. segure por três segundos: aparecem três apertos, um por segundo. O programa não sabe que é o mesmo aperto;
  3. troque o sleep(1) por sleep(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?

FastTeacherPara professores

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
Conhecer o FastTeacher

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 segundo

Se 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ó.

Duas linhas do tempo comparadas. Na de cima, com sleep, o programa passa quase todo o tempo parado dentro do sleep e um aperto rápido do botão cai num trecho em que ninguém olha. Na de baixo, com ticks, o laço dá voltas curtas a cada 10 milissegundos, o LED troca na hora marcada e o aperto é visto na volta seguinte.
Com 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.

Captura de tela do PyPlaca com a lição de várias tarefas: o código com dois blocos de ticks_diff no editor, o circuito com dois LEDs na protoboard e a aba Pinos mostrando os GPIOs 18 e 19 com ritmos diferentes.
Dois LEDs em ritmos independentes: a aba Pinos mostra o GPIO18 trocando a cada 250 ms e o GPIO19 a cada 1 s.

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 terminar

Os argumentos do init:

  • period: o intervalo em milissegundos (também existe freq, em hertz);
  • mode: Timer.PERIODIC repete para sempre; Timer.ONE_SHOT dispara 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_diffmachine.Timer
Quem decide a horao laço perguntaa placa chama
Precisãodepende da pausa do laço (~10 ms)o alarme acontece na hora; o callback roda no próximo ponto seguro
Quantas tarefasquantas quiser4 de hardware (mais os virtuais)
Trabalho pesadodireto no laçosó no laço, guiado por bandeiras
Depurarfácil: tudo é sequencialmais 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.

FastMindAssistente de IA local

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
Baixar grátis para Windows

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.
Diagrama de máquina de estados com três círculos: APAGADO, ACESO e PISCANDO. Setas rotuladas aperto levam de APAGADO para ACESO, de ACESO para PISCANDO e de PISCANDO de volta para APAGADO. No estado PISCANDO, uma seta circular indica a troca do LED a cada 200 ms.
A máquina de estados do exemplo: três estados, um evento (o aperto) e as transições entre eles. O PISCANDO tem tempo marcado, sem 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:

  1. ler as entradas uma vez e descobrir os eventos (apertou);
  2. fazer o que o estado atual manda — um bloco if/elif por estado;
  3. trocar de estado quando um evento pede, anotando a hora se o novo estado tiver tempo marcado;
  4. 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 = agora

Com 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. Use ticks_diff;
  • esquecer de atualizar o ultimo: o if fica verdadeiro em todas as voltas e a tarefa roda sem parar;
  • uma tarefa com sleep escondido: uma função "inocente" que espera um segundo trava todas as outras. Procure sleep e laços while de 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.irq com 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

Fichas de componentes

FastMindAssistente de IA local

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
Baixar grátis para Windows

Plano Essencial grátis para sempre

FastTeacherPara professores

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
Conhecer o FastTeacher

Versão Community grátis · Windows

FastMindAssistente de IA local

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
Baixar grátis para Windows

Plano Essencial grátis para sempre