
Passo 1 de 9
Defina onde a mensagem termina
Defina um contrato textual mínimo para mensagens UART e use LF como a única fronteira entre mensagens.
Trilha de aprendizado · Nível 11 · Tutorial 6
Ao concluir, o aluno será capaz de receber mensagens UART delimitadas e tratar mensagens incompletas, excessivas ou ausentes sem bloquear indefinidamente o programa.
Defina onde a mensagem termina
Defina um contrato textual mínimo para mensagens UART e use LF como a única fronteira entre mensagens. 2 min
Receba aos poucos, dentro do vetor
Receba bytes UART de forma incremental em um vetor fixo, preservando espaço para o terminador nulo e concluindo a linha somente ao receber LF. 3 min
Valide antes de agir
Diferencie o fim de uma linha da validação completa do formato, para que somente mensagens permitidas possam produzir efeitos. 2 min
Descarte o excesso e recupere a sincronização
Recupere o enquadramento após uma mensagem UART longa demais, descartando seu restante até o LF sem interpretar o sufixo como outro comando. 3 min
Dê um prazo para completar a linha
Use um prazo de montagem para descartar linhas UART que ficam incompletas, sem interromper as demais tarefas do firmware. 3 min
Envie mensagens curtas sem esperar por espaço
Envie mensagens UART curtas inteiras apenas quando o buffer de transmissão tiver espaço suficiente, sem interromper o restante do loop. 2 min
Espere por uma resposta, mas só até o prazo
Implemente uma tentativa UART com envio pendente, prazo de resposta de 10 segundos e resultado definido mesmo sem resposta. 3 min
Não confunda resposta atrasada com sucesso
Evite que respostas antigas, inesperadas, tardias ou parciais sejam confundidas com o sucesso da tentativa atual. 2 min
Teste as falhas e confirme a recuperação
Teste o sketch integrado no Monitor Serial, force falhas de delimitação e prazo e confirme a recuperação sem interromper o firmware. 5 min

Passo 1 de 9
Defina um contrato textual mínimo para mensagens UART e use LF como a única fronteira entre mensagens.
A UART entrega bytes em sequência. Embora cada byte viaje em um quadro UART, esses quadros não informam onde começa ou termina uma mensagem da aplicação.
Por exemplo, os bytes podem chegar como Q, :, 0, 1, \n, R, :, 0, 1, \n. Sem uma regra do protocolo, o firmware vê apenas um fluxo contínuo. Neste tutorial, a regra de fronteira será: LF (\n) termina uma mensagem.
Nas sequências abaixo, \n e \r são representações visíveis dos caracteres LF e CR; eles ocupam um byte no fluxo real.
| Bytes recebidos em sequência | Linhas já completas |
|---|---|
| `Q:01\nR:01\n` | `Q:01` e `R:01` |
| `Q:01` | Nenhuma; linha incompleta |
| `R:02\r\n` | `R:02` |
| `Q:03\r` | Nenhuma; CR não termina |
Use um contrato textual pequeno e restrito:
Q:01\nR:01\nQ significa pedido e R significa resposta. O caractere : ocupa sempre a segunda posição. Os dois últimos caracteres são o identificador da tentativa, sempre com dois algarismos.
A resposta repete o identificador do pedido: R:01 corresponde a Q:01. Durante os ensaios, não reutilize um identificador em outra tentativa. Isso permite distinguir uma resposta antiga da resposta esperada.
O identificador liga pedido e resposta; o LF cria a fronteira entre as linhas.

Os campos têm largura e significado definidos; o LF encerra cada mensagem.
Exemplo
Exemplo de troca:
O Uno envia Q:01\n. O interlocutor responde R:01\n para aquela tentativa.
R:02\n é uma resposta de outro identificador, e Q:1\n não respeita a largura de dois algarismos. Mais adiante, o firmware validará rigorosamente o conteúdo; por enquanto, a regra é reconhecer quando uma linha terminou.
No protocolo, o LF é o único terminador. Muitos monitores seriais enviam CRLF: nesse caso, o CR (\r) é ignorado e o LF (\n) conclui a mensagem.
Q:01\n → linha completa.Q:01\r\n → a mesma linha completa.Q:01\r → ainda incompleta.Q:01 → ainda incompleta.Ignorar CR impede que CRLF seja interpretado como duas mensagens. Um CR isolado nunca fecha a linha.
Dica
Delimitar uma linha não significa, ainda, aceitá-la como comando ou resposta. O LF informa apenas que chegou a fronteira; nos próximos passos, o conteúdo será guardado e validado antes de produzir qualquer efeito.
Relacione cada sequência recebida ao estado de delimitação correto. Considere que as sequências são vistas exatamente como escritas, com \r e \n representando bytes CR e LF.
Toque em um item e depois no par correspondente.

Passo 2 de 9
Receba bytes UART de forma incremental em um vetor fixo, preservando espaço para o terminador nulo e concluindo a linha somente ao receber LF.
A UART pode entregar uma linha em vários momentos. Em vez de esperar os próximos bytes, o loop() lê apenas o que já está disponível, guarda os caracteres de conteúdo e volta às outras tarefas.
Use um vetor de 16 posições: linha[0] até linha[14] recebem no máximo 15 caracteres; linha[15] fica reservado para \0. O índice n informa quantos caracteres de conteúdo já foram armazenados.

