Início
/
Software
/
Dicas de Software
/
Máquina de Estados na Comunicação Serial do Arduino — Organizando Protocolos com FSM
Máquina de Estados na Comunicação Serial do Arduino — Organizando Protocolos com FSM
Angelo Luis Ferreira | 28/09/2026
Acessos: 218
Aprenda a organizar recepção, validação, processamento e tratamento de erros utilizando uma Máquina de Estados Finitos
Objetivo

O objetivo deste artigo é apresentar o conceito de Máquina de Estados Finita, também conhecida pela sigla FSM (Finite State Machine), e mostrar como essa técnica pode ser utilizada para organizar e controlar o fluxo de uma comunicação serial no Arduino.
Nos artigos anteriores desta série, evoluímos gradualmente de uma comunicação serial básica para um protocolo capaz de receber comandos estruturados, trabalhar com char[] e buffers, interpretar mensagens, responder com ACK/NACK, controlar Timeout e verificar a integridade dos dados por meio de checksum.
À medida que novos recursos são acrescentados, porém, o programa também se torna mais complexo.
Nosso protocolo já precisa saber, por exemplo, se está:
- aguardando uma nova mensagem;
- recebendo caracteres;
- validando os dados recebidos;
- processando um comando;
- tratando uma condição de erro.
Até agora, essas situações foram controladas principalmente por variáveis, como:
e por estruturas condicionais:
if (recebendo) {
// ...
}
Essa abordagem funciona bem em programas menores. Entretanto, conforme o protocolo cresce, podemos organizar essas diferentes situações de maneira mais clara utilizando uma
Em vez de controlar o comportamento por várias condições espalhadas pelo programa, passaremos a representar explicitamente cada situação como um estado:
AGUARDANDO
↓
RECEBENDO
↓
VALIDANDO
↓
PROCESSANDO
↓
AGUARDANDO
Ao longo deste artigo, utilizaremos exemplos práticos para compreender primeiro o funcionamento de uma máquina de estados e, depois, aplicaremos esse conceito ao protocolo serial desenvolvido nesta série.
Ao final desta leitura, você será capaz de:
- compreender o que é uma Máquina de Estados Finita (FSM);
- identificar estados, eventos e transições;
- entender como uma FSM organiza o comportamento de um programa;
- reconhecer estados que já existiam implicitamente em nosso protocolo;
- criar estados utilizando
enum;
- controlar estados utilizando
switch...case;
- implementar transições entre diferentes estados;
- utilizar máquinas de estados sem bloquear o
loop() do Arduino;
- aplicar uma FSM à recepção de dados pela porta serial;
- integrar buffer, parser, timeout, checksum, ACK e NACK a uma máquina de estados;
- tratar erros fazendo o sistema retornar a um estado conhecido;
- desenvolver programas mais organizados e preparados para protocolos mais complexos.
Este artigo representa uma mudança importante na evolução da série. Até agora, concentramos nossos esforços principalmente nos recursos do protocolo. A partir daqui, começaremos também a trabalhar na arquitetura do programa, organizando como cada uma dessas etapas se relaciona e em qual momento deve ser executada.
Referências
Este artigo dá continuidade à série sobre comunicação serial no Arduino:
Recomenda-se a leitura prévia dos artigos anteriores para melhor compreensão dos conceitos abordados aqui.
Definições
Antes de implementar nossa primeira máquina de estados, precisamos compreender alguns conceitos fundamentais.
O que é uma Máquina de Estados?
Uma Máquina de Estados Finita, ou FSM (Finite State Machine), é uma forma de organizar o comportamento de um sistema dividindo seu funcionamento em um número limitado de estados.
Em cada momento, o sistema encontra-se em um determinado estado e permanece nele até que algum evento provoque uma mudança.
Essa mudança é chamada de transição de estado.
Podemos representar a ideia básica assim:
ESTADO ATUAL
│
│ ocorre um evento
▼
TRANSIÇÃO
│
▼
NOVO ESTADO
O conceito pode parecer abstrato inicialmente, mas encontramos máquinas de estados em diversas situações do cotidiano.
Considere um semáforo:
VERDE
↓
AMARELO
↓
VERMELHO
↓
VERDE
O semáforo encontra-se sempre em um desses estados.
Depois de determinado intervalo de tempo, ocorre uma transição para o próximo estado.
No Arduino, poderíamos ter:
Estado atual: VERDE
Evento:
tempo esgotado
Novo estado:
AMARELO
Portanto, uma máquina de estados é basicamente uma maneira organizada de responder à pergunta:
Em qual situação o sistema está agora e o que deve fazê-lo mudar para a próxima situação?
O que é um Estado?
Um estado representa uma determinada condição ou situação em que o sistema se encontra.
No exemplo do semáforo, temos três estados:
Enquanto o sistema estiver no estado VERDE, determinadas ações são executadas.
Quando passa para AMARELO, seu comportamento muda.
Na comunicação serial também podemos identificar diferentes estados.
Por exemplo:
AGUARDANDO
RECEBENDO
VALIDANDO
PROCESSANDO
ERRO
Se o Arduino estiver em:
significa que ainda não existe uma mensagem sendo recebida.
Quando chega o caractere:
podemos mudar para:
O comportamento do programa passa então a ser diferente: agora os caracteres recebidos devem ser armazenados no buffer.
O que é um Evento?
Um evento é algo que acontece e pode provocar uma mudança de estado.
Eventos podem ser:
- um caractere recebido pela porta serial;
- o pressionamento de um botão;
- um sensor atingir determinado valor;
- um temporizador expirar;
- uma mensagem terminar;
- ocorrer um timeout;
- ocorrer um erro.
No nosso protocolo, o caractere:
<
pode representar um evento.
Se o Arduino estiver em:
e receber <, poderá iniciar uma nova mensagem:
AGUARDANDO
│
│ recebe '<'
▼
RECEBENDO
Outro evento pode ser o recebimento de:
que indica o final da mensagem:
RECEBENDO
│
│ recebe '>'
▼
VALIDANDO
Observe que estado e evento são conceitos diferentes.
RECEBENDO é uma situação em que o programa se encontra.
Já receber > é um acontecimento que pode fazer o programa abandonar esse estado.
O que é uma Transição?
Uma transição é a mudança de um estado para outro provocada por determinado evento ou condição.
Por exemplo:
AGUARDANDO
│
│ '<'
▼
RECEBENDO
Temos:
Estado atual → AGUARDANDO
Evento → recebeu '<'
Novo estado → RECEBENDO
Portanto:
é uma transição de estado.
Uma máquina de estados é formada justamente por um conjunto de estados e pelas regras que determinam essas transições.
Estado Inicial
Toda máquina de estados precisa começar em algum estado conhecido.
Esse estado é chamado de estado inicial.
Em nosso protocolo, quando o Arduino é ligado, ainda não estamos recebendo nenhuma mensagem.
Portanto, um estado inicial bastante lógico será:
A partir dele, o sistema espera o início de uma nova mensagem:
Arduino ligado
│
▼
┌───────────┐
│ AGUARDANDO│
└─────┬─────┘
│
recebe '<'
│
▼
┌───────────┐
│ RECEBENDO │
└───────────┘
Definir um estado inicial é importante porque torna o comportamento do sistema previsível desde o início da execução.
Ação
Além dos estados e transições, uma máquina de estados normalmente precisa executar ações.
Uma ação é simplesmente alguma tarefa realizada pelo programa.
Por exemplo:
ESTADO: RECEBENDO
Ação: armazenar caracteres no buffer
Ou:
ESTADO: PROCESSANDO
Ação: executar o comando LED
Podemos então começar a visualizar nossa FSM como:

Essa tabela será importante mais adiante, quando transformarmos esses estados em código.
Uma FSM precisa ter sequência fixa?
Não. Esse é um ponto importante.
Uma máquina de estados não significa simplesmente executar uma sequência fixa de etapas.
Dependendo do evento ocorrido, o sistema pode seguir caminhos diferentes.
Por exemplo:
┌─────────────┐
│ RECEBENDO │
└──────┬──────┘
│
┌─────────────┼─────────────┐
│ │ │
recebe > TIMEOUT OVERFLOW
│ │ │
▼ ▼ ▼
VALIDANDO ERRO ERRO
Portanto, o próximo estado depende do que aconteceu durante a execução.
Essa característica torna as máquinas de estados especialmente úteis em sistemas embarcados, nos quais diferentes eventos podem acontecer em momentos diferentes.
Nosso código anterior já possuía estados?
Sim — embora não tivéssemos utilizado formalmente uma FSM.
No artigo anterior utilizamos:
Quando:
o sistema estava, na prática:
Quando executávamos:
o sistema passava, conceitualmente, para:
Portanto, já tínhamos algo semelhante a:
recebendo = false
│
│ recebe '<'
▼
recebendo = true
O problema começa quando precisamos representar mais situações.
Com apenas duas possibilidades, um bool funciona muito bem:
Mas agora queremos representar:
AGUARDANDO
RECEBENDO
VALIDANDO
PROCESSANDO
ERRO
Uma variável bool não consegue representar claramente cinco estados diferentes.
É justamente nesse ponto que uma Máquina de Estados começa a fazer bastante sentido.
FSM não é uma biblioteca
Também é importante não confundir o conceito.
Uma Máquina de Estados Finita não é uma biblioteca específica do Arduino e não exige a instalação de nenhuma biblioteca externa.
FSM é uma técnica de organização da lógica do programa.
Podemos implementá-la utilizando recursos normais da linguagem C++, como:
e:
Vamos seguir exatamente do ponto em que encerramos o bloco de Definições: antes de levar a FSM para o protocolo completo, vale construir um exemplo pequeno, visual e testável. O próprio texto já prepara essa transição ao apresentar enum e switch...case como os recursos que usaremos. Markdown colado
Exemplo Prático — Nossa Primeira Máquina de Estados
Antes de aplicarmos uma Máquina de Estados ao protocolo de comunicação serial, vamos compreender seu funcionamento com um exemplo simples.
Utilizaremos o LED interno do Arduino Uno, conectado normalmente ao pino 13, para criar um sistema com três estados:
DESLIGADO
↓
LIGADO
↓
PISCANDO
↓
DESLIGADO
A mudança de estado será feita por meio do Monitor Serial.
O usuário poderá digitar:
para avançar para o próximo estado.
Assim:
Estado atual Evento Novo estado
DESLIGADO ──── recebe 1 ────> LIGADO
LIGADO ──── recebe 1 ────> PISCANDO
PISCANDO ──── recebe 1 ────> DESLIGADO
Exemplo/Teste: esse exemplo não pretende ainda implementar nosso protocolo <COMANDO|PARAMETRO>. Seu objetivo é apenas tornar visíveis os conceitos de estado, evento, transição e ação antes de aplicá-los à comunicação serial completa.
Criando os Estados com enum
Até agora utilizamos frequentemente variáveis bool para representar duas situações:
Uma variável bool, entretanto, possui apenas dois valores possíveis:
Nosso exemplo precisa representar três estados diferentes:
DESLIGADO
LIGADO
PISCANDO
Para isso, podemos utilizar enum.
enum Estado {
DESLIGADO,
LIGADO,
PISCANDO
};
enum é a abreviação de enumeration (enumeração) e permite criar um tipo formado por um conjunto definido de valores nomeados.
Observação:
- O
enum é uma lista de nomes para um único valor. Imagine o enum como uma etiqueta. Em vez de usar números soltos no código (como 0, 1, 2), você dá nomes a eles (LIGADO, DESLIGADO, EM_ESPERA). Uma variável do tipo enum guarda apenas um desses estados por vez.
- O
array é uma caixa com várias gavetas separadas. O array é uma coleção de dados. Ele serve para guardar múltiplos valores (que podem ser números, textos ou outros objetos) organizados em fila, onde cada gaveta tem um número de índice para você acessar o conteúdo.
Em seguida, criamos uma variável desse tipo:
Estado estadoAtual = DESLIGADO;
Podemos interpretar essa declaração como:
Estado → tipo da variável
estadoAtual → nome da variável
DESLIGADO → valor inicial
Portanto, quando o Arduino iniciar:
Definição: enum permite representar um conjunto limitado de opções utilizando nomes significativos em vez de números.
Por que não utilizar números?
Tecnicamente, poderíamos criar algo como:
byte estadoAtual = 0;
e definir:
0 = DESLIGADO
1 = LIGADO
2 = PISCANDO
Depois teríamos condições como:
if (estadoAtual == 2) {
// ...
}
O problema é que o número 2, isoladamente, não explica o que representa.
Com enum, podemos escrever:
if (estadoAtual == PISCANDO) {
// ...
}
Agora o próprio código informa sua intenção.
✔️ Mais legível:
❌ Menos legível:
Dica: nomes de estados devem representar claramente a situação do sistema. Isso facilita a leitura, a manutenção e a expansão do programa.
Controlando os Estados com switch...case
Agora precisamos determinar o que o Arduino deverá fazer em cada estado.
Para isso, utilizaremos:
A estrutura básica é:
switch (estadoAtual) {
case DESLIGADO:
// ações
break;
case LIGADO:
// ações
break;
case PISCANDO:
// ações
break;
}
O switch verifica o valor de:
e direciona a execução para o case correspondente.
Se:
estadoAtual == LIGADO
será executado:
case LIGADO:
Se:
estadoAtual == PISCANDO
será executado:
case PISCANDO:
Importante: o break encerra aquele case e impede que a execução continue automaticamente para o próximo.
Código Completo do Primeiro Exemplo
const byte LED = 13;
enum Estado {
DESLIGADO,
LIGADO,
PISCANDO
};
Estado estadoAtual = DESLIGADO;
unsigned long tempoAnterior = 0;
const unsigned long INTERVALO = 500;
bool estadoLed = false;
void setup() {
Serial.begin(9600);
pinMode(LED, OUTPUT);
Serial.println("Maquina de Estados iniciada.");
Serial.println("Digite 1 para mudar de estado.");
Serial.println();
Serial.println("Estado atual: DESLIGADO");
}
void loop() {
// =====================================================
// EVENTO RECEBIDO PELO MONITOR SERIAL
// =====================================================
if (Serial.available() > 0) {
char comando = Serial.read();
if (comando == '1') {
switch (estadoAtual) {
case DESLIGADO:
estadoAtual = LIGADO;
Serial.println("DESLIGADO -> LIGADO");
break;
case LIGADO:
estadoAtual = PISCANDO;
Serial.println("LIGADO -> PISCANDO");
break;
case PISCANDO:
estadoAtual = DESLIGADO;
Serial.println("PISCANDO -> DESLIGADO");
break;
}
}
}
// =====================================================
// AÇÃO DE CADA ESTADO
// =====================================================
switch (estadoAtual) {
case DESLIGADO:
digitalWrite(LED, LOW);
break;
case LIGADO:
digitalWrite(LED, HIGH);
break;
case PISCANDO:
if (millis() - tempoAnterior >= INTERVALO) {
tempoAnterior = millis();
estadoLed = !estadoLed;
digitalWrite(LED, estadoLed);
}
break;
}
}
Monitor serial

