Trilha de aprendizado · Nível 10 · Tutorial 3

Máquinas de estados no firmware

Implementar uma sequência de sinalização acionada por botão usando estados e transições temporizadas, sem interromper o atendimento das entradas.

  • Nível: Intermediário
  • Duração: 18 min
  • 8 passos
Máquinas de estados no firmware

O que você vai percorrer

  1. Definir o comportamento antes do código Planeje a sinalização como estados distintos antes de traduzi-la para o firmware. 2 min
  2. Separar estado, evento e marca de entrada Diferencie a memória da máquina, o estado estável do botão, o evento momentâneo e a marca temporal que acompanharão a sinalização. 2 min
  3. Traduzir a tabela para enum e switch Transforme os estados da tabela em nomes de código e organize a seleção com enum, switch, case, break e default. 2 min
  4. Executar a ação de entrada uma única vez Centralize os efeitos de uma mudança de estado para que sejam executados apenas na entrada. 2 min
  5. Avançar por tempo sem bloquear o loop Use a marca de entrada de cada estado para avançar pelos pulsos e pela pausa sem bloquear o loop. 2 min
  6. Definir a prioridade do cancelamento Defina a prioridade do pressionamento aceito sobre o vencimento do intervalo para garantir uma única transição por atualização da máquina. 2 min
  7. Integrar entrada, máquina e saída Integre o evento de botão tratado por rebote à máquina de estados e execute a sequência no Uno R3 próprio. 2 min
  8. Aplicar e verificar a sequência completa Verifique a sinalização no Uno R3, teste cancelamentos e confirme por rastreamento a prioridade entre evento e prazo. 4 min

O que você vai aprender

  • Converter uma tabela de comportamento em enum e switch com transições explícitas.
  • Executar uma ação de entrada no estado sem repeti-la indevidamente em todas as passagens.
  • Resolver de forma previsível a coincidência entre um acionamento e o término de um intervalo.
  • Verificar a sequência por pressionamento, permanência, soltura e cancelamento.

Antes de começar

  • Máquinas de estados para sequenciamento digital
  • Funções com parâmetros e retorno
  • Temporização não bloqueante com millis
  • Eventos de botão com tratamento de rebote

Passo 1 de 8

Definir o comportamento antes do código

Planeje a sinalização como estados distintos antes de traduzi-la para o firmware.

Visualize a sequência

Comportamento desejado

Um pressionamento aceito do botão inicia uma sinalização no LED integrado: ele acende por 1000 ms, apaga por 500 ms e acende novamente por 1000 ms. Ao fim, volta a ficar apagado.

Durante essa sequência, um novo pressionamento aceito cancela a sinalização e leva a aplicação de volta ao repouso. Manter o botão pressionado não cria comandos adicionais; a soltura também não muda diretamente a sequência.

Fluxo da sinalização

Diagrama com quatro etapas: LED apagado em repouso, primeiro pulso aceso, pausa apagada, segundo pulso aceso e retorno ao repouso; pressionamentos durante os estados ativos retornam ao repouso.

A sequência avança pelo tempo; um novo pressionamento durante uma etapa ativa a cancela.

Especifique cada etapa

Tabela de comportamento

A tabela é a especificação antes do código. Ela separa o que o LED mostra da situação em que a aplicação está.

Estados e transições

O botão produz um evento de pressionamento já validado pelo tratamento de rebote estudado anteriormente.
Estado atualLEDPermanece atéPróximo estado
REPOUSOApagadoPressionamento aceitoPRIMEIRO_PULSO
PRIMEIRO_PULSOAceso1000 ms ou pressionamentoPAUSA ou REPOUSO
PAUSAApagado500 ms ou pressionamentoSEGUNDO_PULSO ou REPOUSO
SEGUNDO_PULSOAceso1000 ms ou pressionamentoREPOUSO

Diferencie repouso e pausa

Mesma saída, significados diferentes

Observe que REPOUSO e PAUSA deixam o LED apagado, mas não são o mesmo estado.

  • Em REPOUSO, um pressionamento inicia o primeiro pulso.
  • Em PAUSA, um pressionamento cancela a sequência e retorna ao repouso.