O índice nunca pode avançar para a posição reservada ao terminador nulo.
LF ('\n') pertence à mensagem recebida como delimitador, mas não é armazenado como conteúdo. Ao recebê-lo, o firmware escreve \0 no vetor para que a linha possa ser tratada como texto em C.
\0 não é um byte que o protocolo envia para terminar a linha: ele existe apenas na representação local. Já CR ('\r') é ignorado, conforme o contrato do passo anterior.
Trecho para inserir em um sketch; processLine() representa o processamento da linha completa.
char linha[16];
uint8_t n = 0;
const uint8_t MAX_POR_PASSAGEM = 4;
void processLine(const char *texto) {
Serial.print("Linha completa: ");
Serial.println(texto);
}
void receberSerial() {
uint8_t atendidos = 0;
while (Serial.available() > 0 && atendidos < MAX_POR_PASSAGEM) {
char c = (char)Serial.read();
atendidos++;
if (c == '\r') {
continue;
}
if (c == '\n') {
linha[n] = '\0';
processLine(linha); // use a linha antes de reutilizá-la
n = 0;
continue;
}
if (n < sizeof(linha) - 1) {
linha[n] = c;
n++;
}
}
}
void loop() {
receberSerial();
// Outras tarefas do firmware continuam aqui.
}Dica
A condição é verificada antes da escrita: n < sizeof(linha) - 1. Assim, quando n vale 15, não há espaço para outro caractere de conteúdo, mas ainda há espaço para colocar \0 se o próximo byte for LF.
Neste trecho, o limite de quatro bytes é uma escolha de cooperação com o restante do firmware; não é uma exigência da UART. O tratamento correto de uma linha que ultrapassa o limite será definido no próximo passo.
Complete as lacunas para o caso em que os 15 caracteres permitidos já foram armazenados e o próximo byte é LF.
char linha[16];
uint8_t n = 15;
char c = '\n';
if (c == '\n') {
linha[] = ;
processLine(linha);
n = 0;
} else if (c != '\r' && n sizeof(linha) - 1) {
linha[n++] = c;
}
Passo 3 de 9
Diferencie o fim de uma linha da validação completa do formato, para que somente mensagens permitidas possam produzir efeitos.
Receber LF só informa que uma linha terminou. Ainda falta decidir se os bytes formam uma mensagem permitida pelo contrato.
Neste tutorial, o conteúdo de uma mensagem tem exatamente quatro caracteres:
Q:01 — pedidoR:01 — respostaO primeiro caractere só pode ser Q ou R; o segundo é obrigatoriamente :; os dois últimos são algarismos do identificador. O LF chega depois desses quatro caracteres e não faz parte do conteúdo armazenado.
Assim, Q:01 pode seguir para tratamento. Já Q:01X, Q:0, Z:01 e uma linha vazia chegam completos ao LF, mas não são mensagens válidas.

O LF cria uma candidata a mensagem; a validação decide se ela pertence ao protocolo.
Dica
A rotina de recepção já conhece a quantidade de caracteres armazenados em n. Passe esse valor junto com a linha completa para a validação.
Não use apenas strlen() para decidir o tamanho: um byte \0 recebido como dado poderia encerrar a leitura textual antes da inspeção. O tamanho registrado permite examinar todos os bytes recebidos antes do LF.
A validação confere primeiro o tamanho exato e só então acessa os campos fixos. Depois, verifica o tipo, o separador e cada algarismo do identificador.
Nada deve ser executado enquanto os caracteres ainda chegam. Somente a rotina chamada após o LF recebe uma linha candidata.
Exemplos de conteúdo já concluído por LF. O LF não aparece na coluna de conteúdo.
| Conteúdo recebido | Resultado | Motivo |
|---|---|---|
| Q:01 | Aceita | Tipo, separador e 2 dígitos corretos |
| R:99 | Aceita | Formato completo permitido |
| Q:0 | Rejeita | Falta um dígito |
| Q:01X | Rejeita | Há um sufixo extra |
| Z:01 | Rejeita | Tipo desconhecido |
| Q:\0 | Rejeita | Byte de controle no ID |
| Rejeita | Linha vazia não é mensagem |
Troque a chamada feita quando chega LF por processarLinhaCompleta(linha, n). A função abaixo usa o tamanho recebido, não strlen(), e só produz efeito depois de uma validação completa.
Uma linha inválida é ignorada: ela não executa comando, não vira pedido e não representa resposta válida.
Trecho para chamar somente quando o receptor encontrar LF. Ele mantém o contrato Q:01 e R:01.
bool formatoValido(const char *linha, uint8_t n) {
return n == 4 &&
(linha[0] == 'Q' || linha[0] == 'R') &&
linha[1] == ':' &&
linha[2] >= '0' && linha[2] <= '9' &&
linha[3] >= '0' && linha[3] <= '9';
}
void processarLinhaCompleta(const char *linha, uint8_t n) {
if (!formatoValido(linha, n)) {
return; // Linha vazia, desconhecida ou malformada: sem efeito.
}
if (linha[0] == 'Q') {
Serial.println(F("Pedido reconhecido"));
} else {
Serial.println(F("Resposta reconhecida"));
}
}
Exemplo
No receptor do step anterior, a chamada no caso de LF passa a ser:
processarLinhaCompleta(linha, n);
Faça essa chamada antes de zerar n. O \0 local ainda pode ser escrito para exibir a linha, mas a decisão de validade usa n e as posições do vetor.
Complete a condição inteira que aceita somente um pedido ou resposta com os quatro bytes previstos. Ela deve rejeitar tanto Q:01X quanto Q:\0, mesmo que o começo pareça correto.
Preencha os elementos que tornam o formato estrito.
bool formatoValido(const char *linha, uint8_t n) {
return n == &&
(linha[0] == 'Q' || linha[0] == 'R') &&
linha[1] == &&
linha[2] >= '0' && linha[2] <= &&
linha[3] >= '0' && linha[3] <= '9';
}
Passo 4 de 9
Recupere o enquadramento após uma mensagem UART longa demais, descartando seu restante até o LF sem interpretar o sufixo como outro comando.
O vetor de 16 posições aceita no máximo 15 caracteres de conteúdo; a última posição é reservada para o terminador local \0. Se chegar mais um caractere de conteúdo antes do LF, a linha inteira é inválida.
Não processe os 15 primeiros caracteres como se fossem uma mensagem. Eles podem ser apenas o prefixo de uma linha excessiva. Em vez disso, entre no estado DESCARTANDO: pare de gravar no vetor e ignore tudo até encontrar o próximo LF.

