UART Serial Communication: Baud Rate, Flow Control and Debugging

UART remains the first serial protocol I reach for when bringing up a new sensor board, and for good reason. It is simple, asynchronous, and available on almost every microcontroller from an ATtiny to an ESP32. Unlike shared-bus protocols, a UART link is point-to-point: one transmitter, one receiver, no addressing, no bus arbitration. That simplicity makes it perfect for sensor interfacing where you need to stream readings from a GPS module, a particulate sensor, or a debug console without spending pins or code space. In my experience, most UART problems beginners hit are not mysterious — they come from mismatched baud rates, missing grounds, or assuming the link is error-free when it is not. This article walks through how UART actually works at the signal level, how to choose a baud rate you can live with, when flow control matters, how to wire mixed-voltage nodes safely, and how I debug the inevitable garbage characters on the bench.

How UART Frames Bits Without a Shared Clock

UART stands for Universal Asynchronous Receiver/Transmitter. Asynchronous is the key word. There is no clock line. Instead, both sides agree in advance on timing and frame format, then rely on precise sampling to recover data. When the line is idle it sits high at logic high (VCC). Every byte begins with a start bit, which is a high-to-low transition, followed by 5 to 9 data bits, an optional parity bit, and one or two stop bits that return the line to high.

I have found that visualizing the frame on an oscilloscope is the fastest way for beginners to internalize this. At 9600 baud, each bit lasts about 104 microseconds. The receiver detects the falling edge of the start bit, waits half a bit period to sample in the middle of the bit, and then samples each subsequent bit in the center of its window. Most modern UARTs oversample by 16x, meaning they sample the line 16 times per bit and use a majority vote of the middle three samples to decide if a bit is 0 or 1. This is why UART can tolerate a few percent of clock error and still work.

Anatomy of a UART Frame: Start, Data, Parity and Stop Bits

The default frame on most sensor modules is 8 data bits, no parity, one stop bit, often written as 8N1. Data is transmitted LSB first. So the byte 0x41 (ASCII 'A', binary 01000001) appears on the wire as: start (0), then 1-0-0-0-0-0-1-0 (LSB first), then stop (1). If you enable even parity, the UART adds a ninth bit that makes the total number of 1s even, which lets the receiver detect a single-bit error. In practice, I leave parity disabled for sensor links and rely on a checksum at the protocol layer, as parity catches very little and costs throughput.

Stop bits give the receiver time to prepare for the next start bit. One stop bit is standard. Two stop bits are sometimes used with very slow electromechanical devices or when the receiver needs more processing time. The cost is 10% lower throughput, so I only use 2 stop bits when the datasheet explicitly requires it.

Why the Idle-High Line and Start Bit Edge Matter

Because the line idles high, a break condition — holding the line low longer than a full frame — can be used to signal an error or reset. More importantly, any disconnect or unpowered sensor will read as a constant low if you have pull-downs enabled, which looks like a continuous break. I always enable an internal pull-up on the RX pin or add a 10k external pull-up so a disconnected sensor reads as idle high rather than constant framing errors. That one resistor has saved me hours of debugging phantom data.

Calculating Baud Rate and Managing Clock Tolerance

Baud rate is the number of signaling events per second. For UART, baud rate equals bits per second, including start, data, parity and stop bits. If you send 8N1 at 9600 baud, you get 960 bytes per second because each byte costs 10 bits on the wire. Beginners often assume any baud rate will work if both sides are set the same, but the real constraint is how accurately each side can generate its bit clock from its system clock.

On an AVR microcontroller, the baud rate generator is a divider: UBRR = (F_CPU / (16 * Baud)) - 1 in normal mode, or (F_CPU / (8 * Baud)) - 1 in double-speed mode. On ARM parts like STM32, the baud rate register value is similarly derived from PCLK. The Zephyr Project Documentation provides good reference formulas for various SoCs and explains why a 16x oversampling clock is standard. If your system clock does not divide evenly, you get baud rate error. UART typically tolerates about ±2-3% total error between transmitter and receiver before sampling drifts into the wrong bit window at the end of a 10-bit frame.

Standard Baud Rates and Where 9600 and 115200 Come From

Common rates — 9600, 19200, 38400, 57600, 115200 — come from historical modem crystals like 1.8432 MHz which divide cleanly. When interfacing modern sensors, I start at 9600 or 115200 unless the sensor requires something else. Many GPS modules default to 9600, while lidar and high-rate IMU boards push 460800 or 921600. Higher rates mean less time per bit, which makes clock error and signal integrity more critical and leaves your MCU less time to service each byte.

Clock Error in Practice: 8 MHz Internal Oscillators vs Crystals