Portanto, a saída sozinha não informa o comportamento seguinte. A aplicação precisa guardar em qual etapa está, mesmo quando duas etapas comandam a mesma saída.

Dica

Atenção

Não use o nível mantido do botão como se fosse vários comandos. Um pressionamento aceito é um acontecimento único: depois de ser usado para iniciar ou cancelar, continuar segurando o botão não reinicia nem cancela novamente.

Pratique a leitura da tabela

Confira o comportamento

Associe cada estado à condição que determina seu avanço normal ou sua resposta ao pressionamento.

Toque em um item e depois no par correspondente.

Passo 2 de 8

Separar estado, evento e marca de entrada

Diferencie a memória da máquina, o estado estável do botão, o evento momentâneo e a marca temporal que acompanharão a sinalização.

O que permanece no loop

Estado é memória da sequência

A cada passagem por loop(), a máquina precisa saber em que etapa da sinalização está. Essa informação é o estado da aplicação e permanece armazenada entre passagens.

Por exemplo, depois que um pressionamento inicia a sequência, o estado pode continuar como PRIMEIRO_PULSO durante muitas passagens por loop(). Ele não deve voltar automaticamente a REPOUSO no início de cada passagem.

Isso é diferente de ler o botão: a leitura é renovada frequentemente; o estado da máquina registra a etapa que ainda está em execução.

Linha do tempo com várias passagens pelo loop, um evento breve de botão, um estado que permanece e uma marca de tempo fixa no início do estado.

O estado e a marca de entrada permanecem; o evento existe somente na passagem em que é detectado.

Não confunda botão com máquina

Quatro dados, quatro responsabilidades

Para organizar a sequência, separe quatro informações que têm papéis diferentes:

  • Estado da máquina: etapa atual da sinalização, como REPOUSO ou PRIMEIRO_PULSO.
  • Evento de pressionamento: informa que um novo pressionamento válido foi aceito agora.
  • Marca de entrada: instante millis() registrado quando a máquina entrou no estado atual.
  • Estado estável do botão: informação do tratamento de rebote que diz se o botão continua pressionado ou solto.

O estado estável do botão pode permanecer “pressionado” por bastante tempo. Já o evento de pressionamento deve ser breve: ele ocorre uma vez na transição estável de solto para pressionado.

Compare a duração e a função de cada informação.

Dados consultados repetidamente pela aplicação.
InformaçãoDuração típicaPara que serve
Estado da máquinaVárias passagensIndicar a etapa atual
Evento de pressionamentoUma passagemPedir uma ação uma vez
Marca de entradaEnquanto o estado durarReferenciar a entrada no estado
Estado estável do botãoEnquanto houver pressãoRepresentar o botão aceito

Evite comandos repetidos

Um toque, um evento

Imagine que o botão já foi aceito pelo tratamento de rebote e permanece pressionado.

Na passagem em que foi aceito, eventoPressionou vale true e a máquina pode iniciar a sequência. Nas passagens seguintes, enquanto o botão continua pressionado, o estado estável ainda indica “pressionado”, mas eventoPressionou deve voltar a false.

Assim, o mesmo toque não vira vários comandos. A máquina permanece na etapa que iniciou até ocorrer uma transição prevista. A soltura, por si só, não muda a sequência desta aplicação.

Verificação rápida

O botão está estável como pressionado há várias passagens por loop(). O evento de pressionamento já iniciou a sequência na passagem anterior. Nesta passagem, o evento deve continuar verdadeiro para iniciar a sequência novamente.

A afirmação é verdadeira ou falsa?

Passo 3 de 8

Traduzir a tabela para enum e switch

Transforme os estados da tabela em nomes de código e organize a seleção com enum, switch, case, break e default.

Dar nome ao estado atual

Nomes explícitos para as etapas

A variável de estado guarda em que etapa a aplicação está entre uma passagem e outra de loop(). Em vez de números como 0, 1 e 2, use um enum: os nomes documentam o comportamento e reduzem trocas acidentais entre valores sem significado.

