A UART is an asynchronous serial communication peripheral that transmits and receives without sharing a clock signal.
It is frequently used for consoles, GNSS receivers, modems, and other nearby modules. UART alone is not a long-distance link; for that, it needs a physical transceiver like RS-232 or RS-485.
In UART, there is no shared clock. Each device must know in advance the speed at which to talk (Baudrate). And most importantly, it is Asynchronous. At any moment, without warning, the GPS could send you a data frame. If you are not listening at that precise microsecond, you lose the data.
In Zephyr, handling UART by polling is easy, but inefficient. Today we’ll learn to do it properly: using interrupts.
Configuring the hardware in the overlay
Typically, a development board already comes with a UART configured as a “Console” (where printk output goes). We want to use another UART to connect a sensor.
Suppose our board has a free &uart1 node.
/* app.overlay */
&uart1 {
status = "okay";
current-speed = <9600>; /* Standard GPS speed */
/* Optional: Parity/stop bits configuration if hardware requires it */
/* parity = "none"; */
};Pin Conflict: Make sure the TX/RX pins of uart1 are not being used by another peripheral (like an LED or I2C). On some boards, you need to check the “Pin Control” (pinctrl) to reassign them, although in this basic course we will assume you are using the defaults.
Polling API for transmitting
For sending data (TX), the blocking method (polling) is usually acceptable, since we typically send short, controlled commands.
#include <zephyr/kernel.h>
#include <zephyr/drivers/uart.h>
#include <errno.h>
/* Get the device directly */
/* Note: For UART we sometimes use DEVICE_DT_GET instead of DT_SPEC */
#define UART_NODE DT_NODELABEL(uart1)
static const struct device *uart_dev = DEVICE_DT_GET(UART_NODE);
void send_byte(uint8_t c) {
/* Sends a character and waits for it to complete (blocking) */
uart_poll_out(uart_dev, c);
}
void send_string(const char *s) {
while (*s) {
uart_poll_out(uart_dev, *s++);
}
}uart_poll_in() is non-blocking: it returns -1 when there is no data. Polling it in a loop without sleeping, however, consumes CPU unnecessarily.
Interrupt API for receiving
The correct way to receive data in Zephyr is to configure an Interrupt (ISR). When the hardware detects that a byte is coming in, the Kernel pauses whatever it is doing, executes our function, we save the data, and continue.
It is a 3-step process:
Define the Callback function.
Configure the Callback on the device.
Enable data reception.
The complete code
This example acts as an “echo”. The ISR copies the bytes to a queue, and the main thread forwards them, so that blocking output does not occur inside the interrupt.
#include <zephyr/kernel.h>
#include <zephyr/drivers/uart.h>
/* Reference to the UART1 device */
const struct device *uart_dev = DEVICE_DT_GET(DT_NODELABEL(uart1));
/* Temporary buffer for the ISR */
static uint8_t rx_buf[10];
/* Byte queue between the ISR and the main thread */
K_MSGQ_DEFINE(rx_queue, sizeof(uint8_t), 64, 1);
/* --- This is our ISR function (Runs in interrupt context) --- */
void serial_cb(const struct device *dev, void *user_data)
{
/* 1. Check what caused the interrupt */
if (uart_irq_update(dev) <= 0) {
return;
}
/* 2. Is it a DATA AVAILABLE (RX) interrupt? */
if (uart_irq_rx_ready(dev)) {
int data_length;
/* 3. Read from the hardware FIFO until empty */
while ((data_length = uart_fifo_read(dev, rx_buf, sizeof(rx_buf))) > 0) {
/* Copy without waiting; if the queue fills up, discard 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 not ready\n");
return 0;
}
/* 1. Register the callback function */
if (uart_irq_callback_user_data_set(uart_dev, serial_cb, NULL) < 0) {
return -ENOTSUP;
}
/* 2. Enable RX interrupts (Reception) */
uart_irq_rx_enable(uart_dev);
printk("System ready. Write something on UART1...\n");
/* Echo from the thread context */
while (1) {
uint8_t byte;
if (k_msgq_get(&rx_queue, &byte, K_FOREVER) == 0) {
uart_poll_out(uart_dev, byte);
}
}
}To compile the API used in the example, add CONFIG_UART_INTERRUPT_DRIVEN=y to prj.conf.
In a real application, the thread can extract the bytes from a message queue or a ring buffer and reconstruct the complete frame, for example an NMEA sentence from a GNSS receiver.
Ring buffers
Because UART is a continuous stream of data, Message Queues are sometimes “too structured”. Zephyr offers Ring Buffers (sys/ring_buffer.h), which are very efficient circular byte buffers.
- ISR: Does
ring_buf_put(puts bytes in bulk). - Thread: Does
ring_buf_get(retrieves bytes and looks for newline\ncharacters).
Asynchronous UART API
Just as an advanced note: Zephyr has a third way to use UART called the Async API, which uses DMA (Direct Memory Access). It allows you to tell the hardware: “Let me know when you have received 100 bytes or 10ms of silence has passed”.
It is very powerful for saving CPU, but not all hardware drivers support it (Nordic nRF ones do, ESP32 ones have limited support depending on the version). To start with, the Interrupt API we saw above is the most compatible and standard.