Framing: bytes sem fronteiras não são mensagens
Depois que você aceita que UART entrega apenas um stream contínuo de bytes, a próxima pergunta inevitável é:
onde começa e onde termina uma mensagem?
Esse é o ponto onde a maioria dos firmwares entra em colapso silencioso. Sem framing explícito, o sistema depende de sorte, alinhamento perfeito e ausência de ruído. Em outras palavras: depende de condições que não existem em sistemas reais.
O erro clássico: confiar em delimitadores “humanos”
Um padrão extremamente comum é tratar UART como se fosse uma linha de texto:
// ❌ Framing frágil baseado em '\n'
char line[64];
if (uart_read_line(line, sizeof(line))) {
if (strcmp(line, "STATUS\n") == 0) {
send_status();
}
}
Esse modelo falha por razões fundamentais:
\né apenas um byte comum, não um marcador confiável- Se um byte for perdido, o framing se perde junto
- Um
\nespúrio no meio do payload quebra tudo - Não há como distinguir lixo de dados válidos
- Não existe recuperação de estado
Pior: quando o framing quebra, todas as mensagens seguintes também quebram, porque o parser não sabe mais onde está.
Framing é uma decisão de protocolo, não de conveniência
Sistemas robustos definem claramente:
- Como uma mensagem começa (Start of Frame – SOF)
- Como ela termina (End of Frame – EOF) ou qual é seu tamanho
- O que acontece se algo der errado no meio
Um framing explícito cria uma propriedade essencial:
o parser pode se realinhar mesmo após erro ou ruído
Abordagem correta 1: marcador de início + comprimento
Um dos modelos mais robustos e usados em sistemas industriais é:
[ SOF ][ LEN ][ PAYLOAD ][ CRC ]
Exemplo de implementação:
#define SOF 0xAA
#define MAX_PAYLOAD 64
typedef enum {
WAIT_SOF,
WAIT_LEN,
WAIT_PAYLOAD,
WAIT_CRC
} parser_state_t;
static parser_state_t state = WAIT_SOF;
static uint8_t length = 0;
static uint8_t payload[MAX_PAYLOAD];
static uint8_t index = 0;
void parser_process_byte(uint8_t byte) {
switch (state) {
case WAIT_SOF:
if (byte == SOF) {
state = WAIT_LEN;
}
break;
case WAIT_LEN:
length = byte;
if (length > MAX_PAYLOAD) {
state = WAIT_SOF; // framing inválido
} else {
index = 0;
state = WAIT_PAYLOAD;
}
break;
case WAIT_PAYLOAD:
payload[index++] = byte;
if (index >= length) {
state = WAIT_CRC;
}
break;
case WAIT_CRC:
if (crc_ok(payload, length, byte)) {
handle_packet(payload, length);
}
state = WAIT_SOF;
break;
}
}
Observe algumas propriedades importantes:
- O parser não trava
- Um erro não compromete os próximos frames
- É possível descartar pacotes inválidos
- O sistema sempre consegue se realinhar ao próximo
SOF
Isso é engenharia de protocolo, não “leitura de serial”.
Abordagem correta 2: delimitador com escape (SLIP-like)
Em sistemas onde o tamanho não é conhecido antecipadamente, usa-se delimitador + escape:
[ END ][ DATA ][ DATA ][ ESC + END ][ DATA ][ END ]
Exemplo simplificado:
#define END 0xC0
#define ESC 0xDB
#define ESC_END 0xDC
void parser_process_byte(uint8_t byte) {
static bool escape = false;
if (byte == END) {
handle_packet(payload, index);
index = 0;
escape = false;
return;
}
if (escape) {
if (byte == ESC_END) {
payload[index++] = END;
}
escape = false;
return;
}
if (byte == ESC) {
escape = true;
return;
}
payload[index++] = byte;
}
Esse modelo é extremamente tolerante a ruído e amplamente usado em sistemas embarcados reais, inclusive sobre UART, rádio e USB CDC.
Framing define se o sistema sobrevive ao erro
Sem framing explícito:
- Um byte perdido corrompe tudo
- O sistema entra em estados inválidos
- Reset vira “estratégia de recuperação”
- Bugs aparecem apenas em campo
Com framing explícito:
- Erros são localizados
- O sistema se recupera sozinho
- Logs fazem sentido
- Atualizações remotas se tornam viáveis
Uma regra prática
Se você não consegue responder claramente:
“Como meu parser se realinha após um erro aleatório no meio do stream?”
Então não existe protocolo, apenas esperança.