Os quatro nomes vêm diretamente da tabela já definida: REPOUSO, PRIMEIRO_PULSO, PAUSA e SEGUNDO_PULSO. A variável começa em REPOUSO e não deve ser declarada dentro de loop(), pois seria reinicializada a cada passagem.

Fluxo que o enum representa

Diagrama com quatro estados em ciclo: LED apagado, aceso, apagado e aceso, voltando ao primeiro; setas de cancelamento retornam ao primeiro estado.

Na ordem do ciclo: REPOUSO, PRIMEIRO_PULSO, PAUSA e SEGUNDO_PULSO. O desenho mostra que estados com LED apagado ainda podem ter papéis diferentes.

Selecionar sem continuar para o próximo case

Um case para cada estado

Use switch (estado) para traduzir cada linha da tabela em um case. Cada case contém somente as decisões válidas naquele estado.

A atribuição estado = PAUSA; altera a variável para as próximas passagens por loop(), mas não faz o switch voltar ao topo nesta mesma execução. Por isso, todo caso deve terminar em break: ele encerra a seleção atual e devolve o controle ao restante do programa.

Enum e switch da sequência

Estrutura inicial da máquina. As condições temporizadas serão acrescentadas depois; aqui, observe como os nomes da tabela viram cases e como cada case é encerrado.

cpp
enum Estado {
  REPOUSO,
  PRIMEIRO_PULSO,
  PAUSA,
  SEGUNDO_PULSO
};

Estado estado = REPOUSO;

void atualizarMaquina(bool eventoPressionado) {
  switch (estado) {
    case REPOUSO:
      if (eventoPressionado) {
        estado = PRIMEIRO_PULSO;
      }
      break;

    case PRIMEIRO_PULSO:
      // A condição de tempo será incluída depois.
      break;

    case PAUSA:
      // A condição de tempo será incluída depois.
      break;

    case SEGUNDO_PULSO:
      // A condição de tempo será incluída depois.
      break;

    default:
      estado = REPOUSO;
      digitalWrite(LED_BUILTIN, LOW);
      break;
  }
}

Dica

O papel do default

O default é o caminho definido quando estado não corresponde a nenhum case. Nesta aplicação, ele restabelece REPOUSO e comanda o LED apagado.

Isso define uma resposta coerente para um valor não reconhecido; não é uma promessa de proteger o firmware contra qualquer tipo de falha.

Praticar enum, break e default

Complete a seleção de estados

Complete a estrutura usando o nome do segundo estado, os encerramentos necessários e o caso para valor não reconhecido.

cpp
enum Estado {
  REPOUSO,
  ,
  PAUSA,
  SEGUNDO_PULSO
};

switch (estado) {
  case REPOUSO:
    if (eventoPressionado) {
      estado = PRIMEIRO_PULSO;
    }
    

  case PRIMEIRO_PULSO:
    // Decisão temporal será incluída depois.
    break;

  
    estado = REPOUSO;
    digitalWrite(LED_BUILTIN, LOW);
    
}

Passo 4 de 8

Executar a ação de entrada uma única vez

Centralize os efeitos de uma mudança de estado para que sejam executados apenas na entrada.

Ação de entrada versus permanência

Entrar não é permanecer

Uma passagem por loop() pode ocorrer muitas vezes enquanto a máquina continua no mesmo estado. Por isso, se o LED e a marca entradaEstado forem atualizados dentro do case a cada passagem, a ação de entrada será repetida sem que tenha havido transição.

Separe dois papéis:

  • Ao entrar: registrar o instante de entrada e comandar a saída correspondente ao novo estado.
  • Enquanto permanece: apenas consultar as condições que poderão levar a uma transição.

Assim, a marca temporal permanece fixa durante a permanência. Nos próximos passos, ela servirá de referência para verificar os prazos sem bloquear o loop.

Ação única na transição

Comparação entre repetir a marca de tempo a cada passagem pelo estado e registrá-la uma única vez ao entrar nele.

A transição concentra a alteração do estado, da marca de entrada e da saída. As passagens seguintes apenas avaliam condições.

Transição com efeitos coerentes

Uma única função de transição