In one deployment I switched from a 16 MHz crystal to the internal 8 MHz RC oscillator to save cost, and the UART link that was perfect at 115200 started dropping bytes. The RC oscillator was ±2% over temperature, and combined with ~0.8% divider error, we exceeded the sampling window. Switching to 38400 or calibrating the oscillator fixed it. If you must use an internal oscillator, stay conservative on baud rate or enable auto-baud detection if your sensor supports it.

// AVR ATmega328P baud rate setup for 9600 baud @ 16 MHz
// UBRR = (16000000 / (16 * 9600)) - 1 = 103
#include <avr/io.h>
#define F_CPU 16000000UL
#define BAUD 9600
#define UBRR_VALUE ((F_CPU / (16UL * BAUD)) - 1)

void uart_init(void) {
    UBRR0H = (uint8_t)(UBRR_VALUE >> 8);
    UBRR0L = (uint8_t)UBRR_VALUE;
    UCSR0B = (1 << RXEN0) | (1 << TXEN0);   // Enable RX and TX
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); // 8 data bits, 1 stop bit
    // Error at this setting: ((16000000/(16*(103+1)))-9600)/9600 = 0.2%
}

void uart_transmit(uint8_t data) {
    while (!(UCSR0A & (1 << UDRE0))); // Wait for empty transmit buffer
    UDR0 = data;
}
Feature UART I2C SPI
Topology Point-to-point, asynchronous Multi-drop bus, half-duplex Point-to-point or bus with chip-select, full-duplex
Wires Required 2 (TX, RX) + GND, optional RTS/CTS 2 (SDA, SCL) + GND 4 (MOSI, MISO, SCK, CS) + GND
Clocking No shared clock, baud rate agreement Shared clock (SCL), clock stretching allowed Shared clock (SCK), master-driven
Typical Sensor Throughput 9.6 kbps to 921.6 kbps 100 kbps to 400 kbps (up to 3.4 Mbps in HS mode) 1 Mbps to 20+ Mbps
Best Use Case Debug consoles, GPS, low-to-medium rate sensors, long-ish cables Multiple low-speed sensors on two wires (temp, humidity) High-speed sensors (IMU, ADC, display) needing synchronous bursts

If you need a shared bus with multiple sensors on the same two wires, I2C Protocol Tutorial: Addressing, Clock Stretching and Multi-Master is a better fit. For high-speed, full-duplex sensor reads where you control the clock, SPI Communication: Full-Duplex Data Transfer for High-Speed Sensors will give you an order of magnitude more throughput than UART can.

Hardware Flow Control and Software Flow Control in Practice

At low baud rates and with small sensor packets, you can get away without flow control. The moment you have a fast sensor and a receiver that occasionally does heavy processing — writing to flash, servicing another interrupt, or running a filter — bytes will be lost to overrun. Flow control is a way for the receiver to tell the transmitter to pause.

I have learned to ask: can the receiver guarantee it will drain the UART FIFO before the next byte arrives? At 115200 baud a new byte arrives every 86 microseconds. If your main loop occasionally blocks for 2 milliseconds to handle another task, you will overrun without flow control or DMA.

RTS/CTS Handshaking When You Have Extra Wires

Hardware flow control uses two extra lines: RTS (Request To Send, output from receiver) and CTS (Clear To Send, input to transmitter). The receiver deasserts RTS when its buffer is almost full, and the transmitter checks CTS before sending the next byte. It is fast, handled in hardware on most MCUs, and does not interfere with data bytes. For continuous sensor streams — for example a 100 Hz IMU dumping 32-byte packets — I always enable RTS/CTS if the sensor exposes those pins. The wiring is a cross: RTS on one side to CTS on the other, and vice versa.

XON/XOFF Software Flow Control When You Are Limited to Two Wires

Software flow control uses in-band bytes: XON (0x11) to resume and XOFF (0x13) to pause. It needs no extra wires but has drawbacks: it cannot be used with binary data that may contain 0x11/0x13 unless you escape those bytes, and it adds latency because the XOFF byte must be transmitted and parsed. I use XON/XOFF only for ASCII-based sensors like older data loggers or when my connector physically only has TX/RX/GND. If you choose it, ensure your sensor firmware actually honors it — many cheap modules ignore it entirely.

// STM32 HAL UART with RTS/CTS hardware flow control
// Board: STM32F4, APB1 clock 42 MHz, 115200 8N1

UART_HandleTypeDef huart2;

