Comunicação Serial Avançada no Arduino: ACK, NACK e Timeout em Protocolos de Aplicação

Comunicação Serial avançada com Arduino - Validação de mensagens recebidas e executadas

Objetivo

O objetivo deste artigo é aprofundar o desenvolvimento de protocolos de aplicação na comunicação serial do Arduino, implementando mecanismos de confirmação de mensagens (ACK/NACK) e controle de timeout para tornar a comunicação mais robusta, previsível e confiável.

Nos artigos anteriores desta série, construímos progressivamente:

  • Os fundamentos da comunicação serial no Arduino
  • Leitura não bloqueante utilizando char[]
  • Manipulação manual de buffers
  • Parsing de comandos
  • Implementação de um protocolo simples de aplicação estruturado

Agora avançaremos para um novo nível:

???? garantir que as mensagens realmente foram recebidas, interpretadas e executadas corretamente.

Para isso, implementaremos um sistema baseado em:

  • ACK (Acknowledgment) → confirmação de recebimento

  • NACK (Negative Acknowledgment) → indicação de erro

  • Timeout → detecção de falha ou ausência de resposta

E evoluiremos para uma etapa mais próxima de sistemas embarcados profissionais, adicionando mecanismos capazes de:

  • Confirmar mensagens válidas
  • Detectar falhas de comunicação
  • Identificar mensagens incompletas
  • Tratar erros de protocolo
  • Evitar espera infinita por dados

Ao final deste artigo, você será capaz de:

  • Entender o funcionamento de protocolos confiáveis de comunicação
  • Implementar confirmação de mensagens no Arduino
  • Detectar falhas utilizando timeout
  • Criar respostas automáticas ACK/NACK
  • Desenvolver parsers mais robustos
  • Validar mensagens recebidas
  • Construir sistemas embarcados mais previsíveis e seguros

Este conteúdo representa um passo importante na evolução entre projetos didáticos e arquiteturas utilizadas em automação, IoT e sistemas embarcados profissionais.

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

ACK (Acknowledgment)

ACK é uma mensagem de confirmação enviada por um dispositivo para informar que uma mensagem foi recebida e processada corretamente.

Por exemplo:

<ACK>

Na prática:

  • Um comando é enviado ao Arduino
  • O Arduino interpreta e executa o comando
  • O Arduino responde com ACK

Isso permite confirmar que a comunicação ocorreu corretamente.

NACK (Negative Acknowledgment)

NACK é uma mensagem utilizada para indicar erro na comunicação ou no processamento do comando.

Ela pode ocorrer quando:

  • A mensagem possui formato inválido
  • O comando não existe
  • O parâmetro recebido é inválido
  • O buffer foi corrompido
  • O timeout foi excedido

Exemplos:

<NACK>
ou:
<NACK|ERRO_COMANDO>
O uso de NACK torna o protocolo mais robusto e previsível.

Timeout

Timeout é o tempo máximo permitido para que uma operação seja concluída.

Na comunicação serial, ele é utilizado para detectar situações como:

  • Mensagens incompletas
  • Falhas de transmissão ou perda de dados
  • Travamentos de comunicação
  • Ausência de resposta

Por exemplo:

O Arduino começa a receber:

<LED|1

Mas o caractere > nunca chega.

Nesse caso, o sistema pode aguardar por um período máximo e cancelar automaticamente a mensagem incompleta.

Isso evita que o parser fique preso indefinidamente esperando o restante dos dados.

Comunicação Serial Confiável

Nos artigos anteriores, implementamos protocolos estruturados para organização das mensagens.

Agora adicionaremos mecanismos de confiabilidade ao sistema.

Isso significa que o Arduino será capaz de:

  • Confirmar mensagens válidas
  • Detectar erros
  • Ignorar mensagens inválidas
  • Cancelar recepções incompletas
  • Proteger o parser contra corrupção de dados

Esse modelo é amplamente utilizado em:

  • Automação industrial
  • Sistemas IoT
  • Comunicação entre microcontroladores
  • Equipamentos eletrônicos
  • Protocolos embarcados

Estrutura do Protocolo

Neste artigo continuaremos utilizando mensagens no formato:

<COMANDO|PARAMETRO>

Exemplos:

<LED|1>
<LED|0>
<BUZZER|1>
<BUZZER|0>

Respostas do sistema:

<ACK>
<NACK>

ou:

<ACK|LED_ON>
<NACK|PARAMETRO_INVALIDO>
Componentes do Projeto

Neste projeto utilizaremos:

  • Arduino Uno
  • LEDs
  • Resistores
  • Buzzer
  • Protoboard
  • Monitor Serial da Arduino IDE

O Monitor Serial será utilizado como terminal de testes para envio e recebimento das mensagens do protocolo.

O que será desenvolvido ao longo do artigo?

Ao longo deste tutorial implementaremos:

✔ Parser robusto
✔ Controle de LEDs
✔ Controle de buzzer
✔ ACK/NACK automático
✔ Timeout de recepção
✔ Validação de mensagens
✔ Tratamento de erros
✔ Comunicação serial mais segura

Resultado Final Esperado

Ao final do projeto, teremos um sistema semelhante ao fluxo abaixo:

Monitor Serial

│ <LED|1>

Arduino

│ <ACK|LED_ON>

Monitor Serial

Se ocorrer erro:

<LED|X>

Resposta:

<NACK|PARAMETRO_INVALIDO>

Ou mensagem incompleta:

<LED|1

Resultado:

TIMEOUT

Implementando ACK, NACK e Timeout no Arduino

Após entender os conceitos de ACK, NACK e Timeout, vamos agora implementar um protocolo mais robusto no Arduino utilizando:

✔ Leitura não bloqueante
✔ Parser estruturado
✔ Confirmação automática de mensagens
✔ Tratamento de erros
✔ Controle de timeout

O objetivo será transformar o Arduino em um sistema capaz de:

  • Receber mensagens estruturadas
  • Validar o protocolo
  • Executar comandos
  • Responder automaticamente ao emissor
  • Detectar falhas de comunicação

Estratégia de Funcionamento

Nosso parser funcionará da seguinte forma:

  1. Aguarda o caractere <
  2. Inicia captura da mensagem
  3. Armazena os caracteres no buffer
  4. Aguarda o caractere >
  5. Finaliza a string com \0
  6. Processa a mensagem
  7. Responde com ACK ou NACK

Além disso:

  • Se a mensagem ultrapassar o tamanho do buffer → erro
  • Se o caractere > não chegar dentro do tempo esperado → timeout
  • Se o comando for inválido → NACK

Código de Exemplo

#include <string.h>

// =======================================================
// CONFIGURAÇÕES
// =======================================================
#define TIMEOUT 3000   // 3 segundos

// =======================================================
// PINOS
// =======================================================
const byte LED = 13;
const byte BUZZER = 8;

// =======================================================
// BUFFER
// =======================================================
char buffer[40];
byte indice = 0;

// =======================================================
// CONTROLE DO PARSER
// =======================================================
bool recebendo = false;
unsigned long tempoInicial = 0;

// =======================================================
// SETUP
// =======================================================
void setup() {
  Serial.begin(9600);

  pinMode(LED, OUTPUT);
  pinMode(BUZZER, OUTPUT);

  Serial.println("Sistema pronto.");
  Serial.println("Protocolo iniciado.");
  Serial.println("Exemplo:");
  Serial.println("<LED|1>");
  Serial.println("<BUZZER|0>");
  Serial.println("-------------------");
}

// =======================================================
// LOOP PRINCIPAL
// =======================================================
void loop() {
  while (Serial.available() > 0) {
    char c = Serial.read();
    // ---------------------------------------------------
    // Ignora lixo antes do caractere <
    // ---------------------------------------------------
    if (!recebendo && c != '<') {
      continue;
    }
    // ---------------------------------------------------
    // INÍCIO DA MENSAGEM
    // ---------------------------------------------------
    if (c == '<') {
      recebendo = true;
      indice = 0;
      tempoInicial = millis();
      continue;
    }
    // ---------------------------------------------------
    // RECEBIMENTO DA MENSAGEM
    // ---------------------------------------------------
    if (recebendo) {
      // FINAL DA MENSAGEM
      if (c == '>') {
        buffer[indice] = '\0';
        recebendo = false;
        processaMensagem();
        indice = 0;
      }
      else {
        // PROTEÇÃO CONTRA OVERFLOW
        if (indice < 39) {
          buffer[indice++] = c;
        }
        else {
          recebendo = false;
          indice = 0;
          Serial.println("<NACK|OVERFLOW>");
        }
      }
    }
  }

  // =====================================================
  // CONTROLE DE TIMEOUT
  // =====================================================
  if (recebendo) {
    if (millis() - tempoInicial > TIMEOUT) {
      recebendo = false;
      indice = 0;
      Serial.println("<NACK|TIMEOUT>");
    }
  }
}

// =======================================================
// PROCESSAMENTO DA MENSAGEM
// =======================================================
void processaMensagem() {
  // Divide comando e parâmetro
  char* comando = strtok(buffer, "|");
  char* parametro = strtok(NULL, "|");

  // ---------------------------------------------------
  // VALIDAÇÃO
  // ---------------------------------------------------
  if (comando == NULL || parametro == NULL) {
    Serial.println("<NACK|FORMATO_INVALIDO>");
    return;
  }

  // =====================================================
  // COMANDO LED
  // =====================================================
  if (strcmp(comando, "LED") == 0) {
    if (strcmp(parametro, "1") == 0) {
      digitalWrite(LED, HIGH);
      Serial.println("<ACK|LED_ON>");
    }
    else if (strcmp(parametro, "0") == 0) {
      digitalWrite(LED, LOW);
      Serial.println("<ACK|LED_OFF>");
    }
    else {
      Serial.println("<NACK|PARAMETRO_LED>");
    }
  }

  // =====================================================
  // COMANDO BUZZER
  // =====================================================
  else if (strcmp(comando, "BUZZER") == 0) {
    if (strcmp(parametro, "1") == 0) {
      tone(BUZZER, 1000);
      Serial.println("<ACK|BUZZER_ON>");
    }
    else if (strcmp(parametro, "0") == 0) {
      noTone(BUZZER);
      Serial.println("<ACK|BUZZER_OFF>");
    }
    else {
      Serial.println("<NACK|PARAMETRO_BUZZER>");
    }
  }

  // =====================================================
  // COMANDO INVÁLIDO
  // =====================================================
  else {
    Serial.println("<NACK|COMANDO_INVALIDO>");
  }
}

Exemplos de resultados

Como o Código Funciona

1. Início da Mensagem

O parser aguarda o caractere:

<

Quando ele chega:

if (c == '<')

o sistema:

  • ativa o modo de recepção
  • limpa o índice
  • inicia o controle de timeout

2. Armazenamento no Buffer

Os caracteres recebidos são armazenados:

buffer[indice++] = c;

Exemplo:

<LED|1>

O buffer armazenará:

LED|1

3. Finalização da Mensagem

Quando chega:

>

a string é finalizada:

buffer[indice] = '\0';

Isso transforma o conteúdo em uma string válida do padrão C.

4. Parsing da Mensagem

O código utiliza:

strtok(buffer, "|");

para dividir:

LED|1

em:

  • comando → "LED"
  • parâmetro → "1"

5. ACK

Se o comando for válido:

<ACK|LED_ON>

6. NACK

Se ocorrer erro:

<NACK|COMANDO_INVALIDO>

7. Timeout

Se a mensagem começar mas não terminar:

<LED|1

o sistema aguardará:

TIMEOUT 3000

Após 3 segundos:

<NACK|TIMEOUT>

Como o Parser Funciona Internamente

Nos exemplos anteriores utilizamos um parser capaz de receber mensagens estruturadas, validar seu formato e executar comandos. Mas o que acontece internamente desde o momento em que um caractere chega pela porta serial até a execução de uma ação?

Parser ou analisador sintático é o código que analisa a entrada de dados brutos da comunicação serial e executa os seguintes passos:

  • Delimitação: Ele identifica onde uma mensagem começa e termina.
  • Tokenização: Divide a mensagem recebida em partes menores ou variáveis (os tokens).
  • Processamento: Executa os comandos de acordo com os parâmetros recebidos.

Compreender esse fluxo é fundamental para desenvolver protocolos mais robustos e confiáveis.

Fluxo Completo da Mensagem

Considere a seguinte mensagem enviada pelo Monitor Serial:

<LED|1>

Os caracteres chegam ao Arduino um a um. Cada caractere é armazenado temporariamente no buffer de recepção (RX) da comunicação serial.

O loop principal verifica constantemente se existem dados disponíveis:

while (Serial.available() > 0)

Quando existe pelo menos um byte disponível (após o digitar a tecla [enter]), o caractere é lido:

char c = Serial.read();

A partir desse momento, o parser passa a decidir o que fazer com cada caractere recebido.

Execução do Parser - Estados:

Estado 1 — Aguardando Início da Mensagem

recebendo = false;

Nesse estado:

  • O sistema ignora caracteres inválidos.
  • Aguarda o caractere <.
  • Nenhum dado é armazenado.

Exemplo:

abcxyz<LED|1>

Os caracteres: a b c x y z são ignorados.

Somente quando o parser encontra: <

ele muda de estado.

Estado 2 — Recebendo Mensagem

recebendo = true;

Agora cada caractere recebido passa a ser armazenado no buffer:

buffer[indice++] = c;

Exemplo:

LED|1

Ao encontrar: >

a recepção termina e a mensagem é enviada para processamento.

Fluxo Visual do Parser

Aguardando '<'


Recebe '<'


Inicia recepção


Armazena caracteres


Recebe '>'


Finaliza buffer


Executa parsing da mensagem


ACK ou NACK

Esse modelo simples é extremamente eficiente e amplamente utilizado em sistemas embarcados.

Como o Buffer Evolui Durante a Recepção

Suponha a mensagem:

<MOTOR|150>

A evolução do buffer será:

Após receber 

MOTOR|150

Quando o caractere > chega:

buffer[indice] = '\0';