Crie uma função para ser a única porta de mudança de estado. Ela recebe o próximo estado e o instante já observado na passagem atual. Primeiro, valida o destino; depois atualiza, em conjunto, o estado, a marca temporal e o LED.

O comando do LED não fica misturado à decisão de qual transição escolherá o próximo estado. Quem decide chama transicionarPara(...); a função aplica os efeitos da entrada.

Centralize a entrada no estado

Use esta função após as declarações do enum, de estado e de entradaEstado.

cpp
void transicionarPara(Estado proximo, unsigned long agora) {
  switch (proximo) {
    case REPOUSO:
    case PRIMEIRO_PULSO:
    case PAUSA:
    case SEGUNDO_PULSO:
      break;

    default:
      proximo = REPOUSO;
      break;
  }

  estado = proximo;
  entradaEstado = agora;

  if (estado == PRIMEIRO_PULSO || estado == SEGUNDO_PULSO) {
    digitalWrite(LED_BUILTIN, HIGH);
  } else {
    digitalWrite(LED_BUILTIN, LOW);
  }
}

Exemplo

Também trate o destino inesperado

Em cada case, chame a função somente quando a decisão realmente mudar o estado. Por exemplo, uma condição que inicia a sequência poderá chamar:

transicionarPara(PRIMEIRO_PULSO, agora);

Já o default do switch principal pode recuperar uma situação não reconhecida pela mesma porta:

default:
transicionarPara(REPOUSO, agora);
break;

Isso define LED apagado, estado REPOUSO e uma nova marca de entrada coerente. Não é uma proteção contra toda falha possível; é apenas o comportamento definido para esse caso.

Inicialize e consolide

Inicialização explícita

A inicialização não deve esperar uma transição que ainda não aconteceu. Em setup(), estabeleça explicitamente a condição inicial:

cpp
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);

  estado = REPOUSO;
  entradaEstado = millis();
  digitalWrite(LED_BUILTIN, LOW);
}

Prática: efeitos da transição

Complete a função de transição. As lacunas devem garantir que a marca é registrada e que o LED recebe o nível correspondente ao estado ativo ou inativo.

Complete a função

Preencha as ações que pertencem à entrada no novo estado.

cpp
void transicionarPara(Estado proximo, unsigned long agora) {
  estado = proximo;
   = agora;

  if (estado == PRIMEIRO_PULSO || estado == SEGUNDO_PULSO) {
    digitalWrite(LED_BUILTIN, );
  } else {
    digitalWrite(LED_BUILTIN, );
  }
}

Passo 5 de 8

Avançar por tempo sem bloquear o loop

Use a marca de entrada de cada estado para avançar pelos pulsos e pela pausa sem bloquear o loop.

Usar a marca de entrada

Consultar, não esperar

Nos estados temporizados, a máquina faz uma consulta curta: verifica se o intervalo contado desde entradaEstado já venceu.

agora - entradaEstado >= intervalo

Se a condição for falsa, não há transição nesta passagem. A função retorna, e o loop() pode continuar atendendo o botão e as demais tarefas. Não use delay() nem um laço de espera dentro de um case.

A marca entradaEstado foi registrada ao entrar no estado pela função transicionarPara(...), construída no step anterior.

Uma marca para cada entrada

Linha do tempo visual da sequência: primeiro pulso aceso, pausa apagada e segundo pulso aceso; cada etapa temporizada tem uma marca de relógio no início.

A marca temporal pertence ao estado atual e só muda quando uma transição realmente acontece.

Avançar sem travar

Manter a referência estável

Durante a permanência em um estado, a referência temporal deve ficar intacta. Atualizá-la em toda passagem deslocaria continuamente o início da contagem: a diferença agora - entradaEstado continuaria pequena e o prazo poderia nunca vencer.

A marca é atualizada somente na inicialização ou ao chamar transicionarPara(...). Quando ocorre uma transição, o mesmo instante agora observado nesta passagem passa a ser o início do intervalo seguinte.

Casos temporizados

Trecho para incluir nos cases temporizados de atualizarMaquina(...). A função transicionarPara(...) e a variável entradaEstado já foram definidas anteriormente.