void uart2_init_with_flow_control(void) {
    huart2.Instance = USART2;
    huart2.Init.BaudRate = 115200;
    huart2.Init.WordLength = UART_WORDLENGTH_8B;
    huart2.Init.StopBits = UART_STOPBITS_1;
    huart2.Init.Parity = UART_PARITY_NONE;
    huart2.Init.Mode = UART_MODE_TX_RX;
    huart2.Init.HwFlowCtl = UART_HWCONTROL_RTS_CTS; // Enable RTS/CTS
    huart2.Init.OverSampling = UART_OVERSAMPLING_16;
    HAL_UART_Init(&huart2);
    // Configure RTS/CTS pins to alternate function in GPIO init
    // Transmitter will automatically pause when CTS is deasserted
}

// Non-blocking transmit will respect CTS in hardware
HAL_UART_Transmit_IT(&huart2, sensor_cmd, sizeof(sensor_cmd));

Wiring UART for Sensor Nodes: Voltage Levels, Grounding and Crossover

UART is often called TTL serial or CMOS serial, but those labels hide the voltage question. A 5V Arduino Uno transmits at 0V for 0 and 5V for 1. A 3.3V ESP32 or nRF52 transmits at 0V and 3.3V. Connect a 5V TX directly to a 3.3V RX and you risk latch-up or permanent damage. In my experience, the sensor survives the first few minutes and fails weeks later in the field.

TX to RX Crossover and Why Common Ground is Non-Negotiable

UART is not like I2C where devices share the same lines. You must cross over: TX on device A goes to RX on device B, and RX on A goes to TX on B. I label my cables on both ends for this reason. Equally important is a common ground. Without a shared GND reference, the receiver has no way to interpret the voltage on RX. On battery-powered sensor nodes with separate supplies, I run a dedicated ground wire alongside TX/RX, even if the supplies are supposedly grounded through chassis. A floating ground shows up as intermittent framing errors that disappear when you attach a debugger — because the debugger provides the ground.

Level Shifting 3.3V and 5V Sensor Interfaces Safely

For a 5V sensor talking to a 3.3V MCU, the 3.3V TX to 5V RX direction is usually fine because 3.3V is above the TTL high threshold (2.0V). The dangerous direction is 5V TX to 3.3V RX. Options: a resistive divider (e.g., 1.8k / 3.3k) works at low baud rates but slows edges; a MOSFET-based bi-directional shifter (like BSS138) works well up to ~500 kbps; a dedicated IC like TXB0108 or 74LVC245 is best for high speed or multiple lines including RTS/CTS. I avoid simple series resistors alone — they limit current but do not guarantee voltage.

Also watch for RS-232. True RS-232 uses ±12V and inverted logic. You cannot connect RS-232 directly to a microcontroller UART pin. You need a transceiver like MAX3232. Many industrial sensors still expose RS-232 on a DB9, so check the datasheet for voltage levels before wiring.

Polling, Interrupt and DMA Strategies for Continuous Sensor Streams

How you handle UART data in firmware matters as much as wiring. The three common methods are polling, interrupt-driven, and DMA, and the right choice depends on baud rate and what else your MCU is doing, such as sampling an ADC Sampling Techniques: Resolution, Noise Reduction and Oversampling routine that cannot be blocked.

Polling means your loop checks the RX flag and reads the byte. It is simple and fine at 9600 baud when you have little else to do. At 115200 with a 10 ms sensor fusion loop, polling will miss bytes. The FreeRTOS Documentation shows how blocking on a UART queue can starve lower-priority tasks, which is exactly what happens with polling in a super-loop.

Why Interrupts Beat Polling for Sensor Interfacing

With interrupts, the UART peripheral triggers an ISR when a byte arrives, and the ISR pushes the byte into a ring buffer. Your main code then parses packets at its leisure without strict timing. This decouples the asynchronous arrival from processing. Keep the ISR short: just buffer the byte and clear flags. Never do printf or heavy parsing inside the ISR.

Ring Buffers and DMA for High-Rate Links Without Data Loss

For high-rate streams — say a GPS at 921600 baud or a sensor dumping 1 kB packets — even interrupts can become expensive. DMA lets the UART peripheral write incoming bytes directly to RAM without CPU involvement, and you get an interrupt only when a block is complete or idle line is detected. On STM32 I use DMA with idle-line detection to receive variable-length sensor frames. Combined with a ring buffer, this handles continuous streams while the CPU runs filters or communications stacks.

// Interrupt-driven ring buffer for UART RX (generic C, no HAL)
// Size must be power of two for mask optimization
#define RING_SIZE 128
volatile uint8_t rx_ring[RING_SIZE];
volatile uint8_t rx_head = 0;
volatile uint8_t rx_tail = 0;

// Called from UART RX ISR - keep it minimal
void uart_rx_isr(void) {
    uint8_t data = UDR0; // Read hardware register, clears RXC flag
    uint8_t next_head = (rx_head + 1) & (RING_SIZE - 1);
    if (next_head != rx_tail) { // Drop byte if buffer full, avoid overwrite
        rx_ring[rx_head] = data;
        rx_head = next_head;
    } else {
        // Buffer overrun - increment error counter for debugging
    }
}

