MCU & FPGA RTOS Zephyr RTOS: Como Projetar Comunicação Serial Robusta com UART, SPI e I²C

Zephyr RTOS: Como Projetar Comunicação Serial Robusta com UART, SPI e I²C


Table of Contents

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,console
  • zephyr,shell-uart
  • zephyr,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

0 0 votos
Classificação do artigo
Inscrever-se
Notificar de
guest
0 Comentários
mais antigos
mais recentes Mais votado
Feedbacks embutidos
Ver todos os comentários

Related Post

0
Adoraria saber sua opinião, comente.x