Get in touch

Explaining UART Protocol

IoT

Software Development

12 mins read

In this article, fundamental principles of UART will be discussed – definition of UART, how packets are transmitted over UART, arrangement and role of bits in frame protocol, what frame format is used, advantages and disadvantages of UART and most common UART use cases. The end goal of this article is to provide a clear explanation of the UART protocol and showcase an example of how it can be implemented using the Zephyr RTOS and STM32 board. But first, let’s see what the UART by definition is. 

 

What is UART? 

UART stands for Universal Asynchronous receiver-transmitter. It is a hardware communication protocol – it can be a physical circuit on a microcontroller or stand-alone integrated circuit (IC), used for asynchronous serial communication. It is one of the most used device-to-device communication protocols and can work with many different types of protocols that use serial communication.  Serial communication is the process of sending data one bit at a time sequentially and asynchronous means that endpoint interfaces are not synchronized by a common clock signal, but another form is used, like start and stop signals. UART is very simple – uses only two wires to transmit and receive data between two devices.

UART Interface 

Each UART contains two signals: 

 

  • Transmitter (Tx) 
  • Receiver (Rx)  

Main purpose of these two lines for each device is to transmit and receive serial data from serial communication. These lines are unidirectional , which means UART supports two-way communication and it can be used as:

 

  • Simplex – data is transmitted in one direction only
  • Half – duplex – each side transmit, but only one at the time
  • Full – duplex – both sides transmit simultaneously

 

The transceiver and receiver both have ground wire – it is used for keeping both sides (devices) on the same reference voltage.

Transmitting and receiving UART are both connected . Controlling data is sent in parallel form, but that data will be transmitted on the transmission line (TX) in serial form – bit by bit, to the RX line on another UART. The receiving UART then converts the serial data into parallel data.

 

As UART is asynchronous, transmitter and receiver need to be set on the same baud rate to be synchronized. The baud rate is defined as rate at which information is transferred to a communication channel. In the serial communication that is the number of bits per second that will be transferred. The most common UART baud rates in use today are: 9600, 19200, 57600, 115200 and 460800. The transmitter generates bitstream based on its clock signal while the receiver is using its clock signal to sample incoming data.

 

UART frame format and protocol

 

The mode of transmission in UART is in form of packet called UART frame. In addition to baud rate, both sides of UART communication have to use the same frame structures and parameters. In further text, UART frame structure will be explained.

UART frames consist of a start bit, data frame, optional parity bit and stop bit.
UART protocol does not define voltage levels as “high” for logic 1 or “low” for logic 0 like it is in most digital systems. In UART those levels are sometimes called “mark” for high level and “space” for low level. When UART is in the idle state, in which UART is when no data is being transmitted, the voltage line is held high. Thus, an error on the line or on the transmitter can be easily detected.

Start bit

 

It is already said that UART is asynchronous and because of that one bit is used to advise the receiver that transmission is starting. As UART voltage line is held high in idle state, at the start of transmission, the voltage level changes from high to low and back to high level in one clock cycle. That is the sign for the receiver to be prepared for incoming data.

 

Data frame

 

Immediately after start bit data frame is sent. Data frame contains the actual, useful data. The size of the data frame is 5-9 bits. If parity is used, maximal size is 8 bits, and if parity is not used, maximal size can be 9 bits. These data bits are usually sent with the least significant bit first.

 

Parity bit

 

After the data frame, parity bit can be placed – it is optional. It is used for error detection, to see if data has been changed or not. The value of the parity bit depends on the type of parity being used: even or odd. If we are using even parity, the number of ones (1) must be even number. In odd parity, number of ones must be odd number. For example, if we have data 11001010, number of ones is 4, which is even number. If we are using even parity, parity bit will be 0. But if we are using odd parity, parity bit will be 1.

 

Using a single parity bit, parity bit can detect only that one bit is flipped. If even numbers of bits  are flipped there is no way to reliably detect that.

 

Stop bit

 

To signal the end of data packet, a stop bit is used. Opposite of start bit, the voltage level on transmission line goes from low level to high level for one or two bits. The second stop bit is optional, and it is usually used to give the receiver enough time to get ready for the next frame, but this is uncommon practice.

 

Transmision

 

UART transmission consists of 5 steps.

In the first step  transmitting UART receives data in parallel form from the data bus.

After that, transmitting UART adds start bit, potentially parity bit (which is also calculated on transmitting UART) and stop bit to the data frame to make an UART package (frame).

 

In the third step, transmitter sends the whole package to receiving UART one bit at a time . Note that now the data bits are in LSB order – least significant bit is in the first place. This is where transmitter is done with his job and takes the main role.

The fourth step is on the receiving UART side: it samples incoming frame at the preconfigured baud rate, checks parity and discards information bits: start bit, parity and stop bit.

 

The last step is converting serial data back into parallel form so it can be transferred to the data bus on the receiving UART.

Configuration of UART

 