int16_t uart_read_byte(void) {
    if (rx_head == rx_tail) return -1; // Empty
    uint8_t data = rx_ring[rx_tail];
    rx_tail = (rx_tail + 1) & (RING_SIZE - 1);
    return data;
}

// In main loop: parse packets without blocking
void process_sensor_stream(void) {
    int16_t b;
    while ((b = uart_read_byte()) != -1) {
        if (parse_byte((uint8_t)b)) {
            handle_complete_packet();
        }
    }
}

Debugging UART Links: From Garbage Output to Framing Errors

When UART fails, it fails in characteristic ways. Garbage characters (þ ÿ Ù) almost always mean baud rate mismatch, wrong frame format, or clock error. No data at all often means TX/RX not crossed or missing ground. Intermittent bytes with occasional framing errors suggest noise, long unterminated wires, or voltage mismatch. I start every debug session with the lowest layer: physical wiring, then baud, then software.

First, verify baud with measurement. A logic analyzer costing $10 can capture the start bit width and tell you if the sensor is really at 115200 or actually at 111111 due to divider error. An oscilloscope shows ringing and noise. In my experience, cheap jumper wires longer than 30 cm at 460800 baud will show rounded edges and reflections. Twisting TX with GND or using a short shielded cable cleans this up immediately.

Using a Logic Analyzer and Oscilloscope to Verify Timing

Set your analyzer to the expected baud and capture. If the decoded bytes look correct but your MCU still sees framing errors, check oversampling and clock settings. On one board, we had configured the STM32 UART for 8N1 while the sensor was set to 8E1 (even parity). The data looked almost right — every byte was off by one bit shift — but parity errors flagged constantly. Matching the frame format fixed it instantly. Always confirm data bits, parity, and stop bits on both ends.

Common Pitfalls I've Seen in Field Deployments

Overrun errors (ORE) happen when a new byte arrives before you read the previous one. Some UARTs lock up until you clear ORE by reading status then data registers in sequence. Framing errors (FE) mean the stop bit was not high when expected — usually a baud mismatch or noise. Noise errors (NE) from break or glitch detection should be treated as warnings to check cabling. In firmware, I log these error flags to a counter that I can read over my debug console instead of silently ignoring them — silent failure is the worst kind.

Another subtle bug: not disabling the UART TX pin's pull-up/pull-down during sleep. On low-power sensor nodes, a floating TX line can wake the peer or draw current. I configure TX as push-pull high when idle and only enable the UART peripheral when needed, then return the pin to a defined state before sleep. It reduces sleep current by tens of microamps, which matters over months on battery.

Frequently Asked Questions

Can I connect two UART transmitters together to share one receiver?

No, UART TX pins are push-pull outputs. If two transmitters drive the line at the same time, they will contend and can be damaged. If you need multiple sensors talking to one MCU UART, use a multiplexer, separate UART peripherals, or switch to a bus protocol like I2C or CAN. If you must share a line, use open-drain with pull-ups and a collision-avoidance protocol, but at that point you are reinventing a bus and should pick a bus in the first place.

What is the maximum cable length for UART sensor links?

There is no fixed limit, but practical limits depend on baud rate and cable capacitance. At 9600 baud I have run 3 meters of twisted pair reliably. At 115200, keep it under 1 meter with standard jumper wires, or use shielded cable and lower the baud if you need distance. For longer runs, convert to RS-485 or RS-232 transceivers which are designed for noise immunity and distance. Signal ground must run with the signal — do not rely on earth ground.

Why am I getting correct data but also occasional corrupted bytes?

Intermittent corruption usually means one of three things: baud rate error near the tolerance edge, noise on long wires, or overrun where your firmware is not draining the RX buffer fast enough. Check error flags for overrun and framing errors, lower the baud rate to see if corruption disappears, and capture the line on a logic analyzer to verify bit timing. If corruption happens during flash writes or heavy processing, move UART handling to interrupts or DMA with a ring buffer.

Do I need to use flow control for a simple temperature sensor that sends a reading once per second?

Usually not. If the sensor sends a short ASCII line at a low rate and your MCU can read it before the next line arrives, no flow control is needed. I add RTS/CTS only when the sensor streams continuously, uses high baud rates above 38400, or when the receiver may block for milliseconds. For a 1 Hz sensor at 9600 baud, a simple interrupt-driven buffer is more than enough.

Related Articles

References & Standards: FreeRTOS Documentation · Zephyr Project Documentation · MQTT Specification