cpp
case PRIMEIRO_PULSO:
  if ((unsigned long)(agora - entradaEstado) >= 1000UL) {
    transicionarPara(PAUSA, agora);
  }
  break;

case PAUSA:
  if ((unsigned long)(agora - entradaEstado) >= 500UL) {
    transicionarPara(SEGUNDO_PULSO, agora);
  }
  break;

case SEGUNDO_PULSO:
  if ((unsigned long)(agora - entradaEstado) >= 1000UL) {
    transicionarPara(REPOUSO, agora);
  }
  break;

Dica

Prazo observado, não espera ativa

O vencimento é percebido em uma passagem pelo loop() que observa a condição como verdadeira. Portanto, a mudança não é prometida exatamente no milissegundo do prazo. O importante é que a consulta seja breve e repetida com frequência, sem bloquear o atendimento das entradas.

Conferir os intervalos

Prática: intervalos da sequência

Complete as condições temporais usando a duração do estado que está terminando. As lacunas representam os intervalos da tabela de comportamento.

Completar as condições

Preencha os intervalos temporizados da sequência.

cpp
case PRIMEIRO_PULSO:
  if ((unsigned long)(agora - entradaEstado) >= ) {
    transicionarPara(PAUSA, agora);
  }
  break;

case PAUSA:
  if ((unsigned long)(agora - entradaEstado) >= ) {
    transicionarPara(SEGUNDO_PULSO, agora);
  }
  break;

case SEGUNDO_PULSO:
  if ((unsigned long)(agora - entradaEstado) >= ) {
    transicionarPara(REPOUSO, agora);
  }
  break;

Diagnosticar uma espera que não termina

Diagnosticar a marca temporal

A máquina está em PRIMEIRO_PULSO, mas a condição de 1000 ms nunca se torna verdadeira. Qual alteração provavelmente causou esse problema?

Passo 6 de 8

Definir a prioridade do cancelamento

Defina a prioridade do pressionamento aceito sobre o vencimento do intervalo para garantir uma única transição por atualização da máquina.

Escolher uma única decisão

Uma regra para a coincidência

Nos estados ativos — PRIMEIRO_PULSO, PAUSA e SEGUNDO_PULSO — um novo pressionamento aceito cancela a sequência e leva a aplicação para REPOUSO. Essa é uma política escolhida para esta sinalização.

Em uma passagem por loop(), podem estar verdadeiros tanto o evento de pressionamento quanto a condição de tempo decorrido. A máquina não tenta decidir qual ocorreu fisicamente “primeiro”: ela usa os valores observados naquela passagem e aplica uma prioridade definida no código.

Dica

Prioridade visível

O cancelamento é testado antes do prazo em cada estado ativo. Assim, há no máximo uma transição por chamada da atualização da máquina.

Diagrama de prioridade

Diagrama em que um pressionamento durante qualquer etapa ativa retorna diretamente ao repouso, enquanto os prazos avançam a sequência somente sem cancelamento.

Durante uma etapa ativa, o caminho de cancelamento tem prioridade sobre o avanço temporizado.

Codificar a prioridade

Cancelar antes de avançar

Considere a atualização em um estado ativo. A cadeia if/else if torna as alternativas mutuamente exclusivas: se o cancelamento ocorrer, o teste de prazo não decide outra transição nessa mesma chamada.

Decisão com prioridade

Trecho para um estado ativo, como PRIMEIRO_PULSO

cpp
case PRIMEIRO_PULSO:
  if (pressionamentoAceito) {
    transicionarPara(REPOUSO, agora);
  } else if (agora - entradaNoEstado >= 1000UL) {
    transicionarPara(PAUSA, agora);
  }
  break;

Dica

Uma transição nesta chamada

Mesmo que transicionarPara() altere a variável de estado, o switch já selecionou este case para a chamada atual. O break encerra a seleção. Portanto, o evento que cancelou não reinicia a sequência imediatamente; será necessário soltar e fazer um novo pressionamento aceito em outra passagem.

Aplicar a regra

Prática: completar a decisão

Complete a condição que deve ser avaliada primeiro em um estado ativo. Ela representa o evento momentâneo já produzido pelo tratamento de botão.

Prioridade no código