When starting to work with any hardware protocol, the first step is to look into the datasheet of the device you are working with, so this also applies to working with UART. Check available UARTs, pins associated with UART and specific details such as operation mode, frame format, length of data frame, parity bit and length of stop bit.

 

Next step is to check UART operation details: number and purpose of particular UART registers (which registers are used for data, interrupt, status, control, etc.), baud rate computation. Baud rate is configured using the formula in the reference manual. No universal formula is used to calculate the baud rate, but it depends on the type of device (microcontroller).

 

Here is formula for calculating baud rate for a specific microcontroller (NXP LPC1768) :

This equation can be rearranged as follows:

Description of variables is:

 

PCLK – Peripheral Clock in the Hz

MULVAL – part of fractional divider register to set the clock pre-scaler

DIVADDVAL – part of fractional divider register to set the clock pre-scalar

Note: when choosing MULVAL and DIVADDVAL values, take into consideration limitations (MULVAL cannot be 0, while DIVADDVAL can, MULVAL should be grater or equal to DIVADDVAL, etc.)

UxDLM – baud rate divider value

UxDLL – baud rate divider value

 

If your UART details are:

 

  • PCLK = 25 MHz
  • Baud rate = 115200

 

You should use following values:

 

  • MULVAL = 15
  • DIVADDVAL = 2
  • UxDLM = 0
  • UxDLL = 12

 

Now, when you have all these numbers, you can start with UART initialization by writing these values into registers.

The next thing you should also consider when using UART is the challenges that UART poses depending on the type of application. For example, in real-time applications – originally UART was not designed for usage in real-time applications, but with some modifications, like taking care that UART has non-blocking functions, it can be used as well. Also, you should be especially careful when sending large amounts of data.

 

To prevent data loss during transmission “flow control” is used. There are two types of flow control: hardware flow control (RTS/CTS) and software flow control (XON/XOFF).

 

Why UART?

 

Every protocol has its advantages and disadvantages, including UART. We will highlight some of them.

 

Advantages

 

  • Well documented and supported protocol.
  • Easy to set up.
  • Only two wires are necessary for full-duplex data transmission.
  • Parity bit ensures basic error checking is integrated in the data frame.
  • No need for clock signal.
  • If both sides are set up for it, frame structure can be changed.

 

Disadvantages

 

  • The size of the data frame is limited to a maximum of 9 bits if parity bit is not used, and 8 bits if party bit is used.
  • Speed for data transfer is less compared with parallel data transfer.
  • UART supports only communication between two devices – no multiple masters or (and) slaves.
  • Appropriate baud rate must be selected on the transmitter and receiver side.
  • Transmitter and receiver must agree on the rules of transmission in advance.

 

Where UART?

UART is one of the simplest and most used serial protocols. Today, we can find UART in GPS receivers, GSM and GPRS modems, Bluetooth modules, RFID based applications, Wireless communication systems.

Some fields where UART can (and is) used are debugging that can help with catching bugs in early development process, for manufacturing function-level tracking, also for customer or client updates and for testing purposes.

 

Example – using UART on STM32 with Zephyr

 

What is Zephyr and why it should be used is covered in our blog that you can find on our page: https://www.byte-lab.com/why-developers-should-choose-zephyr-rtos/. To configure UART on Zephyr, you need to add configuration options in proj.conf file and edit main.c file (if you already have an application directory set up). Zephyr provides three different ways to access the UART peripheral with different API functions:

 

  • Polling API
  • Interrupt-driven API
  • Asynchronous API (using DMA)

 

Below is a code example for UART using Interrupt-driven API. With the Interrupt-drive API, slow communication can happen in the background while the thread can continue doing some other work.

Zephyr provides samples for many drivers, sensors, etc. on this git page:  https://github.com/zephyrproject-rtos/zephyr/tree/main/samples.

 

main.c

#include 
#include 
#include 
#include 
/* change this to any other UART peripheral if desired */
#define UART_DEVICE_NODE DT_CHOSEN(zephyr_shell_uart)

#define MSG_SIZE 32

/* queue to store up to 10 messages (aligned to 4-byte boundary) */
K_MSGQ_DEFINE(uart_msgq, MSG_SIZE, 10, 4);

static const struct device *const uart_dev = DEVICE_DT_GET(UART_DEVICE_NODE);

/* receive buffer used in UART ISR callback */
static char rx_buf[MSG_SIZE];
static int rx_buf_pos;