Depois do excesso, apenas o LF conhecido separa a linha inválida da próxima linha.
Dica
O descarte não tenta corrigir uma mensagem longa nem aproveitar o seu final. Todos os bytes até o próximo LF ainda pertencem à mesma linha inválida.
O CR continua sendo ignorado. Somente LF fecha a linha e, durante o descarte, autoriza o retorno à recepção normal.
Trecho para integrar ao receptor incremental dos steps anteriores. Chame receberByte(Serial.read()) para cada byte disponível. A função de processamento recebe o vetor e o comprimento explícito, preservando a validação estrita do step anterior.
enum EstadoRx { RECEBENDO, DESCARTANDO };
char linha[16];
byte indice = 0;
EstadoRx estadoRx = RECEBENDO;
void receberByte(int recebido) {
char c = (char)recebido;
if (estadoRx == DESCARTANDO) {
if (c == '\n') {
indice = 0;
estadoRx = RECEBENDO;
}
return; // nunca armazena trecho de linha inválida
}
if (c == '\r') {
return; // CR é ignorado
}
if (c == '\n') {
linha[indice] = '\0';
processarLinhaCompleta(linha, indice);
indice = 0;
return;
}
if (indice < sizeof(linha) - 1) {
linha[indice++] = c;
} else {
indice = 0; // invalida o prefixo armazenado
estadoRx = DESCARTANDO;
}
}A comparação de limite ocorre antes da escrita, portanto nenhuma posição além de linha[15] é acessada.
Zerar indice ao detectar o excesso só invalida a cópia local. Isso não permite iniciar outra mensagem imediatamente: DESCARTANDO impede que o sufixo da linha longa seja tratado como um novo comando. Quando chega LF, processarLinhaCompleta() não é chamada para a linha excessiva; a próxima linha começa do zero.
Dica
A chamada mantém o comprimento explícito: processarLinhaCompleta(linha, indice). Esse valor é necessário para a validação conferir os quatro bytes de Q:01 ou R:01 sem depender apenas de texto terminado em nulo.
Complete as lacunas que protegem o vetor e restabelecem a recepção somente após o delimitador. O caractere que excede o limite não é armazenado; a linha longa inteira não chega à validação.
Preencha a condição de espaço e os estados da máquina de recepção.
if () {
linha[indice++] = c;
} else {
indice = 0;
estadoRx = ;
}
if (estadoRx == DESCARTANDO && c == '\n') {
indice = 0;
estadoRx = ;
}Considere uma linha que já ocupou os 15 caracteres permitidos. Chega um 16º caractere, depois mais bytes, depois LF. Em seguida, chega Q:01\n, que cabe no vetor e respeita o formato do protocolo. Ordene os eventos.
Coloque os eventos na ordem em que ocorrem.
Atenção
Excesso no vetor da aplicação não é o mesmo que perda de bytes no buffer interno da UART. Aqui, o firmware ainda leu os bytes e decidiu ignorá-los até LF. Esse mecanismo não recupera bytes que tenham sido perdidos antes de Serial.read().
Portanto, a ressincronização recupera uma fronteira conhecida do protocolo; ela não reconstrói dados ausentes.

Passo 5 de 9
Use um prazo de montagem para descartar linhas UART que ficam incompletas, sem interromper as demais tarefas do firmware.
Uma linha em montagem não pode ocupar o receptor para sempre. Use um timeout de montagem de 3 segundos, contado a partir do primeiro caractere de conteúdo.
A marca de início é registrada uma única vez. Cada byte posterior apenas completa a mesma linha: ele não reinicia o prazo. O caractere CR ('\r') continua ignorado e também não inicia nem prolonga a contagem.
Assim, enviar Q:01 muito lentamente por mais de 3 segundos não torna a mensagem aceitável só porque o LF chegou no final. Os 3 segundos facilitam os ensaios manuais no Monitor Serial; não são um valor universal para todo protocolo.