Preencha a primeira condição da cadeia de decisão.

cpp
if () {
  transicionarPara(REPOUSO, agora);
} else if (agora - entradaNoEstado >= intervalo) {
  transicionarPara(proximoEstado, agora);
}

Prever o resultado

Coincidência de evento e prazo

A máquina está em PRIMEIRO_PULSO. Na passagem atual, pressionamentoAceito é verdadeiro e já se passaram 1000 ms desde a entrada nesse estado. Após atualizar a máquina, qual é o estado e a saída?

Passo 7 de 8

Integrar entrada, máquina e saída

Integre o evento de botão tratado por rebote à máquina de estados e execute a sequência no Uno R3 próprio.

Preparar a integração

Um ciclo curto e previsível

A integração mantém responsabilidades separadas: o loop() observa o tempo uma vez, o tratamento de rebote produz um evento momentâneo e a máquina decide se haverá transição. O LED é comandado apenas pela função transicionarPara(...), quando a máquina entra em um estado.

Assim, manter o botão pressionado não cria vários comandos, e não há delay() bloqueando novas leituras.

Montagem da prática

Esquema de um Arduino Uno R3 com botão ligado entre D2 e GND; o LED integrado da placa é a saída.

Com a placa desenergizada, conecte o botão entre D2 e GND. Use o LED integrado e alimente o Uno somente pelo USB.

Atenção

Cuidados na montagem

Desconecte o USB antes de alterar a montagem. Não conecte fonte externa a VIN nem ao pino de 5 V enquanto o cabo USB estiver alimentando a placa.

Adicionar entrada e memória

Declarações e evento de pressionamento

Parte 1 do sketch: declarações, protótipos e tratamento de rebote. Crie um único sketch na IDE e insira os blocos na ordem apresentada.

cpp
const byte PINO_BOTAO = 2;
const unsigned long DEBOUNCE_MS = 30;
const unsigned long PULSO_MS = 1000;
const unsigned long PAUSA_MS = 500;

enum Estado {
  REPOUSO,
  PRIMEIRO_PULSO,
  PAUSA,
  SEGUNDO_PULSO
};

Estado estado = REPOUSO;
unsigned long entradaEstado = 0;

bool leituraAnterior = HIGH;
bool estadoEstavel = HIGH;
unsigned long mudouEm = 0;

bool atualizarBotao(unsigned long agora);
void transicionarPara(Estado proximo, unsigned long agora);
void atualizarMaquina(bool eventoPressionado, unsigned long agora);

bool atualizarBotao(unsigned long agora) {
  bool leitura = digitalRead(PINO_BOTAO);

  if (leitura != leituraAnterior) {
    leituraAnterior = leitura;
    mudouEm = agora;
  }

  bool eventoPressionado = false;
  if ((unsigned long)(agora - mudouEm) >= DEBOUNCE_MS &&
      leitura != estadoEstavel) {
    estadoEstavel = leitura;
    if (estadoEstavel == LOW) {
      eventoPressionado = true;
    }
  }
  return eventoPressionado;
}

O evento não é o nível do botão

atualizarBotao() retorna true somente na passagem em que um pressionamento estável é aceito. Nas demais passagens, inclusive enquanto o botão continua pressionado, retorna false.

As variáveis do botão e da máquina são persistentes: foram declaradas fora das funções e não são reinicializadas pelo loop().

Completar o sketch

Máquina e comando do LED

Parte 2 do sketch: a função de transição aplica a ação de entrada; a máquina decide qual transição é válida. Nos estados ativos, o evento de pressionamento é testado antes do prazo.

cpp
void transicionarPara(Estado proximo, unsigned long agora) {
  switch (proximo) {
    case REPOUSO:
    case PRIMEIRO_PULSO:
    case PAUSA:
    case SEGUNDO_PULSO:
      break;
    default:
      proximo = REPOUSO;
      break;
  }

  estado = proximo;
  entradaEstado = agora;

  if (estado == PRIMEIRO_PULSO || estado == SEGUNDO_PULSO) {
    digitalWrite(LED_BUILTIN, HIGH);
  } else {
    digitalWrite(LED_BUILTIN, LOW);
  }
}

