Una UART es un periférico de comunicación serie asíncrona que transmite y recibe sin compartir una señal de reloj.
Se utiliza con frecuencia para consolas, receptores GNSS, módems y otros módulos cercanos. UART por sí sola no es un enlace de larga distancia; para eso necesita un transceptor físico como RS-232 o RS-485.
En la UART no hay reloj compartido. Cada dispositivo tiene que saber de antemano a qué velocidad hablar (Baudrate). Y lo más importante: es Asíncrona. En cualquier momento, sin avisar, el GPS puede enviarte una trama de datos. Si no estás escuchando en ese preciso microsegundo, pierdes el dato.
En Zephyr, manejar la UART por sondeo (Polling) es fácil, pero ineficiente. Hoy vamos a aprender a hacerlo bien: usando interrupciones.
Configurar el hardware en el overlay
Normalmente, una placa de desarrollo ya trae una UART configurada como “Consola” (donde salen los printk). Nosotros queremos usar otra UART para conectar un sensor.
Supongamos que nuestra placa tiene un nodo &uart1 libre.
/* app.overlay */
&uart1 {
status = "okay";
current-speed = <9600>; /* Velocidad estándar de GPS */
/* Opcional: Configuración de paridad/stop bits si el hardware lo requiere */
/* parity = "none"; */
};Conflicto de Pines: Asegúrate de que los pines TX/RX de la uart1 no están siendo usados por otro periférico (como un LED o I2C). En algunas placas, hay que revisar el “Pin Control” (pinctrl) para reasignarlos, aunque en este curso básico asumiremos que usas los por defecto.
API de polling para transmitir
Para enviar datos (TX), el método de bloqueo (polling) suele ser aceptable, ya que normalmente enviamos comandos cortos y controlados.
#include <zephyr/kernel.h>
#include <zephyr/drivers/uart.h>
#include <errno.h>
/* Obtenemos el dispositivo directamente */
/* Nota: Para UART a veces usamos DEVICE_DT_GET en lugar de DT_SPEC */
#define UART_NODE DT_NODELABEL(uart1)
static const struct device *uart_dev = DEVICE_DT_GET(UART_NODE);
void enviar_byte(uint8_t c) {
/* Envía un carácter y espera a que salga (bloqueante) */
uart_poll_out(uart_dev, c);
}
void enviar_cadena(const char *s) {
while (*s) {
uart_poll_out(uart_dev, *s++);
}
}uart_poll_in() es no bloqueante: devuelve -1 cuando no hay datos. Consultarlo en un bucle sin dormir, sin embargo, consume CPU innecesariamente.
API por interrupciones para recibir
La forma correcta de recibir datos en Zephyr es configurar una Interrupción (ISR). Cuando el hardware detecta que entra un byte, el Kernel pausa lo que esté haciendo, ejecuta nuestra función, guardamos el dato y seguimos.
Es un proceso de 3 pasos:
Definir la función de Callback.
Configurar el Callback en el dispositivo.
Habilitar la recepción de datos.
El código completo
Este ejemplo actúa como un “eco”. La ISR copia los bytes a una cola y el hilo principal los reenvía, de modo que la salida bloqueante no ocurre dentro de la interrupción.
#include <zephyr/kernel.h>
#include <zephyr/drivers/uart.h>
/* Referencia al dispositivo UART1 */
const struct device *uart_dev = DEVICE_DT_GET(DT_NODELABEL(uart1));
/* Buffer temporal para la ISR */
static uint8_t rx_buf[10];
/* Cola de bytes entre la ISR y el hilo principal */
K_MSGQ_DEFINE(rx_queue, sizeof(uint8_t), 64, 1);
/* --- Esta es nuestra función ISR (Corre en contexto de interrupción) --- */
void serial_cb(const struct device *dev, void *user_data)
{
/* 1. Verificamos qué ha causado la interrupción */
if (uart_irq_update(dev) <= 0) {
return;
}
/* 2. ¿Es una interrupción de DATOS DISPONIBLES (RX)? */
if (uart_irq_rx_ready(dev)) {
int data_length;
/* 3. Leemos del FIFO de hardware hasta que se vacíe */
while ((data_length = uart_fifo_read(dev, rx_buf, sizeof(rx_buf))) > 0) {
/* Copiamos sin esperar; si la cola se llena, descartamos bytes. */
for (int i = 0; i < data_length; i++) {
(void)k_msgq_put(&rx_queue, &rx_buf[i], K_NO_WAIT);
}
}
}
}
int main(void)
{
if (!device_is_ready(uart_dev)) {
printk("UART no lista\n");
return 0;
}
/* 1. Registramos la función de callback */
if (uart_irq_callback_user_data_set(uart_dev, serial_cb, NULL) < 0) {
return -ENOTSUP;
}
/* 2. Habilitamos las interrupciones de RX (Recepción) */
uart_irq_rx_enable(uart_dev);
printk("Sistema listo. Escribe algo en la UART1...\n");
/* Eco desde contexto de hilo */
while (1) {
uint8_t byte;
if (k_msgq_get(&rx_queue, &byte, K_FOREVER) == 0) {
uart_poll_out(uart_dev, byte);
}
}
}Para compilar la API utilizada en el ejemplo, añade CONFIG_UART_INTERRUPT_DRIVEN=y a prj.conf.
En una aplicación real, el hilo puede extraer los bytes de una message queue o un ring buffer y reconstruir la trama completa, por ejemplo una sentencia NMEA de un receptor GNSS.
Ring buffers
Como la UART es un chorro de datos continuo, las Message Queues a veces son “demasiado estructuradas”. Zephyr ofrece los Ring Buffers (sys/ring_buffer.h), que son buffers circulares de bytes muy eficientes.
- ISR: Hace
ring_buf_put(mete bytes a saco). - Hilo: Hace
ring_buf_get(saca bytes y busca saltos de línea\n).
API UART asíncrona
Solo como nota avanzada: Zephyr tiene una tercera forma de usar la UART llamada Async API, que utiliza DMA (Direct Memory Access). Permite decirle al hardware: “Avísame cuando hayas recibido 100 bytes o haya pasado 10ms de silencio”.
Es muy potente para ahorrar CPU, pero no todos los drivers de hardware la soportan (los de Nordic nRF sí, los de ESP32 tienen soporte limitado dependiendo de la versión). Para empezar, la API de Interrupciones que hemos visto arriba es la más compatible y estándar.