O conteúdo torna-se:

MOTOR|150\0

Agora temos uma string válida para as funções da biblioteca <string.h>, podendo ser realizada a tokenização.

Tratamento de Mensagens Corrompidas

Um protocolo robusto não assume que todas as mensagens serão válidas.

1️⃣ Por exemplo:

<LED>

Neste caso, o parâmetro não existe.

Quando executamos:

char* comando = strtok(buffer, "|");
char* parametro = strtok(NULL, "|");

teremos:

comando   = "LED"
parametro = NULL

A validação detecta o problema:

if (comando == NULL || parametro == NULL)

Resposta:

<NACK|FORMATO_INVALIDO>

2️⃣ Outro exemplo:

<LED|X>

O comando existe, mas o parâmetro é inválido.

Resposta:

<NACK|PARAMETRO_LED>

3️⃣ Outro caso:

<ABC|123>

O formato está correto.

Mas o comando não existe.

Resposta:

<NACK|COMANDO_INVALIDO>

Como o Timeout Aumenta a Robustez

Imagine que a comunicação seja interrompida durante a transmissão:

<LED|1

O caractere > nunca chega.

Sem timeout:

❌ O parser ficaria aguardando indefinidamente.

Com timeout:

if (millis() - tempoInicial > TIMEOUT)

o sistema cancela automaticamente a operação:

<NACK|TIMEOUT>

Isso evita travamentos e mensagens presas no buffer.

Como o Controle de Overflow Protege a Memória

Suponha que alguém envie uma mensagem com mais de 40 caracteres:

<AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>

Se não existisse proteção:

buffer[indice++] = c;

continuaria gravando além do tamanho do array definido.

Isso poderia:

  • Corromper variáveis vizinhas
  • Produzir comportamentos imprevisíveis
  • Travar o Arduino
  • Reiniciar o sistema

Por isso utilizamos:

if (indice < 39)

Quando o limite é ultrapassado:

<NACK|OVERFLOW>

O que este artigo introduz de importante?

Com esse modelo, passamos a implementar conceitos reais de engenharia embarcada:

✔ Comunicação confiável
✔ Parser robusto
✔ Tratamento de falhas
✔ Timeout
✔ ACK/NACK
✔ Proteção contra overflow
✔ Estrutura de protocolo

Esses mecanismos são utilizados em:

  • automação industrial
  • IoT
  • comunicação serial profissional
  • protocolos embarcados
  • sistemas críticos

Por que usar os termos ACK/NACK?

ACK e NACK não são obrigatórios — eles são uma convenção amplamente usada em protocolos de comunicação.

Vantagens:

1️⃣ Padronização: Engenheiros e sistemas embarcados reconhecem imediatamente o significado de ACK (Acknowledgment) e NACK (Negative Acknowledgment).

2️⃣ Baixo consumo de banda: Mensagens curtas ocupam menos bytes na transmissão serial.

3️⃣ Facilidade de automação: Softwares conseguem verificar rapidamente se a resposta começa com ACK ou NACK.

Pode ser mais amigável?

Sim. Em projetos educacionais, interfaces gráficas ou aplicações voltadas ao usuário final, respostas mais descritivas podem ser melhores.

Exemplos:

<SUCESSO|LED_LIGADO>
<ERRO|PARAMETRO_INVALIDO>

Conclusão

Ao longo deste artigo, evoluímos o protocolo de aplicação desenvolvido anteriormente adicionando três elementos fundamentais para a construção de sistemas embarcados mais confiáveis: ACK, NACK e Timeout.

Com o mecanismo de ACK, o Arduino passou a confirmar explicitamente que uma mensagem foi recebida e processada com sucesso. Com o NACK, o sistema tornou-se capaz de informar erros de formato, comandos inválidos, parâmetros incorretos e outras falhas de comunicação. Já o controle de Timeout permitiu detectar mensagens incompletas e evitar que o parser permanecesse aguardando dados indefinidamente.

Além disso, implementamos um parser não bloqueante baseado em char[], proteção contra overflow, tratamento de mensagens corrompidas e um fluxo de processamento mais robusto — características presentes em protocolos utilizados em automação, IoT e sistemas embarcados profissionais.

Este artigo mostrou como projetar uma comunicação serial previsível, segura e preparada para crescer. Os conceitos apresentados aqui formam a base para recursos mais avançados, como checksum, retransmissão automática, máquinas de estados e comunicação entre múltiplos dispositivos.

Dominar ACK, NACK e Timeout é um passo essencial para sair da comunicação serial puramente didática e entrar no universo dos protocolos de comunicação confiáveis utilizados no mundo real.

O anúncio abaixo ajuda a manter o Squids Arduino funcionando

Comentários

×

Infomações do site / SEO








×

Adicionar Marcadores