void atualizarMaquina(bool eventoPressionado, unsigned long agora) {
  switch (estado) {
    case REPOUSO:
      if (eventoPressionado) {
        transicionarPara(PRIMEIRO_PULSO, agora);
      }
      break;

    case PRIMEIRO_PULSO:
      if (eventoPressionado) {
        transicionarPara(REPOUSO, agora);
      } else if ((unsigned long)(agora - entradaEstado) >= PULSO_MS) {
        transicionarPara(PAUSA, agora);
      }
      break;

    case PAUSA:
      if (eventoPressionado) {
        transicionarPara(REPOUSO, agora);
      } else if ((unsigned long)(agora - entradaEstado) >= PAUSA_MS) {
        transicionarPara(SEGUNDO_PULSO, agora);
      }
      break;

Conclusão da máquina, setup e loop

Parte 3 do sketch: complete a função da máquina e acrescente setup() e loop(). Depois de inserir as três partes no mesmo sketch, grave-o no Uno R3.

cpp
    case SEGUNDO_PULSO:
      if (eventoPressionado) {
        transicionarPara(REPOUSO, agora);
      } else if ((unsigned long)(agora - entradaEstado) >= PULSO_MS) {
        transicionarPara(REPOUSO, agora);
      }
      break;

    default:
      transicionarPara(REPOUSO, agora);
      break;
  }
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(PINO_BOTAO, INPUT_PULLUP);

  unsigned long agora = millis();
  leituraAnterior = digitalRead(PINO_BOTAO);
  estadoEstavel = leituraAnterior;
  mudouEm = agora;
  estado = REPOUSO;
  entradaEstado = agora;
  digitalWrite(LED_BUILTIN, LOW);
}

void loop() {
  unsigned long agora = millis();
  bool eventoPressionado = atualizarBotao(agora);
  atualizarMaquina(eventoPressionado, agora);
}
Saída esperada
LED inicialmente apagado.
Após um pressionamento aceito: LED aceso por cerca de 1 s, apagado por cerca de 0,5 s, aceso por cerca de 1 s e novamente apagado.
Um novo pressionamento aceito durante a sequência cancela a execução e deixa o LED apagado.

A duração é aproximada: a transição ocorre na primeira passagem pelo loop que observar o prazo vencido.

Dica

Uma referência de tempo por passagem

O mesmo valor de agora é entregue ao botão e à máquina na mesma passagem pelo loop(). Em um estado ativo, a cadeia if / else if dá prioridade ao cancelamento: se eventoPressionado for verdadeiro, o prazo não provoca outra transição nessa passagem.

Conferir a integração

Fluxo do ciclo

Ordene as operações de uma passagem pelo loop().

  1. Chamar `atualizarMaquina(eventoPressionado, agora)`.
  2. Obter `agora` com `millis()`.
  3. Chamar `atualizarBotao(agora)` e obter `eventoPressionado`.

Conectar evento e máquina

Complete o argumento que liga o evento do botão à atualização da máquina.

cpp
void loop() {
  unsigned long agora = millis();
  bool eventoPressionado = atualizarBotao(agora);
  atualizarMaquina(, agora);
}

Passo 8 de 8

Aplicar e verificar a sequência completa

Verifique a sinalização no Uno R3, teste cancelamentos e confirme por rastreamento a prioridade entre evento e prazo.

Observar a sequência normal

Testes independentes de observação

Com o sketch do step anterior gravado no Uno R3, compare a especificação com o LED integrado. Use somente a alimentação USB nesta prática.

Faça cada linha da tabela como uma execução independente: antes de iniciar o próximo caso, espere o LED ficar apagado em REPOUSO. Um novo pressionamento válido só pode existir após soltar o botão e pressioná-lo novamente.

Ao observar, considere as durações aproximadas: o loop detecta os prazos em passagens sucessivas, portanto não é esperado medir transições fisicamente exatas no milissegundo.

Roteiro de verificação

Execute cada caso a partir de REPOUSO, com o LED apagado. Registre o que realmente ocorreu no seu equipamento.

