Explaining the Concept of UART (Universal Asynchronous Receiver/Transmitter) in Embedded Systems

Explain the concept of UART (Universal Asynchronous Receiver/Transmitter) in embedded systems

UART was the very first communication protocol I got working on a microcontroller, and honestly, it’s still the one I reach for the most. Whether I’m debugging firmware by printing values over a serial console, talking to a GPS module, or bridging a microcontroller to a PC, UART is almost always involved somewhere. In this article, I’ll walk through what UART actually is, how it works at the electrical and timing level, how to configure it in code, and where it fits in a real embedded system.

What Is UART?

UART stands for Universal Asynchronous Receiver/Transmitter. It’s not a communication protocol in the same sense as I2C or SPI with a defined bus standard — it’s actually a hardware peripheral (or its behavior implemented in software) that converts data between parallel form (inside the microcontroller) and serial form (one bit at a time, sent over a wire). The word “Asynchronous” is the key word here: there’s no shared clock signal between the transmitter and receiver. Both sides simply agree in advance on a data rate (baud rate) and format, and then time their signals independently.

graph LR
    A[MCU 1 - Parallel Data] -->|TX pin| B((Serial Bitstream))
    B -->|RX pin| C[MCU 2 - Parallel Data]
    C -->|TX pin| D((Serial Bitstream))
    D -->|RX pin| A

Why Asynchronous Matters

Compare this to SPI, which has a dedicated clock line (SCK) telling both devices exactly when to sample each bit. UART has no such line. Instead, both devices are pre-configured with the same baud rate (bits per second), and the receiver uses start bits and internal oversampling to figure out exactly when each bit begins and ends. This is what makes UART attractive for simple point-to-point links: only two wires are needed for full-duplex communication (plus ground), and there’s no clock line to route.

The UART Frame Structure

Every byte sent over UART is wrapped in a “frame” with the following structure:

graph LR
    S[Start Bit - 0] --> D0[Data Bit 0]
    D0 --> D1[Data Bit 1]
    D1 --> D2[...]
    D2 --> D7[Data Bit 7]
    D7 --> P[Parity Bit - optional]
    P --> ST[Stop Bit/s - 1 or 2]

This is usually described in shorthand like “8N1” — 8 data bits, No parity, 1 stop bit — which is by far the most common configuration you’ll see in embedded projects.

Timing Diagram: A Single UART Frame

sequenceDiagram
    participant Line as UART TX Line
    Note over Line: Idle (High)
    Line->>Line: Start bit (Low) - 1 bit period
    Line->>Line: D0 D1 D2 D3 D4 D5 D6 D7
    Line->>Line: Parity bit (optional)
    Line->>Line: Stop bit (High) - 1+ bit periods
    Note over Line: Return to Idle (High)

The receiver’s UART hardware doesn’t just sample once per bit — internally, most UART peripherals oversample each bit period (commonly 16x) and take the majority value near the middle of the bit window. This oversampling is what makes UART tolerant of small clock mismatches between transmitter and receiver; as long as both sides’ baud rates are within roughly 2–3% of each other, communication stays reliable.

Baud Rate: The Shared Agreement

Baud rate is the number of bits transmitted per second, and it must match on both ends of the link. Common baud rates include 9600, 19200, 38400, 57600, and 115200. If one device is configured for 9600 baud and the other for 115200, you’ll get garbled data — this is one of the single most common beginner mistakes I still see (and occasionally still make myself when copy-pasting old init code).

The baud rate is generated from the microcontroller’s peripheral clock divided down by a baud rate generator register. For an STM32, for example:

USARTDIV = f_PCLK / (16 * baud_rate)   [for oversampling by 16]

Configuring UART on an STM32 (Register-Level Example)

#include "stm32f4xx.h"

void UART2_Init(void) {
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;   // Enable USART2 clock
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;    // Enable GPIOA clock

    // Configure PA2 (TX) and PA3 (RX) as alternate function
    GPIOA->MODER &= ~(GPIO_MODER_MODER2 | GPIO_MODER_MODER3);
    GPIOA->MODER |= (2 << (2 * 2)) | (2 << (3 * 2));  // AF mode
    GPIOA->AFR[0] |= (7 << (4 * 2)) | (7 << (4 * 3));  // AF7 = USART2

    // Baud rate: assuming 16 MHz PCLK1, 9600 baud
    USART2->BRR = 16000000 / 9600;

    USART2->CR1 |= USART_CR1_TE | USART_CR1_RE;  // Enable TX and RX
    USART2->CR1 |= USART_CR1_UE;                 // Enable USART
}

void UART2_SendByte(uint8_t data) {
    while (!(USART2->SR & USART_SR_TXE));  // Wait until TX buffer empty
    USART2->DR = data;
}

uint8_t UART2_ReceiveByte(void) {
    while (!(USART2->SR & USART_SR_RXNE));  // Wait until data received
    return USART2->DR;
}

void UART2_SendString(const char *str) {
    while (*str) {
        UART2_SendByte(*str++);
    }
}

Configuring UART on an Arduino (High-Level Example)

Arduino’s HardwareSerial library hides all the register-level details:

void setup() {
    Serial.begin(9600);          // Initialize UART at 9600 baud
    Serial.println("UART Ready");
}

void loop() {
    if (Serial.available()) {
        char c = Serial.read();
        Serial.print("Received: ");
        Serial.println(c);
    }
}

Interrupt-Driven UART Reception

Polling (while (!(USART2->SR & USART_SR_RXNE));) works fine for simple demos, but it blocks the CPU while waiting. In real firmware, I almost always use interrupt-driven reception so the CPU can do other work while bytes arrive asynchronously:

#define RX_BUFFER_SIZE 64
volatile uint8_t rx_buffer[RX_BUFFER_SIZE];
volatile uint8_t rx_head = 0, rx_tail = 0;

void USART2_IRQHandler(void) {
    if (USART2->SR & USART_SR_RXNE) {
        uint8_t byte = USART2->DR;
        uint8_t next_head = (rx_head + 1) % RX_BUFFER_SIZE;
        if (next_head != rx_tail) {
            rx_buffer[rx_head] = byte;
            rx_head = next_head;
        }
        // If buffer full, byte is dropped - consider flow control
    }
}

This circular (ring) buffer pattern is extremely common in embedded UART drivers — it decouples the rate at which bytes arrive from the rate at which your application processes them.

DMA-Based UART for High Throughput

For applications streaming a lot of UART data (e.g., logging sensor data at high rates, or talking to a GPS module continuously), even interrupt-per-byte becomes a CPU burden. Using DMA (Direct Memory Access), the UART peripheral can move received bytes directly into a memory buffer without any CPU intervention at all, only interrupting once a full buffer (or a configured amount) has arrived.

graph LR
    UART[UART Peripheral] -->|DMA Channel| RAM[Memory Buffer]
    RAM -->|Interrupt on completion| CPU[CPU: Process buffer]

Flow Control

Some UART implementations add two extra signal lines — RTS (Request To Send) and CTS (Clear To Send) — to prevent a fast sender from overrunning a slow receiver’s buffer. This is called hardware flow control. It’s less commonly used in simple embedded projects but becomes relevant when bridging to modems, some Bluetooth/Wi-Fi modules, or PC-side serial applications that can’t guarantee timely reads.

UART vs I2C vs SPI

FeatureUARTI2CSPI
Wires needed2 (TX, RX) + GND2 (SDA, SCL) + GND4 (MOSI, MISO, SCK, CS) + GND
Clock lineNone (asynchronous)Shared clockShared clock
Devices per busPoint-to-point (2 devices)Multiple (addressed)Multiple (via chip select)
SpeedModerate (up to ~few Mbps)Moderate (100kHz–3.4MHz)High (up to tens of Mbps)
ComplexityVery simpleModerateModerate

Real-World Applications of UART

IoT Integration Example

In an IoT weather station I built, a low-power sensor MCU reads temperature/humidity and sends a compact packet over UART to an ESP32, which then handles Wi-Fi and MQTT publishing. This separation keeps the sensor MCU simple (it doesn’t need a full networking stack) while the ESP32 handles connectivity — a very common pattern in multi-chip IoT designs.

// Sensor MCU side - packing a simple frame
void send_reading(float temp, float humidity) {
    char frame[32];
    snprintf(frame, sizeof(frame), "T:%.2f,H:%.2f\r\n", temp, humidity);
    UART2_SendString(frame);
}
// ESP32 side - parsing the frame (Arduino-style)
void loop() {
    if (Serial2.available()) {
        String line = Serial2.readStringUntil('\n');
        float t, h;
        sscanf(line.c_str(), "T:%f,H:%f", &t, &h);
        publish_to_mqtt(t, h);
    }
}

Reliability and Error Handling

UART peripherals typically expose status flags for:

Checking and handling these flags matters in production firmware — silently ignoring overrun errors, for instance, can cause your parser to desync from the byte stream and never recover until a reset.

if (USART2->SR & USART_SR_ORE) {
    volatile uint8_t dummy = USART2->DR;  // Clear overrun by reading DR
    // Log or handle error
}

Security Considerations

UART itself has no built-in encryption or authentication — it’s a raw, plaintext, wire-level protocol. Exposed UART debug headers on production IoT devices are a well-known attack vector: if a device’s UART pins are left accessible with a bootloader or shell prompt reachable, an attacker with physical access can often extract firmware, dump memory, or even gain a root shell. In production hardware, it’s good practice to disable, fuse-lock, or physically remove debug UART access before shipping.

Debugging Tips

Frequently Asked Questions

Is UART the same as RS-232? No, though they’re related. UART describes the logic-level (0V/3.3V or 0V/5V) serial framing used inside embedded systems. RS-232 is an older electrical standard that uses different (and inverted, higher) voltage levels for longer-distance, noise-resistant communication — a level shifter (like a MAX232 chip) is needed to convert between UART and true RS-232.

Can UART support more than two devices on a bus? Not directly — standard UART is point-to-point. If you need multi-device communication, you’d typically use I2C, SPI, or a UART-based multi-drop protocol like RS-485 with addressing built into your own packet format.

What happens if the baud rates don’t match? Bits get sampled at the wrong moments, producing garbled data and often framing errors. Even a small mismatch (a few percent) is usually tolerated due to oversampling, but a large mismatch (e.g., 9600 vs 19200) will corrupt every byte.

What’s the maximum practical UART speed? This depends on the microcontroller and physical wiring, but many microcontrollers support UART bit rates well into the multi-megabit range over short, clean traces; over longer cables, lower baud rates (9600–115200) are far more common for reliability.

Summary

UART is one of the oldest and simplest serial communication peripherals used in embedded systems, and that simplicity is exactly why it remains so widely used today — for debug consoles, GPS modules, Bluetooth chips, and inter-board communication. It works by agreeing on a baud rate in advance and framing each byte with start/stop bits so the receiver can recover timing without a shared clock line. Understanding UART deeply — from the frame structure and oversampling to interrupt-driven and DMA-based reception — is foundational, because nearly every other embedded communication concept builds on the same ideas of framing, buffering, and error handling.

References and Further Reading

Exit mobile version