Simulação
Para compreender melhor o funcionamento da nossa primeira Máquina de Estados Finita (FSM), podemos utilizar o simulador Tinkercad Circuits, que permite executar o programa em um Arduino Uno virtual, sem precisar montar um circuito físico.
Neste exemplo, utilizaremos apenas o LED interno do Arduino (LED_BUILTIN, pino 13), portanto não será necessário adicionar componentes ou realizar conexões externas.
Como executar a simulação
-
Abra o projeto no Tinkercad e clique em Código para inserir o programa apresentado anteriormente.
-
Clique em Iniciar simulação.
-
Abra o Monitor Serial e configure a velocidade para 9600 baud, caso seja necessário.
-
Digite 1 e envie o comando para mudar de estado. Repita o procedimento para observar as próximas transições.

A cada comando, o Monitor Serial também apresenta a transição realizada, por exemplo:
DESLIGADO -> LIGADO
LIGADO -> PISCANDO
PISCANDO -> DESLIGADO
Dica: configure o Monitor Serial para não adicionar caracteres de fim de linha, se essa opção estiver disponível. O exemplo reconhece apenas o caractere '1' e ignora os demais.
Importante: observe que o LED continua piscando enquanto o Arduino aguarda novos comandos. Isso acontece porque utilizamos millis() em vez de delay(), permitindo que o programa continue executando o loop() sem bloquear a recepção serial.
Esse experimento demonstra, na prática, como um evento provoca uma transição de estado e altera o comportamento do sistema — exatamente o princípio que aplicaremos ao nosso protocolo de comunicação serial.
Como o Exemplo Funciona
No início do programa, declaramos:
enum Estado {
DESLIGADO,
LIGADO,
PISCANDO
};
A palavra-chave enum permite criar um novo tipo de dado chamado Estado, formado por um conjunto de constantes nomeadas.
Nesse exemplo, o tipo Estado possui três valores possíveis:

Por padrão, quando não atribuímos valores explicitamente, a primeira constante recebe 0 e as seguintes recebem valores inteiros consecutivos.
Importante: Estado é o nome do tipo que criamos, enquanto DESLIGADO, LIGADO e PISCANDO são as constantes que pertencem a essa enumeração. Elas não são strings nem variáveis independentes, são constantes.
O funcionamento da enumeração no nosso programa pode ser representado assim:
enum Estado
│
├── DESLIGADO = 0
├── LIGADO = 1
└── PISCANDO = 2
│
▼
Estado estadoAtual
│
▼
DESLIGADO (0)
│ recebe '1'
▼
LIGADO (1)
│ recebe '1'
▼
PISCANDO (2)
│ recebe '1'
▼
DESLIGADO (0)
Criando uma variável do tipo Estado
Depois de definir a enumeração, declaramos:
Estado estadoAtual = DESLIGADO;
Essa instrução possui três elementos:
-
Estado — tipo de dado criado com enum.
-
estadoAtual — variável que armazenará o estado atual do sistema.
-
DESLIGADO — valor atribuído inicialmente à variável.
Podemos interpretar essa declaração assim:
Tipo da variável: Estado
Nome da variável: estadoAtual
Valor inicial: DESLIGADO (0)
Portanto, ao iniciar o programa, a variável estadoAtual contém o valor correspondente a DESLIGADO.
Isto define o estado inicial.
Portanto:
Estado atual: DESLIGADO
LED: apagado
Quando o usuário digita: 1
ocorre um evento.
O primeiro switch verifica o estado atual:
case DESLIGADO:
e realiza a transição:
estadoAtual = LIGADO;
Temos então:
DESLIGADO
│
│ recebe '1'
▼
LIGADO
O segundo switch determina a ação correspondente ao novo estado:
case LIGADO:
digitalWrite(LED, HIGH);
break;
O LED acende.
Digitando 1 novamente:
LIGADO
│
│ recebe '1'
▼
PISCANDO
E uma terceira vez:
PISCANDO
│
│ recebe '1'
▼
DESLIGADO
Temos, portanto, um ciclo completo:
recebe '1'
┌──────────────────┐
▼ │
┌────────────┐ │
│ DESLIGADO │ │
└──────┬─────┘ │
│ recebe '1' │
▼ │
┌────────────┐ │
│ LIGADO │ │
└──────┬─────┘ │
│ recebe '1' │
▼ │
┌────────────┐ │
│ PISCANDO │───────────┘
└────────────┘
Um detalhe importante: o LED pisca sem delay()
Observe que não utilizamos:
delay(500);
No estado PISCANDO, utilizamos:
if (millis() - tempoAnterior >= INTERVALO)
Isso permite controlar o intervalo sem interromper completamente a execução do loop().
⚠️ Atenção: utilizar longos delay() em uma máquina de estados pode prejudicar uma de suas principais vantagens: permitir que o programa continue verificando eventos enquanto controla diferentes comportamentos.
Essa característica será ainda mais importante quando voltarmos ao nosso protocolo serial, pois o Arduino precisa continuar verificando a chegada de novos caracteres e condições como timeout.
O que este exemplo nos ensinou?
Agora podemos identificar claramente os quatro elementos apresentados nas definições anteriores:

Entenda melhor: observe também que transição e ação não são a mesma coisa. Alterar estadoAtual muda a situação lógica do sistema; digitalWrite() executa uma ação correspondente àquela situação.
Esse pequeno exemplo contém a mesma lógica fundamental que utilizaremos em nosso protocolo. A diferença é que, em vez de controlar apenas três comportamentos de um LED, nossa próxima máquina de estados terá que controlar etapas como:
AGUARDANDO
↓
RECEBENDO
↓
VALIDANDO
↓
PROCESSANDO
↓
AGUARDANDO
além dos caminhos provocados por timeout, overflow, checksum inválido e outros erros.
Identificando e Projetando os Estados do Nosso Protocolo Serial
Agora que compreendemos o funcionamento de uma Máquina de Estados Finita (FSM) e implementamos nosso primeiro exemplo prático utilizando enum e switch...case, chegou o momento de aplicar esses conceitos ao protocolo de comunicação serial desenvolvido ao longo desta série.
Nos artigos anteriores, construímos um sistema capaz de receber comandos estruturados, armazená-los em um buffer, interpretar seus parâmetros, controlar timeout, responder com ACK/NACK e realizar uma verificação didática de checksum.
Entretanto, essas operações estavam distribuídas entre diferentes condições e funções do programa.
Nosso próximo objetivo não será criar um protocolo completamente novo, mas reorganizar o que já desenvolvemos utilizando uma Máquina de Estados Finita.
1. Identificando os Estados do Protocolo
Antes de escrever o código, precisamos identificar as diferentes situações pelas quais o sistema passa durante a recepção e o processamento de uma mensagem.
Para nosso protocolo, utilizaremos cinco estados:

Importante: cada estado deve possuir uma responsabilidade bem definida. Essa separação facilita a compreensão, os testes e a manutenção do programa.
Representação visual da FSM

