Recuperação, perda e por que sistemas reais não assumem perfeição
Até aqui, já tratamos UART como ela realmente é: um canal bruto, sem garantias, que exige bufferização, framing e validação explícita. Falta agora a pergunta mais importante de todas:
O que o sistema faz quando algo dá errado?
Porque vai dar.
O erro clássico: assumir que “não acontece”
Muitos protocolos caseiros funcionam sob a suposição implícita de que:
- Nenhum byte será perdido
- Frames chegam sempre completos
- O transmissor e o receptor estão sempre sincronizados
- O sistema pode ser resetado se algo sair do controle
Essa mentalidade cria sistemas frágeis, dependentes de reset e impossíveis de operar de forma autônoma.
// ❌ Sistema que só funciona se tudo der certo
if (packet_received) {
process(packet);
}
Aqui não existe estado, não existe timeout, não existe tentativa de recuperação. Se o pacote não vier, o sistema simplesmente para de evoluir.
Sistemas reais assumem falha como condição normal
Em sistemas industriais, robóticos ou médicos, a comunicação é tratada como estatisticamente imperfeita. Isso muda tudo.
Um protocolo maduro define explicitamente:
- O que acontece se um frame não chega
- O que acontece se um frame chega corrompido
- Quando um comando expira
- Como o sistema volta a um estado conhecido
Timeouts: o relógio também é parte do protocolo
Se o parser entra em um estado intermediário e os bytes param de chegar, ele não pode ficar preso ali para sempre.
// ✅ Timeout de recepção
#define RX_TIMEOUT_MS 50
static uint32_t last_byte_time = 0;
void parser_process_byte(uint8_t byte) {
last_byte_time = millis();
// processamento normal
}
void parser_tick(void) {
if ((millis() - last_byte_time) > RX_TIMEOUT_MS) {
state = WAIT_SOF;
index = 0;
}
}
Isso garante que:
- Frames incompletos não travem o parser
- O sistema se auto-recupera
- Ruído não causa deadlock lógico
Re-sincronização explícita
Um bom framing permite que o sistema se realinhe naturalmente. Basta ignorar tudo até encontrar um novo SOF.
case WAIT_SOF:
if (byte == SOF) {
state = WAIT_LEN;
}
break;
Essa lógica simples é extremamente poderosa: qualquer erro é absorvido e o sistema continua funcionando.
Perda de pacotes não é exceção
UART não garante entrega. Portanto, protocolos sérios tratam perda como algo esperado.
Isso pode ser feito de várias formas:
- Comandos idempotentes (repetição não causa efeito colateral)
- Sequência de pacotes
- ACK / NACK
- Estados confirmáveis
Exemplo simples com número de sequência:
typedef struct {
uint8_t seq;
uint8_t cmd;
uint8_t data;
} packet_t;
static uint8_t last_seq = 0xFF;
void handle_packet(packet_t *p) {
if (p->seq == last_seq) {
return; // pacote duplicado
}
last_seq = p->seq;
execute_command(p);
}
Isso evita:
- Execução duplicada
- Estados inconsistentes
- Ações repetidas por retransmissão
UART como transporte muda o jogo
Quando você adiciona:
- Bufferização
- Framing explícito
- Integridade
- Parsing defensivo
- Timeouts
- Re-sincronização
UART deixa de ser “uma porta serial” e passa a ser um transporte confiável por construção, mesmo sem ajuda do hardware.
É nesse ponto que sistemas começam a:
- Conversar entre si
- Cooperar
- Escalar
- Ser atualizáveis
- Rodar por anos sem intervenção humana
A frase que resume tudo
Não se escreve para UART.
Se projeta para ela.
Quem trata UART como printf() constrói demos.
Quem a trata como transporte constrói sistemas distribuídos embarcados.