O relógio começa no primeiro caractere de conteúdo e permanece correndo até a linha terminar ou expirar.
Verifique o vencimento em todas as passagens pelo loop(), inclusive quando Serial.available() é zero. Não use delay() nem espere em um laço pela chegada do próximo byte.
Quando o prazo vence, o conteúdo parcial é invalidado: zere indice, desative linhaAtiva e entre em DESCARTANDO. Nesse estado, os bytes são consumidos sem armazenamento até chegar LF. Esse LF recupera a fronteira entre linhas, mas não valida a mensagem que expirou.
A política para linha vazia continua a mesma dos steps anteriores: quando LF chega durante a recepção normal, processarLinhaCompleta(linha, indice) é chamado mesmo se indice for zero. A validação então rejeita a linha vazia sem efeito.
Acrescente o timeout ao receptor incremental dos steps anteriores. Mantenha o comprimento explícito ao entregar uma linha completa para a validação.
const unsigned long PRAZO_LINHA_MS = 3000UL;
bool linhaAtiva = false;
unsigned long inicioLinha;
void verificarTimeoutMontagem(unsigned long agora) {
if (estadoRx == RECEBENDO && linhaAtiva &&
(unsigned long)(agora - inicioLinha) >= PRAZO_LINHA_MS) {
indice = 0; // invalida o conteúdo parcial
linhaAtiva = false;
estadoRx = DESCARTANDO; // consome bytes até LF
}
}
void receberByte(int recebido) {
char c = (char)recebido;
if (estadoRx == DESCARTANDO) {
if (c == '\n') {
indice = 0;
estadoRx = RECEBENDO;
}
return;
}
if (c == '\r') return; // CR não inicia nem prolonga prazo
if (c == '\n') {
linha[indice] = '\0';
processarLinhaCompleta(linha, indice); // inclusive se indice for 0
indice = 0;
linhaAtiva = false;
return;
}
if (!linhaAtiva) {
linhaAtiva = true;
inicioLinha = millis(); // somente o primeiro conteúdo marca o início
}
if (indice < sizeof(linha) - 1) {
linha[indice++] = c;
} else {
indice = 0;
linhaAtiva = false;
estadoRx = DESCARTANDO; // excesso também descarta até LF
}
}
void loop() {
verificarTimeoutMontagem(millis());
uint8_t atendidos = 0;
while (Serial.available() > 0 && atendidos < MAX_POR_PASSAGEM) {
receberByte(Serial.read());
atendidos++;
}
// Outras tarefas do firmware continuam aqui.
}
Dica
Se a linha expirar e nenhum LF chegar, o firmware permanece em DESCARTANDO, mas continua executando o restante do loop(). Um LF explícito, mesmo muito depois, encerra esse descarte e permite começar uma nova linha. Não há espera ativa nem tentativa de aproveitar o conteúdo vencido.
Complete o limite da condição que detecta o timeout de uma linha em montagem.
if (estadoRx == RECEBENDO && linhaAtiva &&
(unsigned long)(agora - inicioLinha) >= ) {
indice = 0;
linhaAtiva = false;
estadoRx = DESCARTANDO;
}O primeiro caractere de uma linha chega. Depois, o Monitor Serial não envia mais nada por mais de 3 segundos. Por fim, chega um LF. O que o receptor deve fazer?

Passo 6 de 9
Envie mensagens UART curtas inteiras apenas quando o buffer de transmissão tiver espaço suficiente, sem interromper o restante do loop.
A saída da UART também usa um buffer finito. Antes de colocar uma mensagem curta nele, consulte o espaço livre com Serial.availableForWrite().
A regra é enviar a mensagem inteira somente quando houver espaço para todos os bytes dela. Se faltar espaço, não espere em um laço e não envie apenas o começo: mantenha uma única saída pendente e tente novamente na próxima passagem pelo loop().
Assim, enquanto a UART libera espaço, o firmware continua atendendo a recepção incremental, os timeouts e as demais tarefas.

Compare o espaço livre com todos os bytes da mensagem, incluindo o LF.
A resposta do contrato deste tutorial é R:01\n. O LF faz parte do envio; o terminador nulo serve somente para representar a string na memória do C/C++.
| Elemento | Quantidade | Vai pela UART? |
|---|---|---|
| `R`, `:`, `0`, `1` | 4 bytes | Sim |
| LF (`\n`) | 1 byte | Sim |
| Nulo (`\0`) | 1 byte local | Não |
| Total exigido no buffer | 5 bytes | Sim |
Mantenha uma única mensagem pendente e chame atenderSaida() pelo fluxo principal a cada passagem do loop(). A string usa a mesma resposta R:01\n definida no protocolo.
const char resposta[] = "R:01\n";
const size_t bytesResposta = sizeof(resposta) - 1;
bool respostaPendente = true;
void atenderSaida() {
if (!respostaPendente) {
return;
}
if (Serial.availableForWrite() < bytesResposta) {
return; // adia; não espera e não envia um prefixo
}
Serial.write((const uint8_t *)resposta, bytesResposta);
respostaPendente = false;
}
void loop() {
atenderSaida();
// Atendimento incremental da recepção e das demais tarefas.
}Atenção
Mantenha a consulta e a escrita no fluxo principal, com um único responsável por chamar Serial.write(). Outra escrita entre a consulta e o envio poderia ocupar o espaço que acabava de ser medido.
Quando Serial.write() aceita os bytes, eles foram colocados no buffer de transmissão. Isso não comprova que já saíram fisicamente pela UART nem que o interlocutor os recebeu. Não use Serial.flush() como solução: ele aguarda a transmissão terminar e pode bloquear o programa.
Complete a condição que preserva a mensagem inteira. Se não houver espaço para os cinco bytes de R:01\n, a função retorna e deixa respostaPendente verdadeira para uma passagem futura do loop().
Preencha as duas chamadas da decisão de envio.
if (Serial.() < bytesResposta) {
return;
}
Serial.((const uint8_t *)resposta, bytesResposta);
respostaPendente = false;
Passo 7 de 9
Implemente uma tentativa UART com envio pendente, prazo de resposta de 10 segundos e resultado definido mesmo sem resposta.
Uma tentativa usa um único pedido e tem três estados:
LIVRE: não há tentativa ativa.ENVIO_PENDENTE: o pedido foi preparado e entrou na única fila de saída, mas ainda não foi aceito pelo buffer de transmissão.AGUARDANDO: os cinco bytes de Q:nn\n já foram aceitos pelo buffer; agora existe uma resposta pendente.O resultado final é SUCESSO ou SEM_RESPOSTA, e então o estado volta para LIVRE.
No Monitor Serial, Q:01 é um comando local de teste: ele solicita uma tentativa. O Uno transmite Q:01\n; depois, você digita R:01\n como interlocutor simulado. Use um identificador ainda não reutilizado em cada ensaio.