/*
* Read characters from UART until line end is detected. Afterwards push the
* data to the message queue.
*/
void serial_cb(const struct device *dev, void *user_data)
{
uint8_t c;
if (!uart_irq_update(uart_dev)) {
return;
}

if (!uart_irq_rx_ready(uart_dev)) {
return;
}

/* read until FIFO empty */
while (uart_fifo_read(uart_dev, &c, 1) == 1) {
if ((c == '\n' || c == '\r') && rx_buf_pos > 0) {
/* terminate string */
rx_buf[rx_buf_pos] = '
#include 
#include 
#include 
#include 
/* change this to any other UART peripheral if desired */
#define UART_DEVICE_NODE DT_CHOSEN(zephyr_shell_uart)
#define MSG_SIZE 32
/* queue to store up to 10 messages (aligned to 4-byte boundary) */
K_MSGQ_DEFINE(uart_msgq, MSG_SIZE, 10, 4);
static const struct device *const uart_dev = DEVICE_DT_GET(UART_DEVICE_NODE);
/* receive buffer used in UART ISR callback */
static char rx_buf[MSG_SIZE];
static int rx_buf_pos;
/*
* Read characters from UART until line end is detected. Afterwards push the
* data to the message queue.
*/
void serial_cb(const struct device *dev, void *user_data)
{
uint8_t c;
if (!uart_irq_update(uart_dev)) {
return;
}
if (!uart_irq_rx_ready(uart_dev)) {
return;
}
/* read until FIFO empty */
while (uart_fifo_read(uart_dev, &c, 1) == 1) {
if ((c == '\n' || c == '\r') && rx_buf_pos > 0) {
/* terminate string */
rx_buf[rx_buf_pos] = '\0';
/* if queue is full, message is silently dropped */
k_msgq_put(&uart_msgq, &rx_buf, K_NO_WAIT);
/* reset the buffer (it was copied to the msgq) */
rx_buf_pos = 0;
} else if (rx_buf_pos < (sizeof(rx_buf) - 1)) {
rx_buf[rx_buf_pos++] = c;
}
/* else: characters beyond buffer size are dropped */
}
}
/*
* Print a null-terminated string character by character to the UART interface
*/
void print_uart(char *buf)
{
int msg_len = strlen(buf);
for (int i = 0; i < msg_len; i++) {
uart_poll_out(uart_dev, buf[i]);
}
}
void main(void)
{
char tx_buf[MSG_SIZE];
if (!device_is_ready(uart_dev)) {
printk("UART device not found!");
return;
}
/* configure interrupt and callback to receive data */
int ret = uart_irq_callback_user_data_set(uart_dev, serial_cb, NULL);
if (ret < 0) {
if (ret == -ENOTSUP) {
printk("Interrupt-driven UART API support not enabled\n");
} else if (ret == -ENOSYS) {
printk("UART device does not support interrupt-driven API\n");
} else {
printk("Error setting UART callback: %d\n", ret);
}
return;
}
uart_irq_rx_enable(uart_dev);
print_uart("Hello! I'm your echo bot.\r\n");
print_uart("Tell me something and press enter:\r\n");
/* indefinitely wait for input from the user */
while (k_msgq_get(&uart_msgq, &tx_buf, K_FOREVER) == 0) {
print_uart("Echo: ");
print_uart(tx_buf);
print_uart("\r\n");
}
}
'; /* if queue is full, message is silently dropped */ k_msgq_put(&uart_msgq, &rx_buf, K_NO_WAIT); /* reset the buffer (it was copied to the msgq) */ rx_buf_pos = 0; } else if (rx_buf_pos < (sizeof(rx_buf) - 1)) { rx_buf[rx_buf_pos++] = c; } /* else: characters beyond buffer size are dropped */ } } /* * Print a null-terminated string character by character to the UART interface */ void print_uart(char *buf) { int msg_len = strlen(buf); for (int i = 0; i < msg_len; i++) { uart_poll_out(uart_dev, buf[i]); } } void main(void) { char tx_buf[MSG_SIZE]; if (!device_is_ready(uart_dev)) { printk("UART device not found!"); return; } /* configure interrupt and callback to receive data */ int ret = uart_irq_callback_user_data_set(uart_dev, serial_cb, NULL); if (ret < 0) { if (ret == -ENOTSUP) { printk("Interrupt-driven UART API support not enabled\n"); } else if (ret == -ENOSYS) { printk("UART device does not support interrupt-driven API\n"); } else { printk("Error setting UART callback: %d\n", ret); } return; } uart_irq_rx_enable(uart_dev); print_uart("Hello! I'm your echo bot.\r\n"); print_uart("Tell me something and press enter:\r\n"); /* indefinitely wait for input from the user */ while (k_msgq_get(&uart_msgq, &tx_buf, K_FOREVER) == 0) { print_uart("Echo: "); print_uart(tx_buf); print_uart("\r\n"); } }

prj.conf

CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y

stm32f4.overlay

chosen {
zephyr,shell-uart = &usart2;
};

&usart2 {
pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>;
pinctrl-names = "default";
current-speed = <115200>;
status = "okay";
};

This file overlays board .dts file that is already defined. To understand what is Zephyr device tree and how it works in Zephyr RTOS, I recommend you to read our blog on that topic.

 

Note: this code is tested on STM32F4 discovery board.

 

Summary

 

  • UART stand for Universal Asynchronous receiver-transmitter protocol and is a simple, serial, two-wire protocol for exchanging data between two devices.
  • Frame format consists of start bit, data frame, optional parity bit and stop bit.
  • No clock needs to be used.
  • Transmitter and receiver should be on the same baud rate.