UART Driver

v2.x.x

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 pointer, which is a structure to hold information about the peripheral in a standard way.

Some drivers in Zephyr have API-specific structures and calls that encapsulate all the information needed to control the device in one structure. The UART driver does not have this, so we will use the macro call DEVICE_DT_GET() that was covered in the Device driver model section.

const struct device *uart = DEVICE_DT_GET(DT_NODELABEL(uart0));

if (!device_is_ready(uart)) {
    return;
}

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 node label 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 the devicetree panel. Clicking the “Show Compiled DeviceTree Output” displays the final devicetree configuration where you can search for specific controllers and know their state, as shown in the picture below

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 baudrate 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,
		.parity = UART_CFG_PARITY_NONE,
		.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.

Note

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:

EventDescription
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) {
	
	case UART_TX_DONE:
		// do something
		break;

	case UART_TX_ABORTED:
		// do something
		break;
		
	case UART_RX_RDY:
		// do something
		break;

	case UART_RX_BUF_REQUEST:
		// do something
		break;

	case UART_RX_BUF_RELEASED:
		// do something
		break;
		
	case UART_RX_DISABLED:
		// do something
		break;

	case UART_RX_STOPPED:
		// do something
		break;
		
	default:
		break;
	}
}

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;
		}

Receiving

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_US.

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:

case UART_RX_DISABLED:
	uart_rx_enable(dev, rx_buf, sizeof(rx_buf), 100);
	break;

Note

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

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_US);
	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.

	case UART_TX_DONE:
		// Do something here if needed  
		break;
v1.6.0 – v1.9.1

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.

CONFIG_SERIAL enables options for the serial drivers, and is usually enabled by default through the board’s devicetree as we have seen in-depth in Lesson 2. CONFIG_UART_ASYNC_API enables 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)));
	return;
}

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,
		.parity = UART_CFG_PARITY_NONE,
		.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.

Note

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:

EventDescription
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) {
	
	case UART_TX_DONE:
		// do something
		break;

	case UART_TX_ABORTED:
		// do something
		break;
		
	case UART_RX_RDY:
		// do something
		break;

	case UART_RX_BUF_REQUEST:
		// do something
		break;

	case UART_RX_BUF_RELEASED:
		// do something
		break;
		
	case UART_RX_DISABLED:
		// do something
		break;

	case UART_RX_STOPPED:
		// do something
		break;
		
	default:
		break;
				}

}

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;
		}
		

Receiving

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_US.

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:

case UART_RX_DISABLED:
	uart_rx_enable(dev, rx_buf, sizeof(rx_buf), 100);
	break;

Note

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

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_US);
	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.

	case UART_TX_DONE:
		// Do something here if needed  
		break;
Register an account
Already have an account? Log in
(All fields are required unless specified optional)

Forgot your password?
Enter your email address, and we will send a link to reset your password.