Table of Contents
- Passo a passo: configurando UART no Zephyr (do zero ao primeiro byte)
- Passo 1 — Escolha a UART “alvo” (console vs protocolo)
- Passo 2 — Garanta que a UART está habilitada no DeviceTree (DTS)
- Passo 3 — Ajuste o prj.conf para um pipeline robusto (não só “printf”)
- Passo 4 — Pegue o dispositivo UART corretamente (device binding)
- Passo 5 — Teste “o primeiro byte” (transmissão simples)
- Passo 6 — RX do jeito certo (base para o “algoritmo” do artigo)
- Passo 7 — Build e flash (workflow)
- O “melhor algoritmo” no Zephyr para UART (e por que ele também serve para SPI, I²C e afins)
- 2.1 O algoritmo certo não começa na UART — começa na arquitetura
- 2.2 O papel do Zephyr nesse algoritmo
- 2.3 Framing: o coração do algoritmo (onde tudo dá errado)
- 2.4 Implementando framing com máquina de estados (do jeito certo)
- 2.5 Ligando o ring buffer ao parser (Zephyr-style)
- 2.6 Por que esse algoritmo é superior no Zephyr
- 2.7 O erro mais comum de quem “usa Zephyr errado”
- UART, SPI e I²C no Zephyr: o algoritmo certo é o mesmo (o erro também)
- 3.1 O erro conceitual mais comum
- 3.2 UART no Zephyr — transporte frágil, exige disciplina
- 3.3 SPI no Zephyr — rápido, síncrono, e ainda assim traiçoeiro
- 3.4 I²C no Zephyr — endereço não é framing
- 3.5 O padrão correto: transporte plugável, protocolo fixo
- 3.6 Exemplo de abstração limpa (Zephyr-friendly)
- 3.7 O erro que diferencia firmware amador de firmware de produção
- Checklist definitivo de boas práticas no Zephyr para comunicação serial
- 4.1 Mentalidade correta (antes de qualquer linha de código)
- 4.2 Captura de dados (driver / ISR / callback)
- 4.3 Bufferização (o desacoplamento que salva sistemas)
- 4.4 Framing (onde protocolos nascem ou morrem)
- 4.5 Parsing (determinismo acima de tudo)
- 4.6 Integridade (CRC não é luxo)
- 4.7 Threads, prioridades e tempo real (Zephyr específico)
- 4.8 Reuso entre UART, SPI e I²C (prova de arquitetura)
- 4.9 Testabilidade (onde poucos chegam)
- 4.10 O erro final (e mais comum)
- Encerramento
Passo a passo: configurando UART no Zephyr (do zero ao primeiro byte)
A meta aqui é configurar a UART no Zephyr de um jeito que não te empurre para o erro clássico: polling + parsing direto + lógica de negócio colada no RX. O artigo anterior bate exatamente nessa ferida: consumir bytes é diferente de interpretar mensagens.
Passo 1 — Escolha a UART “alvo” (console vs protocolo)
Certo: separar “UART de debug/console” da “UART de protocolo” quando possível. Console muda baudrate, tem logs, pode travar por I/O e vira ruído no seu canal de dados.
Errado: usar a mesma UART para LOG, shell e protocolo binário e depois culpar “ruído” quando o framing quebra.
No Zephyr, console/shell normalmente apontam para:
zephyr,consolezephyr,shell-uartzephyr,uart-mcumgr(se você usar mcumgr)
Se sua placa só tem uma UART, ainda dá para conviver, mas você vai precisar ser bem rígido com framing e prioridades de CPU.
Passo 2 — Garanta que a UART está habilitada no DeviceTree (DTS)
Crie um overlay, por exemplo boards/<sua_placa>.overlay (ou app.overlay), habilitando a UART e configurando baudrate.
Exemplo típico (muda conforme SoC/placa):
/* boards/my_board.overlay */
/* Exemplo genérico: uart0 habilitada */
&uart0 {
status = "okay";
current-speed = <115200>;
/* pinctrl-0 / pinctrl-1 variam por família (nRF, STM32, etc.) */
};
/* Se você quiser garantir que o console NÃO use essa UART, você ajusta chosen/aliases. */
/ {
chosen {
/* Exemplo: console em outra UART, se existir */
/* zephyr,console = &uart1; */
};
};
Certo: configurar isso em DTS/overlay, para o build ser reproduzível.
Errado: “funciona na minha máquina” ajustando baudrate e pinos em código, hardcodeando registradores, ou assumindo que UART0 sempre existe.
Passo 3 — Ajuste o prj.conf para um pipeline robusto (não só “printf”)
Para uma UART de protocolo em firmware de produção, você quer no mínimo:
- driver serial habilitado
- API assíncrona (se disponível) ou interrupção
- ring buffer (ou FIFO/buffer) para desacoplar RX do parser
Exemplo de prj.conf (enxuto e bem prático):
# Serial/UART base
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=n # evite misturar console com protocolo (quando possível)
# Caminho "certo": RX assíncrono (melhor para throughput e menor jitter)
CONFIG_UART_ASYNC_API=y
# Se seu driver não suporta ASYNC, use interrupção
# CONFIG_UART_INTERRUPT_DRIVEN=y
# Bufferização (desacopla captura do parsing)
CONFIG_RING_BUFFER=y
# (Opcional) Logging - use com cuidado se dividir UART
CONFIG_LOG=y
CONFIG_LOG_MODE_DEFERRED=y # reduz impacto de tempo real
Certo: pensar na UART como “pipeline”: captura determinística e interpretação fora do contexto crítico.
Errado: habilitar console + logs síncronos no mesmo canal e depois tentar “consertar” com delays.
Passo 4 — Pegue o dispositivo UART corretamente (device binding)
No Zephyr, você obtém o device via DEVICE_DT_GET().
Exemplo (usando DT_NODELABEL(uart0)):
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
static const struct device *uart_dev = DEVICE_DT_GET(DT_NODELABEL(uart0));
static int uart_check_ready(void)
{
if (!device_is_ready(uart_dev)) {
return -1;
}
return 0;
}
Certo: falhar cedo se a UART não está pronta.
Errado: assumir que sempre existe e só descobrir em runtime “no campo”.
Passo 5 — Teste “o primeiro byte” (transmissão simples)
Antes de RX e protocolo, valide TX:
#include <zephyr/kernel.h>
#include <zephyr/drivers/uart.h>
static void uart_send_str(const struct device *dev, const char *s)
{
while (*s) {
uart_poll_out(dev, (unsigned char)*s++);
}
}
int main(void)
{
if (uart_check_ready() != 0) {
return 0;
}
uart_send_str(uart_dev, "UART OK (Zephyr)\r\n");
while (1) {
k_sleep(K_SECONDS(1));
}
}
Nota importante (didática e honesta): uart_poll_out() é OK para smoke test. Para produção, TX também deve ser bufferizado/assíncrono quando throughput e determinismo importam.
Passo 6 — RX do jeito certo (base para o “algoritmo” do artigo)
O artigo anterior deixa a regra: ISR (ou callback) não interpreta, só captura.
No Zephyr, isso mapeia perfeitamente para:
- UART Async API (callback do driver) → você copia bytes para um ring buffer
- uma thread (ou workqueue) consome ring buffer e roda o parser de framing + CRC + timeouts
Aqui vai um esqueleto “certo” com Async RX + ring buffer (ainda sem framing; isso entra na próxima seção):
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/ring_buffer.h>
#include <string.h>
#define RX_CHUNK_SIZE 64
#define RB_SIZE 512
static const struct device *uart_dev = DEVICE_DT_GET(DT_NODELABEL(uart0));
RING_BUF_DECLARE(rx_rb, RB_SIZE);
/* Buffer fornecido ao driver para DMA/async RX (o driver “enche” e te avisa) */
static uint8_t rx_chunk[RX_CHUNK_SIZE];
/* Callback de eventos da UART (não faça parsing aqui) */
static void uart_cb(const struct device *dev, struct uart_event *evt, void *user_data)
{
ARG_UNUSED(dev);
ARG_UNUSED(user_data);
switch (evt->type) {
case UART_RX_RDY: {
/* Bytes novos disponíveis em evt->data.rx.buf + offset, len */
const uint8_t *p = &evt->data.rx.buf[evt->data.rx.offset];
size_t len = evt->data.rx.len;
/* Captura determinística: empurra no ring buffer e sai */
uint32_t wrote = ring_buf_put(&rx_rb, p, len);
(void)wrote; /* em produção: conte drops e trate overflow */
break;
}
case UART_RX_BUF_REQUEST:
/* Driver pediu um novo buffer para continuar RX sem lacuna */
uart_rx_buf_rsp(uart_dev, rx_chunk, sizeof(rx_chunk));
break;
case UART_RX_DISABLED:
/* Reabilita RX se algo desativou */
uart_rx_enable(uart_dev, rx_chunk, sizeof(rx_chunk), 50 /*timeout ms*/);
break;
default:
break;
}
}
int main(void)
{
if (!device_is_ready(uart_dev)) {
return 0;
}
uart_callback_set(uart_dev, uart_cb, NULL);
/* Habilita RX assíncrono */
uart_rx_enable(uart_dev, rx_chunk, sizeof(rx_chunk), 50 /*timeout ms*/);
while (1) {
/* Aqui ainda não tem “protocolo”: só drenagem do buffer. */
uint8_t tmp[64];
uint32_t n = ring_buf_get(&rx_rb, tmp, sizeof(tmp));
if (n > 0) {
/* Por enquanto, eco para provar RX */
for (uint32_t i = 0; i < n; i++) {
uart_poll_out(uart_dev, tmp[i]);
}
}
k_sleep(K_MSEC(5));
}
}
Certo: callback/ISR só captura bytes, bufferiza e sai. Interpretar mensagens vem depois.
Errado: fazer strcmp, scanf, procurar \n, calcular CRC, ou decidir “liga motor” dentro do callback. Esse é exatamente o tipo de erro que o artigo anterior aponta como firmware que funciona em bancada e falha em produção.
Passo 7 — Build e flash (workflow)
Assumindo seu workspace Zephyr normal:
west build -b <sua_placa> -p auto app
west flash
west terminal