O relógio da resposta começa quando o pedido inteiro entra no buffer de transmissão, não quando o comando é digitado.
Há dois prazos independentes. Eles resolvem problemas diferentes e não devem ser misturados.
| Prazo | Começa quando | Ao vencer |
|---|---|---|
| Montagem: 3 s | Chega o primeiro caractere de conteúdo | Invalida a linha parcial e descarta até LF |
| Resposta: 10 s | O pedido inteiro entra no buffer de transmissão | Encerra a tentativa como sem resposta |
O prazo de resposta começa somente após Serial.write() aceitar todos os cinco bytes de Q:nn\n: Q, :, dois algarismos e LF. Enquanto a fila ainda não puder aceitar o pedido, ou enquanto o buffer UART não tiver espaço, a tentativa não passa a aguardar resposta.
O instante registrado inclui o tempo de transmissão física que ainda falta e o tempo para o interlocutor responder. Colocar bytes no buffer não prova que eles já chegaram ao outro lado.
Este bloco substitui a saída fixa do passo anterior por uma única fila que também identifica o tipo da mensagem. Ele pressupõe que formatoValido() e a recepção incremental já existem.
enum EstadoTentativa { LIVRE, ENVIO_PENDENTE, AGUARDANDO };
enum ResultadoTentativa { SEM_RESULTADO, SUCESSO, SEM_RESPOSTA };
enum TipoSaida { SAIDA_NENHUMA, SAIDA_PEDIDO };
const unsigned long PRAZO_RESPOSTA_MS = 10000UL;
char saida[16];
byte bytesSaida = 0;
bool saidaPendente = false;
TipoSaida tipoSaida = SAIDA_NENHUMA;
EstadoTentativa estadoTentativa = LIVRE;
ResultadoTentativa resultadoTentativa = SEM_RESULTADO;
unsigned long inicioResposta;
char pedido[6]; // Q:nn\n e \0
char idEsperado[2]; // os dois algarismos
bool enfileirar(const char *texto, TipoSaida tipo) {
if (saidaPendente) return false;
byte n = strlen(texto);
if (n >= sizeof(saida)) return false;
memcpy(saida, texto, n + 1);
bytesSaida = n;
tipoSaida = tipo;
saidaPendente = true;
return true;
}
void iniciarTentativa(const char *texto) {
if (estadoTentativa != LIVRE) return;
pedido[0] = 'Q'; pedido[1] = ':';
pedido[2] = texto[2]; pedido[3] = texto[3];
pedido[4] = '\n'; pedido[5] = '\0';
// Sem espaço na única fila: não cria tentativa nem prazo fictício.
if (!enfileirar(pedido, SAIDA_PEDIDO)) return;
idEsperado[0] = texto[2];
idEsperado[1] = texto[3];
resultadoTentativa = SEM_RESULTADO;
estadoTentativa = ENVIO_PENDENTE;
}
void atenderSaida() {
if (!saidaPendente) return;
if (Serial.availableForWrite() < bytesSaida) return;
TipoSaida enviada = tipoSaida;
Serial.write((const uint8_t *)saida, bytesSaida);
saidaPendente = false;
tipoSaida = SAIDA_NENHUMA;
if (enviada == SAIDA_PEDIDO &&
estadoTentativa == ENVIO_PENDENTE) {
inicioResposta = millis();
estadoTentativa = AGUARDANDO;
}
}Dica
Caracteres parciais, linhas inválidas ou qualquer outro dado recebido não reiniciam o prazo de resposta. Esses eventos pertencem ao receptor; eles não confirmam que o interlocutor respondeu ao pedido atual.
Verifique o timeout de resposta em toda passagem pelo loop(), mesmo quando não chegou byte algum. Ao vencer, a tentativa recebe SEM_RESPOSTA e volta a LIVRE.
Uma linha só gera sucesso se já estiver completa e válida, a tentativa estiver em AGUARDANDO e os dois algarismos coincidirem com idEsperado. A recepção parcial continua sujeita ao timeout de montagem de 3 segundos implementado antes.
Use estas funções junto à fila de saída e ao receptor já construído. tratarLinhaCompleta() deve ser chamado somente depois do LF, com o comprimento explícito da linha.
void verificarTimeoutResposta(unsigned long agora) {
if (estadoTentativa == AGUARDANDO &&
(unsigned long)(agora - inicioResposta) >= PRAZO_RESPOSTA_MS) {
resultadoTentativa = SEM_RESPOSTA;
estadoTentativa = LIVRE;
}
}
void tratarLinhaCompleta(const char *texto, byte n) {
verificarTimeoutResposta(millis());
if (!formatoValido(texto, n)) return;
if (texto[0] == 'Q') {
iniciarTentativa(texto);
return;
}
if (texto[0] == 'R' &&
estadoTentativa == AGUARDANDO &&
texto[2] == idEsperado[0] &&
texto[3] == idEsperado[1]) {
resultadoTentativa = SUCESSO;
estadoTentativa = LIVRE;
}
}
void loop() {
verificarTimeoutResposta(millis());
receberSerial(); // recepção incremental dos passos anteriores
atenderSaida(); // pode iniciar AGUARDANDO após a escrita
// As demais tarefas continuam aqui.
}Dica
O timeout de montagem e o timeout de resposta são independentes. Se uma linha parcial expirar, o receptor invalida essa linha e descarta bytes até LF para recuperar a fronteira. A tentativa que já aguarda resposta não recebe sucesso por isso: ela só termina por uma resposta correspondente ou pelo próprio prazo de 10 segundos.
Complete as transições que encerram uma tentativa. A primeira condição representa ausência de resposta após 10 segundos. A segunda registra sucesso apenas para uma resposta completa, válida e com o identificador esperado.
Preencha os estados e resultados das transições finais.
void verificarTimeoutResposta(unsigned long agora) {
if (estadoTentativa == &&
(unsigned long)(agora - inicioResposta) >= PRAZO_RESPOSTA_MS) {
resultadoTentativa = ;
estadoTentativa = LIVRE;
}
}
if (linha[0] == 'R' &&
estadoTentativa == AGUARDANDO &&
linha[2] == idEsperado[0] && linha[3] == idEsperado[1]) {
resultadoTentativa = ;
estadoTentativa = ;
}
Passo 8 de 9
Evite que respostas antigas, inesperadas, tardias ou parciais sejam confundidas com o sucesso da tentativa atual.
Uma resposta só representa sucesso quando três condições coincidem: existe uma tentativa em AGUARDANDO, os dois algarismos são iguais a idEsperado e o prazo de 10 segundos ainda não venceu.
Exemplo: se o Uno enviou Q:08\n, apenas R:08\n recebido a tempo encerra a tentativa com SUCESSO. Uma antiga R:07\n tem formato válido, mas é inesperada para a tentativa 08.
Use identificadores diferentes nos ensaios. Isso ajuda a rejeitar uma resposta antiga; não é uma garantia para uma sessão longa em que os identificadores sejam reutilizados.