Casos para relacionar a tabela de comportamento, o firmware e a observação.
CasoComo iniciarComportamento esperadoResultado observado
PartidaLigue ou reinicie a placaLED apagado; máquina em REPOUSOAnote
Botão mantidoEm REPOUSO, pressione e mantenhaAcende ~1 s, apaga ~0,5 s, acende ~1 s e apagaAnote
SolturaEm REPOUSO, pressione; solte durante um pulso ou a pausaA soltura não causa transição; a sequência temporal continuaAnote
Novo cicloApós a sequência acabar, solte se necessário e pressioneInicia PRIMEIRO_PULSO novamenteAnote

Sequência e soltura esperadas

Linha do tempo de dois pulsos do LED e uma pausa; a soltura do botão durante a sequência não altera a linha do tempo.

Um evento inicia a sequência. Manter pressionado ou soltar depois não cria uma transição por si só.

Comprovar cancelamento e reinício

Cancelar em cada estado ativo

Teste o cancelamento em três execuções separadas. Em cada uma: comece em REPOUSO, pressione para iniciar, solte o botão, e faça um novo pressionamento durante o estado indicado.

  • Em PRIMEIRO_PULSO, o LED está aceso no primeiro pulso.
  • Em PAUSA, o LED está apagado entre os pulsos.
  • Em SEGUNDO_PULSO, o LED está aceso no segundo pulso.

Nos três casos, o novo evento deve cancelar para REPOUSO, deixando o LED apagado. Depois do cancelamento, solte e pressione novamente: esse novo evento deve iniciar outro PRIMEIRO_PULSO. A pausa continua sendo um estado ativo, mesmo tendo a mesma saída apagada de REPOUSO.

Seu registro de evidências

Registre quatro blocos de evidência:

  1. Sequência normal com botão mantido: dois pulsos separados pela pausa e retorno ao LED apagado.
  2. Soltura sem efeito: em que etapa você soltou e como a sequência continuou.
  3. Cancelamentos: resultado de um novo pressionamento em PRIMEIRO_PULSO, PAUSA e SEGUNDO_PULSO.
  4. Novo início: o que ocorreu após soltar e pressionar novamente depois de um cancelamento.

Descreva somente o que observou; este campo serve como seu registro de teste.

Escreva pelo menos 160 caracteres (0/160).

Confirmar a prioridade no código

Rastrear a coincidência de forma determinística

A coincidência entre evento e prazo não precisa ser reproduzida manualmente. Rastreie uma passagem de atualizarMaquina(...) usando os mesmos nomes do sketch:

  • estado é PRIMEIRO_PULSO;
  • eventoPressionado é true;
  • (unsigned long)(agora - entradaEstado) >= PULSO_MS também é verdadeiro.

A política da aplicação dá prioridade ao cancelamento. A cadeia mutuamente exclusiva deve produzir uma única chamada de transicionarPara(...): o LED termina apagado, pois o destino é REPOUSO.

Uma transição quando evento e prazo coincidem

Complete a decisão reconhecível do sketch integrado, preservando a prioridade do cancelamento.

cpp
case PRIMEIRO_PULSO:
   (eventoPressionado) {
    transicionarPara(, agora);
  } else if ((unsigned long)(agora - entradaEstado) >= PULSO_MS) {
    transicionarPara(PAUSA, agora);
  }
  break;

Consolidar evidências

Resumo

Checklist final

  • A tabela de comportamento orienta o código e também os testes observados no equipamento.
  • O estado permanece entre passagens do loop; o evento de pressionamento vale somente na passagem em que é produzido.
  • A marca entradaEstado só muda na inicialização ou em uma transição, permitindo que os intervalos vençam.
  • A ação de entrada comanda o LED uma vez por mudança de estado; não há espera bloqueante na máquina.
  • Nos estados ativos, eventoPressionado tem prioridade sobre o prazo e a cadeia if/else if produz uma única transição.
  • Os resultados anotados conectam a especificação, o sketch gravado e a evidência no LED.

Tutorial concluído

Parabéns! Você concluiu: Máquinas de estados no firmware

Sequência de sinalização verificada com comportamento normal, soltura, cancelamento e prioridade definida.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso