You are currently not logged in and your progress will not be saved. Register or Log in

UART Driver

In Zephyr, there are three different ways to access the UART peripheral, all with different API functions; polling, raw-interrupts and asynchronous.

In this part, we will learn how to use the UART peripheral in asynchronous mode. Asynchronous mode means we will get an interrupt every time new data is received through the UART driver. As opposed to raw-interrupts, the asynchronous mode supports features that allow us to enable receive timeouts and control the amount of data received before an interrupt is triggered.

As mentioned, the UART driver also has other ways to interact with it that we will not cover in this course. You can learn more about the other APIs here. The asynchronous API is quite powerful and covers most use-cases.

Enabling driver

1. As always, when it comes to drivers, the first thing we need to do is to enable the serial driver (UART driver). This is done by adding these two lines in prj.conf.


The first line is usually enabled by default through the board’s devicetree as we have seen in-depth in Lesson 2. However, the second line is important to enable the asynchronous API of the serial driver.

2. Include the header file of the UART driver in your source code.

#include <drivers/uart.h>

3. As we have seen in the previous lessons, a peripheral (GPIO, UART, I2C, SPI, etc.) is instantiated as a device struct, which is a structure to hold information about the peripheral in a standard way.

const struct device *uart= device_get_binding(DT_LABEL(DT_NODELABEL(uart0)));
if (uart == NULL) {
	printk("Could not find  %s!\n\r", DT_LABEL(DT_NODELABEL(uart0)));

The pointer uart of type struct device is the structure that is used when interacting with the UART API.

On the other hand, uart0 is the name of the devicetree node that represents the UART hardware controller on the chip.

Information about the nodes and node names can also be obtained in VS Code in nRF Connect -> Devicetree viewer -> Buses.

One more thing to notice in the devicetree is that the default speed (baud rate) is set to 115200.

UART Configurations

1. UART configurations like baud rate and parity bit can be configured both statically at build time and dynamically at run time as the Kconfig option (CONFIG_UART_USE_RUNTIME_CONFIGURE) is enabled by default.

The default static configuration of the UART hardware is obtained from the devicetree as we have seen in the previous step.

On the other hand, to change the UART configurations dynamically, you need to create a variable of type uart_config.

uart_config struct

An example of creating a variable of type uart_config is shown below:

const struct uart_config uart_cfg = {
		.baudrate = 115200,
		.stop_bits = UART_CFG_STOP_BITS_1,
		.data_bits = UART_CFG_DATA_BITS_8,
		.flow_ctrl = UART_CFG_FLOW_CTRL_NONE

The header-file uart.h has enumerations of all available options.

After that, call the UART API function uart_configure()function and pass it the variable of type uart_config.

	int err = uart_configure(uart, &uart_cfg);

	if (err == -ENOSYS) {
		return -ENOSYS;

2. Define the application callback function for the UART.


A callback function (also known as an interrupt handler or an ISR) runs asynchronously in response to a hardware or software interrupt. In general, ISRs have a higher priority than all threads (covered in Lesson 7). It preempts the execution of the current thread, allowing an action to take place immediately. Thread execution resumes only once all ISR work has been finished. Always try to keep the ISR as short as possible to guarantee your system’s responsiveness and prevent thread starvation.

We have the freedom to choose which UART events of interest to listen to.

Below are the available UART events:

UART_TX_DONEThe whole TX buffer was transmitted
UART_TX_ABORTEDTransmitting aborted due to timeout or uart_tx_abort call
UART_RX_RDY Some data was received and receive timeout occurred (if RX timeout is enabled) or when the receive buffer is full
UART_RX_BUF_REQUESTDriver requests next buffer for continuous reception
UART_RX_BUF_RELEASEDBuffer is no longer used by UART driver
UART_RX_DISABLEDThis event is generated whenever receiver has been stopped, disabled or finished its operation (receive buffer filled) and can be enabled again using uart_rx_enable()
UART_RX_STOPPEDRX has stopped due to external event
Types of events passed to callback in UART_ASYNC_API
Type uart_event_type

The callback function should have the following signature:

static void uart_cb(const struct device *dev, struct uart_event *evt, void *user_data)
	switch (evt->type) {
		// do something

		// do something
	case UART_RX_RDY:
		// do something

		// do something

		// do something
		// do something

		// do something


We do not have to include all of these switch cases. Only include the ones relevant to your application code as we will see in the exercise section of this lesson.

uart_event struct

The uart_event struct contains the UART event type and a union, which could be one of the four members (tx, rx , rx_buf, rx_stop).

The rx member is of type uart_event_rx, which will hold the incoming data over UART.

uart_event_rx struct

3. Register the callback function by calling the function uart_callback_set(), which takes three parameters as shown in the screenshot below:

	err = uart_callback_set(uart, uart_cb, NULL);
		if (err) {
			return err;


In this part, we will explain the needed steps to start receiving data over UART using the asynchronous API of the UART driver.

1. Declare a receive buffer to store the incoming data. The size and the type of the buffer must be selected with your application requirements in mind. For the simple exercises we have in this lesson, which controls LEDs through UART, we will simply declare a buffer of type uint8_t (a byte) of size 10 bytes. In future lessons, we will introduce some more capable data structures (FIFO, circular buffer, etc.) that can be used to store the incoming data with more flexibility.

static uint8_t rx_buf[10] = {0}; //A buffer to store incoming UART data 

2. To start receiving, call the uart_rx_enable() function, and pass the address of the receive buffer.

uart_rx_enable(uart ,rx_buf,sizeof rx_buf,100);

The last parameter is the timeout, which in the context of the uart_rx_enable() function will determine how fast we get notified on incoming data that is less than the whole buffer size. It is called the inactivity period, which is measured after receiving at least a byte. Pick a value that fits your application’s requirements. You can also disable the timeout by passing SYS_FOREVER_MS.

uart_rx_enable() signature

Note that this function returns immediately. Inside the UART ISR we can do (or delegate) the work of copying received data to the specified receive buffer.

3. The data received is accessible through the UART callback on the UART_RX_RDY event.

ItemHow to access it
Data Lengthevt->data.rx.len
Offset to where in the buffer data is storedevt->data.rx.offset
Actual data received evt->rx.buf[rx.offset] to evt->rx.buf[rx.offset+rx.len]

4. Continuous reception is not enabled by default, which means once the receive buffer is full, you must manually enable reception. Inside the UART_RX_DISABLED case of the UART callback, you must re-enable UART to have continuous reception, like below:

	uart_rx_enable(dev, rx_buf, sizeof(rx_buf), 100);


The UART asynchronous API offers a way to perform chained buffer reception. You can declare multiple buffers to seamlessly switch between them when the current buffer is full. To do this you need to call uart_rx_buf_rsp() on the event UART_RX_BUF_REQUEST, which will provide the next buffer. When the current buffer is filled, receiving will automatically go to the next buffer.


Transmitting is a straightforward task as we only need to specify the transmission buffer.

1. Define a transmission buffer to hold the data to be sent. The size and the type of the buffer must be selected with your application requirements in mind. In the exercise section, we will simply send a welcome message. Therefore we will define the transmission buffer to be of type uint8_t.

static uint8_t tx_buf[] =  {"nRF Connect SDK Fundamentals Course\n\r"};

2. Call the function uart_tx() to send the data over UART.

The timeout feature (last parameter) is only valid if flow control is enabled, which is not the default.

	err = uart_tx(uart, tx_buf, sizeof(tx_buf), SYS_FOREVER_MS);
	if (err) {
		return err;

The function returns immediately and the sending is actually managed internally by the UART driver.

(Optional) 3. If your application needs to take action once the whole transmission buffer is transmitted, you could do that by using the UART_TX_DONE event in the UART callback function.

		// Do something here if needed