O formato da resposta não basta: o contexto da tentativa decide seu significado.
Considere que Q:08\n já entrou inteiro no buffer de transmissão e a tentativa está em AGUARDANDO. O prazo de resposta foi iniciado nesse momento.
| Evento | Classificação | Efeito |
|---|---|---|
| `R:08\n` antes do prazo | Resposta esperada | Registra sucesso e encerra |
| `R:07\n` durante espera por 08 | Inesperada | Mantém a espera de 08 |
| Linha inválida ou desconhecida | Rejeitada | Mantém a espera de 08 |
| Nenhum byte recebido | Sem evento | Prazo continua correndo |
| Prazo vencido | Sem resposta | Encerra a tentativa |
| `R:08\n` após vencimento | Sem tentativa pendente | Não reabre o resultado |
Dica
Cheque o vencimento antes de aceitar uma resposta. Assim, se o prazo já venceu quando uma linha R:08 é atendida, a tentativa termina como SEM_RESPOSTA; a linha não pode restaurar o estado AGUARDANDO nem produzir sucesso.
Depois de encerrada, uma resposta recebida sem tentativa pendente não muda resultadoTentativa. Ela pode ser considerada inesperada para fins de observação, mas não é uma confirmação tardia.
Substitua as funções de timeout e de tratamento de linha do step anterior por estas versões. Elas preservam EstadoTentativa, ResultadoTentativa, estadoTentativa, resultadoTentativa, inicioResposta, idEsperado e PRAZO_RESPOSTA_MS.
void encerrarSemResposta() {
if (estadoTentativa != AGUARDANDO) return;
resultadoTentativa = SEM_RESPOSTA; // registro de um evento
estadoTentativa = LIVRE;
// Se havia uma linha ainda em montagem, ela não pode atravessar
// a fronteira entre tentativas.
if (estadoRx == RECEBENDO && linhaAtiva) {
indice = 0;
linhaAtiva = false;
estadoRx = DESCARTANDO;
}
}
void verificarTimeoutResposta(unsigned long agora) {
if (estadoTentativa == AGUARDANDO &&
(unsigned long)(agora - inicioResposta) >= PRAZO_RESPOSTA_MS) {
encerrarSemResposta();
}
}
void tratarLinhaValida(const char *linha, byte n) {
verificarTimeoutResposta(millis()); // antes de aceitar sucesso
if (!formatoValido(linha, n)) return;
if (linha[0] == 'Q') {
iniciarTentativa(linha);
return;
}
if (linha[0] != 'R' || estadoTentativa != AGUARDANDO) {
return; // resposta sem tentativa pendente: não muda resultado
}
if (linha[2] != idEsperado[0] || linha[3] != idEsperado[1]) {
return; // resposta inesperada: não reinicia o prazo
}
resultadoTentativa = SUCESSO; // registro de um único evento
estadoTentativa = LIVRE;
}No ramo que trata LF do receptor, faça a limpeza do estado de montagem antes de chamar tratarLinhaValida(). Isso permite que um timeout de resposta detectado nessa função diferencie uma linha completa de uma linha ainda parcial.
if (c == '\n') {
linha[indice] = '\0';
byte nCompleta = indice;
indice = 0;
linhaAtiva = false;
tratarLinhaValida(linha, nCompleta);
return;
}Chame verificarTimeoutResposta(millis()) em toda passagem pelo loop(), como no step anterior. Se ela vencer enquanto linhaAtiva for verdadeira, encerrarSemResposta() limpa o conteúdo parcial e coloca o receptor em DESCARTANDO. Os bytes restantes são consumidos até LF, sem serem interpretados como a próxima mensagem.
resultadoTentativa é alterado somente em transições: para SUCESSO ou SEM_RESPOSTA. Não envie continuamente textos como “aguardando” pelo loop(): mensagens repetidas podem ocupar o buffer de transmissão. Se você decidir exibir resultados, faça uma única emissão quando esse resultado mudar.
Relacione cada evento ao tratamento correto. Em todos os casos, a tentativa 08 já havia sido iniciada por Q:08\n.
Toque em um item e depois no par correspondente.

Passo 9 de 9
Teste o sketch integrado no Monitor Serial, force falhas de delimitação e prazo e confirme a recuperação sem interromper o firmware.
Use somente o Arduino Uno R3 conectado por USB ao computador. Não conecte dispositivos externos aos pinos 0 e 1: a ponte USB–UART e o Monitor Serial serão o interlocutor.
Os três blocos a seguir compõem um único sketch, na ordem apresentada. O comando local Q:nn inicia uma tentativa; o Uno transmite Q:nn\n. Somente R:nn\n, com o mesmo identificador e antes de 10 s, registra sucesso.
O LED integrado pisca enquanto há uma linha em montagem, descarte, envio pendente ou espera por resposta. Isso oferece uma evidência visual de que o loop() continua sendo executado.

