UART, SPI e I²C no Zephyr: o algoritmo certo é o mesmo (o erro também)
Depois que você entende o pipeline da Seção 2, surge uma constatação importante — e normalmente desconfortável:
Se o seu protocolo só funciona na UART, ele está errado.
UART, SPI e I²C não são protocolos de aplicação.
Eles são meios físicos + elétricos + temporais para transportar bytes.
O erro clássico é mudar de barramento achando que isso “resolve” problemas de framing, perda de dados ou parsing.
Não resolve.
Só muda como o erro se manifesta.
3.1 O erro conceitual mais comum
Vamos ser diretos:
- UART: “preciso de framing”
- SPI: “não preciso, é síncrono”
- I²C: “o endereço já resolve”
❌ Tudo isso está errado.
A única diferença entre esses barramentos é como os bytes chegam até você.
Depois que eles chegam, o problema é idêntico.
Byte chegou → e agora?
Se você não sabe responder isso de forma determinística, o barramento não importa.
3.2 UART no Zephyr — transporte frágil, exige disciplina
Características:
- Assíncrono
- Sem noção de “pacote”
- Pode perder bytes
- Pode entrar em descompasso
Consequência:
UART te obriga a projetar protocolo.
Por isso ela é tão “reveladora” do nível do firmware.
Erro típico em UART:
- Parser dependente de
\n scanf()oustrtok()- Timeout implícito no terminal
👉 No Zephyr, se você não usar:
- buffer intermediário
- framing explícito
- máquina de estados
você está apenas adiando o problema.
3.3 SPI no Zephyr — rápido, síncrono, e ainda assim traiçoeiro
Características:
- Master/slave
- Clock compartilhado
- Transferências em blocos
Isso cria a falsa sensação de segurança:
“SPI não perde byte, então posso interpretar direto.”
❌ Errado.
Problemas reais em SPI:
- O master pode enviar mais bytes do que o slave espera
- O slave pode ser interrompido no meio do frame
- Um reset de um lado não reseta o outro
- O clock pode parar no meio da mensagem
No Zephyr, SPI geralmente entra via:
spi_transceive()spi_read()/spi_write()
Certo:
- SPI → buffer → mesmo parser da UART
Errado:
- SPI → struct direta → lógica de aplicação
Se você faz isso:
spi_read(dev, &cfg, &rx_buf);
process_command((cmd_t *)rx_buf);
você acabou de escrever um protocolo não resiliente.
3.4 I²C no Zephyr — endereço não é framing
Características:
- Endereçamento
- Transações curtas
- Geralmente usado para registros
Aqui surge outro erro comum:
“I²C já tem endereço, não preciso de protocolo.”
❌ Errado de novo.
O endereço só diz quem recebe, não diz:
- onde começa a mensagem
- quanto ela tem
- se está íntegra
- se pertence a esta versão do firmware
No Zephyr, I²C entra via:
i2c_read()i2c_write()i2c_burst_read()
Certo:
- usar I²C como link
- rodar o mesmo framing + CRC
Errado:
- tratar cada transação como “comando implícito”
Isso quebra no primeiro update de firmware ou no primeiro erro de sincronização.
3.5 O padrão correto: transporte plugável, protocolo fixo
Aqui está o ponto-chave do artigo:
Você não escreve um protocolo UART.
Você escreve um protocolo de aplicação.
UART, SPI e I²C são adapters.
Arquitetura correta:
[ UART RX ] ┐
[ SPI RX ] ├─→ ring buffer → parser → aplicação
[ I2C RX ] ┘
No Zephyr, isso significa:
- mesma máquina de estados
- mesmo código de CRC
- mesmo parser
- apenas muda o “driver” que empurra bytes para o buffer
3.6 Exemplo de abstração limpa (Zephyr-friendly)
Defina uma interface simples:
void protocol_feed(const uint8_t *data, size_t len)
{
for (size_t i = 0; i < len; i++) {
protocol_parse_byte(&parser, data[i]);
}
}
Agora:
- UART RX callback chama
protocol_feed() - SPI RX task chama
protocol_feed() - I²C handler chama
protocol_feed()
✔ Código único
✔ Testável
✔ Portável
✔ Evolutivo
3.7 O erro que diferencia firmware amador de firmware de produção
Amador:
“Esse protocolo é UART-only.”
Produção:
“Esse protocolo roda sobre UART hoje, SPI amanhã e BLE depois.”
O Zephyr foi feito para esse segundo cenário.
Mas ele não te impede de fazer o primeiro — esse é o perigo.