O diagrama apresenta o fluxo principal da comunicação. Em condições normais, a mensagem passa pelos estados AGUARDANDO, RECEBENDO, VALIDANDO e PROCESSANDO.
Caso seja detectada uma falha, o sistema poderá entrar no estado ERRO, tratar a ocorrência e retornar a AGUARDANDO.
2. Como Cada Estado Funciona
2.1. Estado AGUARDANDO
É o estado inicial da máquina.
Quando o Arduino é ligado, ainda não existe uma mensagem em processamento. Portanto, o sistema permanece aguardando o caractere que identifica o início de uma nova mensagem:
Por exemplo, ao receber:
o primeiro caractere < provoca a transição:
Qualquer caractere recebido antes de < será ignorado.
Dica: ignorar caracteres fora de uma mensagem ajuda a evitar que dados inesperados sejam interpretados como comandos.
2.2. Estado RECEBENDO
Nesse estado, o Arduino armazena os caracteres recebidos em um buffer do tipo char[].
Considere:
O processo ocorre caractere por caractere:

Observe que os delimitadores < e > não precisam ser armazenados no buffer, pois sua função é identificar o início e o fim da mensagem.
Quando o caractere > é encontrado, o Arduino adiciona o terminador nulo:
buffer[indice] = '\0';
Isso transforma os caracteres armazenados em uma string C válida, pronta para análise.
A máquina então realiza a transição:
⚠️ Atenção: enquanto estiver recebendo caracteres, o sistema também deverá verificar duas condições importantes: overflow do buffer e timeout da recepção.
2.3. Estado VALIDANDO
Depois de receber a mensagem completa, o Arduino precisa verificar se ela pode ser processada.
Nessa etapa, o programa poderá:
-
Verificar se a estrutura da mensagem está correta.
-
Identificar os campos do protocolo.
-
Validar o checksum, quando ele estiver presente.
-
Rejeitar mensagens malformadas ou inconsistentes.
Por exemplo, considere a mensagem:
O conteúdo é:
O Arduino identifica o checksum informado, recalcula a soma sobre LED|1 e compara os valores.
Se estiverem corretos:
Caso contrário:
Importante: no Artigo 5 da série, o próprio Arduino calculava e acrescentava o checksum a partir do comando digitado. Esse procedimento servia para demonstrar o algoritmo, mas não detectava alterações ocorridas antes do cálculo. Neste artigo, vamos distinguir esse teste didático da validação de um checksum realmente recebido, preparando o protocolo para comunicação entre dois dispositivos.
2.4. Estado PROCESSANDO
Nesse estado, a mensagem já passou pelas verificações necessárias.
O Arduino pode então interpretar o comando e executar a ação correspondente.
Por exemplo:
O parser identifica:
Comando: LED
Parâmetro: 1
Em seguida, executa:
digitalWrite(LED, HIGH);
e responde:
Após concluir o processamento, a máquina retorna ao estado inicial:
Se o comando ou parâmetro for inválido, o sistema deverá responder com o NACK correspondente, sem executar uma ação indevida.
2.5. Estado ERRO
O estado ERRO é responsável pelo tratamento de falhas detectadas durante a comunicação.
Por exemplo, se a mensagem começar com <, mas não for concluída dentro do tempo permitido:
o Arduino identifica um timeout.
A transição será:
O sistema poderá responder:
Depois de tratar o erro, deverá limpar os controles de recepção necessários e retornar ao estado:
Isso evita que uma mensagem incompleta comprometa o processamento das próximas mensagens.
3. Criando os Estados com enum
Agora que definimos os estados, podemos representá-los diretamente na linguagem C++.
Utilizaremos:
enum Estado {
AGUARDANDO,
RECEBENDO,
VALIDANDO,
PROCESSANDO,
ERRO
};
Como aprendemos no exemplo anterior, enum cria um tipo de dado com constantes nomeadas.
Neste caso:

Em seguida, declaramos a variável que armazenará o estado atual:
Estado estadoAtual = AGUARDANDO;
Essa declaração significa que:
-
Estado é o tipo criado pela enumeração.
-
estadoAtual é a variável tipo Estado que guarda o estado atual.
-
AGUARDANDO é o valor inicial atribuído à variável.
Quando o Arduino receber <, poderemos escrever:
estadoAtual = RECEBENDO;
Quando encontrar >:
estadoAtual = VALIDANDO;
E quando a mensagem for validada:
estadoAtual = PROCESSANDO;
Observe que a mesma variável acompanha todo o ciclo de vida da mensagem, sem necessidade de criar várias variáveis booleanas para representar cada etapa.
4. Definindo as Regras de Transição
Uma máquina de estados não depende apenas da definição dos estados. Também precisamos estabelecer quais eventos provocam cada transição.
Para o nosso protocolo, teremos inicialmente:

Observação: nem todo evento provoca uma mudança de estado. Enquanto o Arduino recebe caracteres comuns, por exemplo, ele continua em RECEBENDO, apenas atualizando o buffer.
Essa é uma característica fundamental das FSMs: o sistema pode permanecer em um estado durante várias execuções do loop(), até que ocorra um evento capaz de provocar uma transição.
5. Separando Recepção e Processamento
Uma das vantagens dessa arquitetura é a possibilidade de separar responsabilidades.
No nosso protocolo, podemos organizar o programa em duas grandes atividades:
Essa separação também facilita a depuração. Se uma mensagem não estiver sendo recebida corretamente, investigaremos os estados AGUARDANDO e RECEBENDO. Se for recebida, mas rejeitada, investigaremos VALIDANDO e o tratamento de erros.
Entenda melhor: uma FSM não exige obrigatoriamente cinco estados. Poderíamos implementar o protocolo utilizando menos estados ou acrescentando outros. A escolha depende da complexidade e das responsabilidades que desejamos separar. Neste artigo, utilizaremos cinco estados para facilitar a compreensão e a evolução do projeto.
Implementando a Máquina de Estados na Comunicação Serial do Arduino
Agora que identificamos os estados do nosso protocolo e definimos as regras de transição, chegou o momento de transformar esse planejamento em código.
Neste bloco, vamos implementar a estrutura principal da Máquina de Estados Finita (FSM), utilizando os recursos que aprendemos anteriormente: enum, switch...case, char[], Serial.available(), Serial.read() e millis().
Para facilitar a compreensão, construiremos inicialmente uma versão funcional responsável por receber mensagens, controlar o buffer e identificar erros de timeout e overflow.
Posteriormente, integraremos a validação do checksum, a interpretação dos comandos e as respostas ACK/NACK.
A vantagem dessa abordagem é permitir que o leitor compreenda cada etapa antes de trabalhar com o protocolo completo.
1. Definindo os Estados da Máquina
Conforme estabelecemos no bloco anterior, nossa máquina terá cinco estados:
enum Estado {
AGUARDANDO,
RECEBENDO,
VALIDANDO,
PROCESSANDO,
ERRO
};
Cada constante representa uma situação específica do protocolo.
Em seguida, criamos a variável responsável por armazenar o estado atual:
Estado estadoAtual = AGUARDANDO;
Quando o Arduino iniciar, essa variável terá o valor AGUARDANDO.
Durante a execução, o programa poderá alterar esse valor conforme os eventos recebidos:
estadoAtual = RECEBENDO;
ou:
estadoAtual = VALIDANDO;
Importante: a variável estadoAtual é o elemento central da nossa FSM. É por meio dela que o programa identifica qual etapa da comunicação deve executar.
2. Configurando o Buffer e o Timeout
Como já aprendemos nos artigos anteriores, precisamos reservar um espaço de memória para armazenar os caracteres recebidos pela porta serial.
Utilizaremos:
const byte TAM_BUFFER = 50;
const unsigned long TIMEOUT = 3000;
char buffer[TAM_BUFFER];
byte indice = 0;
unsigned long tempoInicial = 0;
Essas variáveis possuem funções específicas:

Observe que não utilizamos mais:
bool recebendo = false;
Isso acontece porque a informação de que estamos recebendo uma mensagem agora é representada diretamente por:
estadoAtual == RECEBENDO
Portanto, o estado substitui a variável booleana utilizada anteriormente, tornando a lógica mais explícita.
3. Criando uma Função para Reiniciar a Recepção
Antes de implementar o loop(), criaremos uma função auxiliar responsável por restaurar as condições iniciais da recepção.
void reiniciarRecepcao() {
indice = 0;
buffer[0] = '\0';
tempoInicial = 0;
estadoAtual = AGUARDANDO;
}
Essa função executa quatro operações:
-
Reinicia o índice do buffer.
-
Marca o buffer como uma string vazia.
-
Reinicia a referência temporal da recepção.
-
Retorna a máquina ao estado AGUARDANDO.
Entenda melhor: não precisamos apagar todos os caracteres armazenados no buffer. Ao colocar '\0' na primeira posição, o conteúdo passa a ser interpretado como uma string vazia. Os bytes antigos podem continuar na memória, mas deixam de fazer parte da string válida.
A função reiniciarRecepcao() será utilizada sempre que uma mensagem terminar de ser processada ou quando ocorrer um erro.
Isso evita repetir o mesmo conjunto de instruções em diferentes partes do programa.
4. Implementando a Estrutura com switch...case
O próximo passo é criar a estrutura que controlará o comportamento da máquina.
switch (estadoAtual) {
case AGUARDANDO:
// Aguarda o início de uma mensagem
break;
case RECEBENDO:
// Armazena os caracteres recebidos
break;
case VALIDANDO:
// Verifica a mensagem
break;
case PROCESSANDO:
// Executa o processamento
break;
case ERRO:
// Trata a condição de erro
break;
}
O switch verifica o valor atual da variável estadoAtual e executa o bloco correspondente.
Diferentemente de uma sequência convencional de instruções, cada estado possui seu próprio comportamento.
Por exemplo, quando estamos em AGUARDANDO, o programa não deve armazenar caracteres comuns. Ele precisa encontrar o delimitador <.
Já em RECEBENDO, os caracteres devem ser armazenados até que seja encontrado >.
5. Implementando a Recepção dos Caracteres
A recepção continua utilizando os mesmos recursos dos artigos anteriores:
while (Serial.available() > 0) {
char c = Serial.read();
// Processamento conforme o estado atual
}
A diferença é que agora o tratamento de cada caractere depende do estado da FSM.
Estado AGUARDANDO
Enquanto o sistema estiver aguardando uma mensagem, somente o caractere < provocará uma transição.
case AGUARDANDO:
if (c == '<') {
indice = 0;
tempoInicial = millis();
estadoAtual = RECEBENDO;
}
break;
Observe:
Essa instrução altera o estado da máquina.
A partir do próximo caractere, o programa passa a executar as regras definidas para RECEBENDO.
Estado RECEBENDO
Agora precisamos armazenar os caracteres até encontrar o delimitador final >.
case RECEBENDO:
if (c == '>') {
buffer[indice] = '\0';
estadoAtual = VALIDANDO;
}
else if (c == '<') {
indice = 0;
tempoInicial = millis();
}
else if (indice < TAM_BUFFER - 1) {
buffer[indice++] = c;
}
else {
estadoAtual = ERRO;
}
break;
Esse bloco contempla quatro situações:
Recebimento de >: encerra a mensagem e altera o estado para VALIDANDO.
Recebimento de <: reinicia a recepção, permitindo sincronizar novamente o início de uma mensagem.
Caractere comum: armazena o caractere no buffer, respeitando sua capacidade.
Buffer cheio: altera o estado para ERRO, impedindo gravações fora dos limites do array.
⚠️ Atenção: a condição indice < TAM_BUFFER - 1 reserva uma posição para o terminador nulo '\0'. Isso evita que a string ultrapasse o espaço de memória reservado.
6. Controlando o Timeout sem Bloquear o Programa
Além de receber caracteres, nossa máquina precisa verificar se uma mensagem permaneceu incompleta por tempo excessivo.
Utilizaremos:
if (estadoAtual == RECEBENDO &&
millis() - tempoInicial >= TIMEOUT) {
estadoAtual = ERRO;
}
A condição verifica duas situações:
-
O Arduino está recebendo uma mensagem.
-
O tempo decorrido desde o início da recepção atingiu o limite definido.
Por exemplo:
Mensagem recebida parcialmente:
<LED|1
Tempo decorrido: 3000 ms
Resultado:
RECEBENDO → ERRO
O programa não utiliza delay() para aguardar a conclusão da mensagem.
Em vez disso, verifica periodicamente o tempo transcorrido.
Dica: utilizar millis() permite que o Arduino continue executando outras tarefas enquanto acompanha o tempo de recepção. Essa técnica é especialmente importante em sistemas embarcados que precisam responder a múltiplos eventos.
7. Código Completo — FSM para Recepção Serial
Agora reuniremos os conceitos em um único exemplo funcional.
Nesta primeira implementação, os estados VALIDANDO e PROCESSANDO terão comportamento simplificado. A mensagem será exibida no Monitor Serial, mas ainda não será interpretada como um comando de aplicação.
Também acrescentaremos uma variável para identificar o motivo de uma falha, permitindo responder adequadamente em caso de timeout ou overflow.
// =======================================================
// CONFIGURAÇÕES
// =======================================================
const byte TAM_BUFFER = 50;
const unsigned long TIMEOUT = 3000;
// =======================================================
// ESTADOS DA MÁQUINA
// =======================================================
enum Estado {
AGUARDANDO,
RECEBENDO,
VALIDANDO,
PROCESSANDO,
ERRO
};
Estado estadoAtual = AGUARDANDO;
// =======================================================
// TIPOS DE ERRO
// =======================================================
enum TipoErro {
SEM_ERRO,
ERRO_TIMEOUT,
ERRO_OVERFLOW,
ERRO_MENSAGEM_VAZIA
};
TipoErro erroAtual = SEM_ERRO;
// =======================================================
// BUFFER E CONTROLE
// =======================================================
char buffer[TAM_BUFFER];
byte indice = 0;
unsigned long tempoInicial = 0;
// =======================================================
// REINICIA A RECEPÇÃO
// =======================================================
void reiniciarRecepcao() {
indice = 0;
buffer[0] = '\0';
tempoInicial = 0;
erroAtual = SEM_ERRO;
estadoAtual = AGUARDANDO;
}
// =======================================================
// SETUP
// =======================================================
void setup() {
Serial.begin(9600);
Serial.println("Maquina de Estados iniciada.");
Serial.println("FSM - Recepcao Serial");
Serial.println();
Serial.println("Digite uma mensagem:");
Serial.println("<LED|1>");
Serial.println("<MOTOR|150>");
Serial.println("<TEMP|?>");
Serial.println("--------------------");
}
// =======================================================
// LOOP PRINCIPAL
// =======================================================
void loop() {
// -----------------------------------------------------
// CONTROLE DE TIMEOUT
// -----------------------------------------------------
if (estadoAtual == RECEBENDO &&
millis() - tempoInicial >= TIMEOUT) {
erroAtual = ERRO_TIMEOUT;
estadoAtual = ERRO;
}
// -----------------------------------------------------
// RECEPÇÃO SERIAL
// -----------------------------------------------------
while (Serial.available() > 0 &&
(estadoAtual == AGUARDANDO ||
estadoAtual == RECEBENDO)) {
char c = Serial.read();
switch (estadoAtual) {
// =================================================
// AGUARDANDO
// =================================================
case AGUARDANDO:
if (c == '<') {
indice = 0;
tempoInicial = millis();
estadoAtual = RECEBENDO;
}
break;
// =================================================
// RECEBENDO
// =================================================
case RECEBENDO:
if (c == '>') {
buffer[indice] = '\0';
estadoAtual = VALIDANDO;
}
else if (c == '<') {
indice = 0;
tempoInicial = millis();
}
else if (indice < TAM_BUFFER - 1) {
buffer[indice++] = c;
}
else {
erroAtual = ERRO_OVERFLOW;
estadoAtual = ERRO;
}
break;
default:
break;
}
}
// -----------------------------------------------------
// VALIDAÇÃO, PROCESSAMENTO E ERROS
// -----------------------------------------------------
switch (estadoAtual) {
// ===================================================
// VALIDANDO
// ===================================================
case VALIDANDO:
if (indice == 0) {
erroAtual = ERRO_MENSAGEM_VAZIA;
estadoAtual = ERRO;
}
else {
estadoAtual = PROCESSANDO;
}
break;
// ===================================================
// PROCESSANDO
// ===================================================
case PROCESSANDO:
Serial.print("Mensagem recebida: <");
Serial.print(buffer);
Serial.println(">");
Serial.println("<ACK|RECEBIDO>");
Serial.println("--------------------");
reiniciarRecepcao();
break;
// ===================================================
// ERRO
// ===================================================
case ERRO:
switch (erroAtual) {
case ERRO_TIMEOUT:
Serial.println("<NACK|TIMEOUT>");
break;
case ERRO_OVERFLOW:
Serial.println("<NACK|OVERFLOW>");
break;
case ERRO_MENSAGEM_VAZIA:
Serial.println("<NACK|MENSAGEM_VAZIA>");
break;
default:
Serial.println("<NACK|ERRO_DESCONHECIDO>");
break;
}
Serial.println("--------------------");
reiniciarRecepcao();
break;
default:
break;
}
}
8. Testando a Máquina de Estados
Podemos executar esse programa diretamente no Arduino Uno ou utilizar o simulador Tinkercad.
Abra o Monitor Serial e envie algumas mensagens para observar o comportamento da FSM.
Simulador didático — Estados da recepção. Digite os comandos abaixo:
< (mensagem com mais de 49 caracteres) >
Resultado mostrado no Monitor Serial (Tinkercad)
Simulação ilustrativa da lógica do programa. Para testar o código real, utilize o Monitor Serial do Arduino ou do Tinkercad.
Uma observação sobre os testes
Nesta etapa, uma mensagem como:
também será aceita, desde que não esteja vazia nem ultrapasse o tamanho do buffer.
Isso acontece porque ainda não implementamos a validação da estrutura do protocolo nem o processamento real dos comandos.
O ACK utilizado neste exemplo: <ACK|RECEBIDO>
Isto significa apenas que a mensagem passou pelas verificações simplificadas desta etapa. Não significa que o comando foi reconhecido ou executado.
9. O Que Aprendemos com Esta Implementação?
Neste bloco, construímos a primeira versão da FSM aplicada à recepção serial.
A principal diferença em relação ao código anterior é que agora cada etapa do funcionamento está associada a um estado específico.

Além disso, introduzimos um segundo enum, chamado TipoErro, para armazenar a causa da falha separadamente do estado atual.
Isso é importante porque o estado ERRO informa que existe uma falha, enquanto erroAtual informa qual falha ocorreu.
Importante: a FSM não elimina a necessidade de if, while ou outras estruturas condicionais. Ela organiza essas estruturas dentro de estados bem definidos, tornando o comportamento geral mais fácil de acompanhar.
Integrando Parser, Checksum e ACK/NACK à Máquina de Estados
Chegamos à etapa final do nosso artigo sobre Máquinas de Estados Finitas (FSM) na Comunicação Serial do Arduino.
Nos blocos anteriores, aprendemos a representar estados utilizando enum, controlar transições com switch...case e construir uma máquina capaz de receber mensagens, armazená-las em um buffer e detectar situações de timeout e overflow.
Agora vamos integrar esses conhecimentos em um projeto prático completo, com componentes eletrônicos que poderão ser montados e simulados no protoboard real ou no Tinkercad.
Nosso sistema permitirá controlar um LED, acionar um buzzer passivo e consultar a temperatura ambiente utilizando um sensor TMP36, tudo por meio de comandos enviados pelo Monitor Serial.
1. Estrutura do projeto

1.1. Ligações elétricas

Tabela conexões

⚠️ Atenção: para identificar os terminais do TMP36, observe o sensor com a face plana voltada para você e os terminais apontando para baixo. Da esquerda para a direita, teremos +Vs, Vout e GND. Confira também as identificações exibidas pelo Tinkercad.
2. Comandos do protocolo
Utilizaremos seis comandos principais:

São cinco mensagens distintas, distribuídas entre três dispositivos ou funções do projeto.
O protocolo também aceitará mensagens com checksum:
<COMANDO|PARAMETRO|CHECKSUM>
Por exemplo:
O valor 386 corresponde à soma dos códigos ASCII dos caracteres de LED|1:
76+69+68+124+49=386
O Arduino recalculará esse valor e verificará se coincide com o checksum recebido.
Dois modos de funcionamento
No modo didático, os comandos podem ser enviados sem checksum. O Arduino calcula e apresenta o valor correspondente para fins de aprendizagem.
No modo de protocolo, o checksum deve acompanhar a mensagem enviada, permitindo verificar sua integridade antes da execução do comando.
Importante: no modo didático, o checksum calculado após a recepção não comprova a integridade da transmissão. Para isso, é necessário comparar o valor recalculado com aquele enviado pelo transmissor.
3. Fluxo da Máquina de Estados
A FSM continuará utilizando os cinco estados definidos anteriormente:

Se ocorrer timeout, overflow, erro de formato ou checksum inválido, o sistema entrará em ERRO, enviará um NACK e retornará ao estado inicial.
4. Código completo — Arduino Uno com LED, Buzzer e TMP36
O programa a seguir reúne todos os recursos desenvolvidos nos artigos anteriores. Além da comunicação serial, acrescentamos a leitura analógica do TMP36.
#include <string.h>
// =======================================================
// CONFIGURAÇÕES
// =======================================================
const byte TAM_BUFFER = 50;
const unsigned long TIMEOUT = 3000;
const bool DEBUG = true;
const bool MODO_DIDATICO = true;
// =======================================================
// PINOS DOS COMPONENTES
// =======================================================
const byte PINO_LED = 7;
const byte PINO_BUZZER = 8;
const byte PINO_TMP36 = A0;
// =======================================================
// ESTADOS DA FSM
// =======================================================
enum Estado {
AGUARDANDO,
RECEBENDO,
VALIDANDO,
PROCESSANDO,
ERRO
};
Estado estadoAtual = AGUARDANDO;
// =======================================================
// TIPOS DE ERRO
// =======================================================
enum TipoErro {
SEM_ERRO,
ERRO_TIMEOUT,
ERRO_OVERFLOW,
ERRO_FORMATO,
ERRO_CHECKSUM
};
TipoErro erroAtual = SEM_ERRO;
// =======================================================
// BUFFER E CONTROLE
// =======================================================
char buffer[TAM_BUFFER];
byte indice = 0;
unsigned long tempoInicial = 0;
char* comando = NULL;
char* parametro = NULL;
// =======================================================
// DEBUG
// =======================================================
void debugEstado(const char* texto) {
if (DEBUG) {
Serial.print("[FSM] ");
Serial.println(texto);
}
}
// =======================================================
// REINICIA A RECEPÇÃO
// =======================================================
void reiniciarRecepcao() {
indice = 0;
buffer[0] = '\0';
tempoInicial = 0;
comando = NULL;
parametro = NULL;
erroAtual = SEM_ERRO;
estadoAtual = AGUARDANDO;
}
// =======================================================
// CALCULA CHECKSUM
// =======================================================
unsigned int calculaChecksum(const char* texto) {
unsigned int soma = 0;
while (*texto) {
soma += (unsigned char)*texto;
texto++;
}
return soma;
}
// =======================================================
// CONVERSÃO NUMÉRICA SEGURA
// =======================================================
bool converterNumero(const char* texto,
unsigned int& valor) {
if (*texto == '\0') {
return false;
}
unsigned long resultado = 0;
while (*texto) {
if (*texto < '0' || *texto > '9') {
return false;
}
byte digito = *texto - '0';
if (resultado > (65535UL - digito) / 10UL) {
return false;
}
resultado = resultado * 10UL + digito;
texto++;
}
valor = (unsigned int)resultado;
return true;
}
// =======================================================
// LEITURA DO SENSOR TMP36
// =======================================================
float lerTemperatura() {
int leitura = analogRead(PINO_TMP36);
float tensao = leitura * (5.0 / 1023.0);
float temperatura = (tensao - 0.5) * 100.0;
return temperatura;
}
// =======================================================
// SETUP
// =======================================================
void setup() {
Serial.begin(9600);
pinMode(PINO_LED, OUTPUT);
pinMode(PINO_BUZZER, OUTPUT);
pinMode(PINO_TMP36, INPUT);
digitalWrite(PINO_LED, LOW);
noTone(PINO_BUZZER);
Serial.println("FSM Serial iniciada.");
Serial.println("Arduino Uno - LED, Buzzer e TMP36");
Serial.println();
Serial.println("Comandos disponiveis:");
Serial.println("<LED|1>");
Serial.println("<LED|0>");
Serial.println("<BUZZER|1>");
Serial.println("<BUZZER|0>");
Serial.println("<TEMP|?>");
Serial.println();
Serial.println("Exemplo com checksum:");
Serial.println("<LED|1|386>");
Serial.println("--------------------");
}
// =======================================================
// LOOP PRINCIPAL
// =======================================================
void loop() {
// -----------------------------------------------------
// VERIFICA TIMEOUT
// -----------------------------------------------------
if (estadoAtual == RECEBENDO &&
millis() - tempoInicial >= TIMEOUT) {
erroAtual = ERRO_TIMEOUT;
estadoAtual = ERRO;
}
// -----------------------------------------------------
// RECEPÇÃO SERIAL
// -----------------------------------------------------
while (Serial.available() > 0 &&
(estadoAtual == AGUARDANDO ||
estadoAtual == RECEBENDO)) {
char c = Serial.read();
switch (estadoAtual) {
// ================================================
// AGUARDANDO
// ================================================
case AGUARDANDO:
if (c == '<') {
indice = 0;
tempoInicial = millis();
estadoAtual = RECEBENDO;
debugEstado("AGUARDANDO -> RECEBENDO");
}
break;
// ================================================
// RECEBENDO
// ================================================
case RECEBENDO:
if (c == '>') {
buffer[indice] = '\0';
estadoAtual = VALIDANDO;
debugEstado("RECEBENDO -> VALIDANDO");
}
else if (c == '<') {
indice = 0;
tempoInicial = millis();
debugEstado("Recepcao reiniciada");
}
else if (indice < TAM_BUFFER - 1) {
buffer[indice++] = c;
}
else {
erroAtual = ERRO_OVERFLOW;
estadoAtual = ERRO;
}
break;
default:
break;
}
}
// -----------------------------------------------------
// EXECUÇÃO DOS ESTADOS
// -----------------------------------------------------
switch (estadoAtual) {
case VALIDANDO:
validarMensagem();
break;
case PROCESSANDO:
processarComando();
reiniciarRecepcao();
break;
case ERRO:
tratarErro();
reiniciarRecepcao();
break;
default:
break;
}
}
// =======================================================
// VALIDAÇÃO DA MENSAGEM
// =======================================================
void validarMensagem() {
if (indice == 0) {
erroAtual = ERRO_FORMATO;
estadoAtual = ERRO;
return;
}
if (DEBUG) {
Serial.print("[DEBUG] Mensagem: ");
Serial.println(buffer);
}
// -----------------------------------------------------
// IDENTIFICA OS SEPARADORES
// -----------------------------------------------------
char* primeiro = strchr(buffer, '|');
char* ultimo = strrchr(buffer, '|');
if (primeiro == NULL ||
primeiro == buffer ||
*(primeiro + 1) == '\0') {
erroAtual = ERRO_FORMATO;
estadoAtual = ERRO;
return;
}
// -----------------------------------------------------
// VERIFICA CHECKSUM, SE PRESENTE
// -----------------------------------------------------
if (primeiro != ultimo) {
// Rejeita separadores adicionais.
if (strchr(primeiro + 1, '|') != ultimo ||
ultimo == primeiro + 1 ||
*(ultimo + 1) == '\0') {
erroAtual = ERRO_FORMATO;
estadoAtual = ERRO;
return;
}
*ultimo = '\0';
unsigned int checksumRecebido;
if (!converterNumero(ultimo + 1,
checksumRecebido)) {
erroAtual = ERRO_FORMATO;
estadoAtual = ERRO;
return;
}
unsigned int checksumCalculado =
calculaChecksum(buffer);
if (DEBUG) {
Serial.print("[DEBUG] Checksum recebido: ");
Serial.println(checksumRecebido);
Serial.print("[DEBUG] Checksum calculado: ");
Serial.println(checksumCalculado);
}
if (checksumRecebido != checksumCalculado) {
erroAtual = ERRO_CHECKSUM;
estadoAtual = ERRO;
return;
}
}
else {
if (!MODO_DIDATICO) {
erroAtual = ERRO_FORMATO;
estadoAtual = ERRO;
return;
}
if (DEBUG) {
Serial.println(
"[DEBUG] Modo didatico: sem checksum recebido"
);
Serial.print("[DEBUG] Checksum calculado: ");
Serial.println(calculaChecksum(buffer));
}
}
// -----------------------------------------------------
// SEPARA COMANDO E PARÂMETRO
// -----------------------------------------------------
*primeiro = '\0';
comando = buffer;
parametro = primeiro + 1;
if (*comando == '\0' ||
*parametro == '\0') {
erroAtual = ERRO_FORMATO;
estadoAtual = ERRO;
return;
}
estadoAtual = PROCESSANDO;
debugEstado("VALIDANDO -> PROCESSANDO");
}
// =======================================================
// PROCESSAMENTO DOS COMANDOS
// =======================================================
void processarComando() {
// -----------------------------------------------------
// LED
// -----------------------------------------------------
if (strcmp(comando, "LED") == 0) {
if (strcmp(parametro, "1") == 0) {
digitalWrite(PINO_LED, HIGH);
Serial.println("<ACK|LED_ON>");
}
else if (strcmp(parametro, "0") == 0) {
digitalWrite(PINO_LED, LOW);
Serial.println("<ACK|LED_OFF>");
}
else {
Serial.println("<NACK|PARAMETRO_LED>");
}
}
// -----------------------------------------------------
// BUZZER PASSIVO
// -----------------------------------------------------
else if (strcmp(comando, "BUZZER") == 0) {
if (strcmp(parametro, "1") == 0) {
tone(PINO_BUZZER, 1000);
Serial.println("<ACK|BUZZER_ON>");
}
else if (strcmp(parametro, "0") == 0) {
noTone(PINO_BUZZER);
Serial.println("<ACK|BUZZER_OFF>");
}
else {
Serial.println("<NACK|PARAMETRO_BUZZER>");
}
}
// -----------------------------------------------------
// SENSOR DE TEMPERATURA TMP36
// -----------------------------------------------------
else if (strcmp(comando, "TEMP") == 0) {
if (strcmp(parametro, "?") == 0) {
float temperatura = lerTemperatura();
Serial.print("<ACK|TEMP=");
Serial.print(temperatura, 1);
Serial.println(">");
}
else {
Serial.println("<NACK|PARAMETRO_TEMP>");
}
}
// -----------------------------------------------------
// COMANDO DESCONHECIDO
// -----------------------------------------------------
else {
Serial.println("<NACK|COMANDO_INVALIDO>");
}
debugEstado("PROCESSANDO -> AGUARDANDO");
}
// =======================================================
// TRATAMENTO DE ERROS
// =======================================================
void tratarErro() {
switch (erroAtual) {
case ERRO_TIMEOUT:
Serial.println("<NACK|TIMEOUT>");
break;
case ERRO_OVERFLOW:
Serial.println("<NACK|OVERFLOW>");
break;
case ERRO_FORMATO:
Serial.println("<NACK|FORMATO_INVALIDO>");
break;
case ERRO_CHECKSUM:
Serial.println("<NACK|CHECKSUM_INVALIDO>");
break;
default:
Serial.println("<NACK|ERRO_DESCONHECIDO>");
break;
}
debugEstado("ERRO -> AGUARDANDO");
}
Observação: o código mantém a mesma arquitetura FSM do exemplo anterior, mas agora os comandos estão vinculados a componentes reais. A leitura do TMP36 é realizada somente quando o comando TEMP|? chega ao estado PROCESSANDO.
O parser foi implementado com a identificação direta dos separadores |, preservando a distinção entre comando, parâmetro e checksum. Isso também permite rejeitar campos vazios e separadores excedentes.
5. Como funciona a leitura do TMP36?
O TMP36 é um sensor analógico de temperatura. Sua saída fornece uma tensão proporcional à temperatura medida.
A relação utilizada é:

Onde:
-
T = temperatura em graus Celsius.
-
Vout = tensão de saída do sensor, em volts.
-
0,5 = tensão de referência correspondente a 0 °C.
-
0,01 = variação de tensão de 10 mV por grau Celsius.
No Arduino Uno, utilizando a referência analógica padrão de 5 V, a conversão é realizada por:
int leitura = analogRead(PINO_TMP36);
float tensao = leitura * (5.0 / 1023.0);
float temperatura = (tensao - 0.5) * 100.0;
Exemplo de cálculo
Suponha que o TMP36 forneça uma tensão de 0,75 V.
Aplicando a fórmula:

Isso significa que, ao enviar:
o Arduino poderá responder:
O valor apresentado dependerá da temperatura configurada no sensor e da resolução da conversão analógica. Portanto, não é um valor fixo nem uma temperatura simulada pelo código.
6. Simulação no Tinkercad
7. Testando o projeto no Tinkercad
Depois de montar o circuito e carregar o programa, clique em Iniciar simulação e abra o Monitor Serial.
Configure a velocidade de comunicação para 9600 baud.
7.1. Testes dos componentes

7.2. Testando a leitura de temperatura
No Tinkercad, durante a simulação, selecione o sensor TMP36 e altere sua temperatura pelo controle disponibilizado pelo simulador.
Envie novamente:
Observe que o Arduino deverá apresentar um novo valor de temperatura.
Essa experiência demonstra a integração entre comunicação serial, conversão analógica e Máquina de Estados Finita.
7.3. Testando o checksum
Envie:
Como o checksum está correto, o LED deverá acender.
Resposta:
Agora envie:
O programa identificará que o checksum recebido não corresponde ao valor calculado.
Resposta: <NACK|CHECKSUM_INVALIDO>
O comando será rejeitado, sem alterar o estado do LED.
7.4. Testando o timeout e o overflow
Para provocar um timeout, envie uma mensagem incompleta:
Após aproximadamente três segundos, o Arduino deverá responder:
Para testar o overflow, envie uma mensagem contendo mais caracteres do que o buffer permite armazenar. O resultado esperado será:
⚠️ Atenção: mantenha MODO_DIDATICO = true para testar comandos sem checksum. Quando quiser exigir a presença do checksum em todas as mensagens, altere essa constante para false.
8. O que há de novo nesta implementação?
A principal evolução em relação aos exemplos anteriores é que agora estamos utilizando três dispositivos físicos diferentes, cada um com um comportamento próprio.

9. Por que utilizar uma FSM neste projeto?
Imagine que o LED esteja aceso e o buzzer emitindo um som contínuo. Enquanto isso, o usuário envia um comando para consultar a temperatura.
O Arduino precisa receber os caracteres, identificar o comando, validar a mensagem, ler o sensor e responder, sem interromper desnecessariamente o funcionamento dos outros componentes.
A FSM permite organizar esse processo por etapas bem definidas.
Importante: os estados da FSM representam as etapas da comunicação serial. Eles não representam diretamente os estados físicos do LED ou do buzzer. Por exemplo, o buzzer pode permanecer ligado enquanto a FSM retorna ao estado AGUARDANDO.
10. Considerações importantes
Nossa implementação demonstra uma arquitetura organizada para comunicação serial em sistemas embarcados. Entretanto, alguns cuidados devem ser considerados.
O checksum por soma simples possui limitações e não detecta todos os tipos de alteração. Aplicações que exigem maior confiabilidade podem utilizar CRC.
O TMP36 fornece uma medição analógica cuja precisão depende, entre outros fatores, da referência de tensão utilizada e das características do sensor. Para nosso projeto didático no Tinkercad, consideramos a referência padrão de 5 V do Arduino Uno.
O buzzer passivo é acionado pela função tone(), que gera um sinal periódico. Diferentemente de um buzzer ativo, ele depende desse sinal para produzir som.
Por fim, o código foi preparado para montagem e teste no Tinkercad, mas seus resultados devem ser confirmados na simulação. Ao utilizar componentes reais, verifique suas especificações elétricas e as conexões antes de alimentar o circuito.
Conclusão
Neste artigo, aprendemos que uma Máquina de Estados Finita não é apenas um recurso teórico de programação, mas uma ferramenta prática para organizar o funcionamento de sistemas embarcados.
Ao integrar a FSM ao nosso protocolo serial, desenvolvemos um sistema capaz de receber mensagens, validar sua estrutura, verificar checksum, interpretar comandos, controlar dispositivos e tratar erros de maneira organizada.
Com um Arduino Uno, um LED, um buzzer passivo e um sensor TMP36, conseguimos demonstrar aplicações concretas desses conceitos, utilizando componentes acessíveis e disponíveis no Tinkercad.
Mais importante ainda, compreendemos que o estado atual da comunicação e o estado físico dos dispositivos são informações diferentes, que podem ser controladas de maneira independente.
Essa separação permite desenvolver sistemas progressivamente mais complexos, sem perder a clareza da arquitetura.
O anúncio abaixo ajuda a manter o Squids Arduino funcionando
Comentários