Nesta prática, o computador simula o interlocutor UART. Os pinos 0 e 1 permanecem sem conexões externas.
Bloco 1 de 3 — coloque no início de um arquivo novo na IDE. A fila de saída guarda também o tipo da mensagem: somente uma saída marcada como SAIDA_PEDIDO pode iniciar o prazo de resposta.
enum EstadoRx { RECEBENDO, DESCARTANDO };
enum EstadoTentativa { LIVRE, ENVIO_PENDENTE, AGUARDANDO };
enum ResultadoTentativa { SEM_RESULTADO, SUCESSO, SEM_RESPOSTA };
enum TipoSaida { SAIDA_NENHUMA, SAIDA_DIAGNOSTICO, SAIDA_PEDIDO };
const byte MAX_POR_PASSAGEM = 4;
const unsigned long PRAZO_LINHA_MS = 3000UL;
const unsigned long PRAZO_RESPOSTA_MS = 10000UL;
char linha[16];
byte indice = 0;
EstadoRx estadoRx = RECEBENDO;
bool linhaAtiva = false;
unsigned long inicioLinha;
char saida[16];
byte bytesSaida = 0;
bool saidaPendente = false;
TipoSaida tipoSaida = SAIDA_NENHUMA;
EstadoTentativa estadoTentativa = LIVRE;
ResultadoTentativa resultadoTentativa = SEM_RESULTADO;
unsigned long inicioResposta;
char pedido[6];
char idEsperado[2];
bool enfileirar(const char *texto, TipoSaida tipo) {
if (saidaPendente) return false;
byte n = strlen(texto);
if (n >= sizeof(saida)) return false;
memcpy(saida, texto, n + 1);
bytesSaida = n;
tipoSaida = tipo;
saidaPendente = true;
return true;
}
void diagnosticar(const char *texto) {
enfileirar(texto, SAIDA_DIAGNOSTICO);
}
bool formatoValido(const char *texto, byte n) {
return n == 4 &&
(texto[0] == 'Q' || texto[0] == 'R') &&
texto[1] == ':' &&
texto[2] >= '0' && texto[2] <= '9' &&
texto[3] >= '0' && texto[3] <= '9';
}
Dica
O comando que inicia a tentativa só muda o estado para ENVIO_PENDENTE se enfileirar() tiver aceitado o pedido. Além disso, a rotina de transmissão guarda o tipo que foi enviado e inicia inicioResposta apenas quando transmite aquele pedido. Uma mensagem de diagnóstico nunca pode iniciar esse relógio.
Bloco 2 de 3 — continue imediatamente após o bloco anterior.
void iniciarTentativa(const char *texto) {
if (estadoTentativa != LIVRE) return;
pedido[0] = 'Q'; pedido[1] = ':';
pedido[2] = texto[2]; pedido[3] = texto[3];
pedido[4] = '\n'; pedido[5] = '\0';
idEsperado[0] = texto[2];
idEsperado[1] = texto[3];
// Sem espaço na única fila: não cria tentativa nem prazo fictício.
if (!enfileirar(pedido, SAIDA_PEDIDO)) return;
resultadoTentativa = SEM_RESULTADO;
estadoTentativa = ENVIO_PENDENTE;
}
void encerrarSemResposta() {
if (estadoTentativa != AGUARDANDO) return;
resultadoTentativa = SEM_RESPOSTA;
estadoTentativa = LIVRE;
diagnosticar("SEM RESPOSTA\n");
if (estadoRx == RECEBENDO && linhaAtiva) {
indice = 0;
linhaAtiva = false;
estadoRx = DESCARTANDO;
}
}
void verificarTimeouts(unsigned long agora) {
if (estadoRx == RECEBENDO && linhaAtiva &&
(unsigned long)(agora - inicioLinha) >= PRAZO_LINHA_MS) {
indice = 0;
linhaAtiva = false;
estadoRx = DESCARTANDO;
diagnosticar("LINHA EXPIROU\n");
}
if (estadoTentativa == AGUARDANDO &&
(unsigned long)(agora - inicioResposta) >= PRAZO_RESPOSTA_MS) {
encerrarSemResposta();
}
}
void tratarLinhaCompleta(const char *texto, byte n) {
verificarTimeouts(millis()); // vencimento tem precedência
if (!formatoValido(texto, n)) {
diagnosticar("INVALIDA\n");
return;
}
if (texto[0] == 'Q') {
iniciarTentativa(texto);
return;
}
if (estadoTentativa != AGUARDANDO ||
texto[2] != idEsperado[0] || texto[3] != idEsperado[1]) {
diagnosticar("INESPERADA\n");
return;
}
resultadoTentativa = SUCESSO;
estadoTentativa = LIVRE;
diagnosticar("SUCESSO\n");
}
void receberByte(int recebido) {
char c = (char)recebido;
if (estadoRx == DESCARTANDO) {
if (c == '\n') {
indice = 0;
linhaAtiva = false;
estadoRx = RECEBENDO;
}
return;
}
if (c == '\r') return;
if (c == '\n') {
linha[indice] = '\0';
byte nCompleta = indice;
indice = 0;
linhaAtiva = false;
tratarLinhaCompleta(linha, nCompleta);
return;
}
if (!linhaAtiva) {
linhaAtiva = true;
inicioLinha = millis();
}
if (indice < sizeof(linha) - 1) {
linha[indice++] = c;
} else {
indice = 0;
linhaAtiva = false;
estadoRx = DESCARTANDO;
diagnosticar("LINHA LONGA\n");
}
}
Bloco 3 de 3 — finalize o mesmo arquivo, grave no Uno e abra o Monitor Serial em 9600 baud.
void receberSerial() {
byte atendidos = 0;
while (Serial.available() > 0 && atendidos < MAX_POR_PASSAGEM) {
verificarTimeouts(millis());
receberByte(Serial.read());
atendidos++;
}
}
void atenderSaida() {
if (!saidaPendente) return;
if (Serial.availableForWrite() < bytesSaida) return;
TipoSaida enviada = tipoSaida;
Serial.write((const uint8_t *)saida, bytesSaida);
saidaPendente = false;
tipoSaida = SAIDA_NENHUMA;
// Só o pedido efetivamente aceito pelo buffer inicia a espera.
if (enviada == SAIDA_PEDIDO && estadoTentativa == ENVIO_PENDENTE) {
inicioResposta = millis();
estadoTentativa = AGUARDANDO;
}
}
void atualizarLed() {
static unsigned long ultimoLed = 0;
static bool nivelLed = false;
bool ativo = linhaAtiva || estadoRx == DESCARTANDO ||
estadoTentativa == ENVIO_PENDENTE ||
estadoTentativa == AGUARDANDO;
if (!ativo) {
nivelLed = false;
digitalWrite(LED_BUILTIN, LOW);
return;
}
if ((unsigned long)(millis() - ultimoLed) >= 150UL) {
ultimoLed = millis();
nivelLed = !nivelLed;
digitalWrite(LED_BUILTIN, nivelLed);
}
}
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(9600);
}
void loop() {
verificarTimeouts(millis());
receberSerial();
verificarTimeouts(millis());
atenderSaida();
atualizarLed();
}
Q:01 SUCESSO LINHA EXPIROU SEM RESPOSTA
A saída é representativa. Os textos dependem dos ensaios enviados e os timeouts dependem da espera real de 3 s ou 10 s.
Abra o Monitor Serial em 9600 baud. Para os casos usuais, selecione Nova linha (LF). No ensaio CRLF, selecione CR e LF. Aguarde cada resultado final antes de começar a próxima tentativa.
No teste parcial, há dois eventos independentes: após 3 s, a linha parcial expira e o receptor entra em descarte; o LF posterior apenas recupera a fronteira. A tentativa iniciada antes continua aguardando até receber a resposta correta ou vencer o prazo de 10 s.
Use um identificador novo para cada tentativa iniciada. As linhas de resposta só têm efeito quando correspondem a uma tentativa pendente.
| Ensaio | Ação no Monitor Serial | Resultado e recuperação esperados |
|---|---|---|
| Válido com LF | Com LF, envie `Q:01`. Ao aparecer `Q:01`, envie `R:01`. | Aparece `SUCESSO`; o LED para de piscar. |
| Válido com CRLF | Selecione CR e LF. Envie `Q:02`; ao aparecer o pedido, envie `R:02`. | Aparece `SUCESSO`. O CR não cria uma linha extra. |
| Conteúdo inválido | Volte para LF e envie `X:01`. | Aparece `INVALIDA`. Nenhuma tentativa é criada. |
| Linha excessiva | Envie `1234567890123456` com LF. Depois, envie `Q:03` e responda `R:03`. | Aparece `LINHA LONGA`; o LF recupera o receptor. A tentativa 03 termina em `SUCESSO`. |
| Parcial e timeout | Envie `Q:04`; ao aparecer o pedido, envie `R:04` com Sem final de linha. Espere 3 s e envie somente LF. Depois aguarde. | Aparece `LINHA EXPIROU`. O LF ressincroniza o receptor; posteriormente aparece `SEM RESPOSTA` para a tentativa 04. |
| Sem resposta | Envie `Q:05` e não envie resposta. | Após cerca de 10 s aparece `SEM RESPOSTA`; o LED para. |
| Resposta tardia | Depois de `SEM RESPOSTA` da 05, envie `R:05` com LF. | Aparece `INESPERADA`; a tentativa encerrada não é reaberta. |
| ID diferente | Envie `Q:06`; ao aparecer o pedido, envie `R:07`. Aguarde o prazo. | Aparece `INESPERADA`; depois aparece `SEM RESPOSTA`. O ID errado não renova o prazo. |
Complete a condição que impede uma saída de diagnóstico de iniciar o prazo de resposta. O instante só deve ser registrado quando a mensagem transmitida for o pedido da tentativa que está em ENVIO_PENDENTE.
Preencha a transição que ocorre depois de Serial.write().
TipoSaida enviada = tipoSaida;
Serial.write((const uint8_t *)saida, bytesSaida);
saidaPendente = false;
tipoSaida = SAIDA_NENHUMA;
if (enviada == &&
estadoTentativa == ) {
inicioResposta = ;
estadoTentativa = ;
}Depois de executar ao menos um caso válido, uma linha longa ou inválida e o ensaio parcial, relate as evidências observadas. Diferencie claramente o LF que recupera o receptor do evento que encerra a tentativa.
Escreva pelo menos 120 caracteres (0/120).
Resumo
Q:nn ou R:nn.Parabéns! Você concluiu: Mensagens UART com delimitação e timeout
Milhares de cursos online em vídeo, ebooks e áudiobooks.
Para testar seus conhecimentos no decorrer dos cursos online
Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail
Baixe nosso aplicativo pelo QR Code ou pelos links abaixo:.
+ de 10 milhões
de alunos
Certificado grátis e
válido em todo o Brasil
60 mil exercícios
gratuitos
4,8/5 classificação
nas lojas de apps
Cursos gratuitos em
vídeo, ebooks e audiobooks