The Physical Layer: Wiring a UART Connection
A Universal Asynchronous Receiver-Transmitter (UART) connection is the most fundamental serial protocol in embedded systems. Unlike synchronous buses, UART does not use a clock line. Instead, both devices must agree on a timing speed (baud rate) beforehand. Because it is point-to-point and asynchronous, the physical layer is incredibly simple, but that simplicity hides a few silicon-killing traps for the unwary.
At the hardware level, a basic UART link requires exactly three wires:
- TX (Transmit): The data output pin.
- RX (Receive): The data input pin.
- GND (Ground): The common reference voltage. Never omit this.
Logic Levels: The 5V vs 3.3V Trap
The most common way to brick a modern microcontroller is ignoring logic levels. Older Arduinos (Uno, Nano, Mega) operate at 5V logic. Modern boards like the ESP32, Raspberry Pi Pico, and STM32 operate at 3.3V logic. If you connect a 5V Arduino TX pin directly to a 3.3V ESP32 RX pin, you will force 5V into a 3.3V-tolerant input, often destroying the ESP32's internal ESD protection diodes and killing the chip.
The Fix: Use a bidirectional logic level converter (like a BSS138-based module, roughly $1.50) between the two boards. If you are in a pinch and only need to shift the 5V TX down to the 3.3V RX, a simple resistor voltage divider (2kΩ series, 3.3kΩ to ground) works perfectly for baud rates under 115200.
Bus Mechanics: Where UART Fits in the Protocol Stack
When designing a sensor network, you must choose the right protocol for your distance, speed, and device count constraints. While I2C and SPI dominate onboard peripheral communication, UART remains the king of off-board, module-to-module links (like GPS modules, cellular modems, and PC serial consoles).
| Feature | UART | I2C | SPI |
|---|---|---|---|
| Wires Required | 2 (TX/RX) + GND | 2 (SDA/SCL) + GND | 4 (MOSI/MISO/SCK/CS) + GND |
| Addressing | None (Point-to-Point) | 7-bit or 10-bit I2C Address | Hardware Chip Select (CS) lines |
| Max Speed | Typically 115,200 bps (up to 1-3 Mbps) | 100 kHz to 3.4 MHz | 10 MHz to 50+ MHz |
| Max Distance | ~15 meters (at 9600 baud) | ~1 meter (highly capacitance limited) | ~30 cm (onboard only) |
| Device Count | 1-to-1 (requires multiplexer for more) | Up to 127 devices on one bus | 1 Master, many Slaves (1 CS per slave) |
| Pull-up Resistors? | No (Push-Pull drivers) | Yes (Open-Drain architecture) | No (Push-Pull drivers) |
As shown in the table, UART is strictly a 1-to-1 connection. If you need to talk to multiple devices, you must use multiple hardware UART ports (like the ESP32's three built-in UARTs) or a software multiplexer. For a deep dive into the electrical characteristics of these buses, refer to the All About Circuits UART primer and the Espressif ESP32 UART API documentation.
Minimal Working Exchange: ESP32 to Arduino Nano
Let's build a reliable bridge between a 3.3V ESP32 DevKit V1 and a 5V Arduino Nano. We will use the ESP32's hardware Serial2 and the Nano's SoftwareSerial to avoid conflicting with the Nano's USB programming port.
Wiring Diagram
| ESP32 DevKit V1 (3.3V) | Logic Level Converter | Arduino Nano (5V) |
|---|---|---|
| GPIO 17 (TX2) | LV1 -> HV1 | Pin 10 (Software RX) |
| GPIO 16 (RX2) | LV2 -> HV2 | Pin 11 (Software TX) |
| 3V3 Pin | LV (Low Voltage Ref) | — |
| — | HV (High Voltage Ref) | 5V Pin |
| GND | GND (Both sides) | GND |
ESP32 Code (Sender)
// ESP32 Hardware Serial2 on GPIO 16 (RX) and 17 (TX)
#define RXD2 16
#define TXD2 17
void setup() {
Serial.begin(115200); // USB Debug
Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2); // UART to Nano
Serial.println('ESP32 UART Initialized');
}
void loop() {
Serial2.println('Hello from ESP32');
delay(1000);
// Read any response from Nano
while (Serial2.available()) {
Serial.print('Nano says: ');
Serial.println(Serial2.readStringUntil('\n'));
}
}
Arduino Nano Code (Receiver)
#include
// Software Serial on Pin 10 (RX) and Pin 11 (TX)
SoftwareSerial nanoUART(10, 11);
void setup() {
Serial.begin(9600); // USB Debug
nanoUART.begin(9600); // UART to ESP32
}
void loop() {
if (nanoUART.available()) {
String msg = nanoUART.readStringUntil('\n');
Serial.println(msg);
// Send acknowledgment back
nanoUART.println('ACK from Nano');
}
}
Debugging the Classic Failures
Every bus protocol has its signature failure mode. On I2C, a missing pull-up resistor or an address clash will halt communication entirely. On SPI, a misconfigured clock polarity (CPOL/CPHA) yields garbage data. But the classic UART failure is almost always a baud rate mismatch or a floating RX line.
- Baud Mismatch: If the ESP32 transmits at 115200 baud and the Nano listens at 9600 baud, the receiver will sample the line at the wrong intervals. The result isn't silence; it's a flood of erratic, non-ASCII garbage characters (like 'ÿ' or 'Ã'). Fix: Hardcode both sides to the exact same baud rate and verify crystal oscillator tolerances.
- Floating RX Line: If the TX wire disconnects or the sender is unpowered, the receiver's RX pin is left floating. Electromagnetic noise from nearby wires can trigger false start bits, causing the microcontroller to fire endless serial interrupts and crash your main loop. Fix: Add a 10kΩ pull-up resistor to the RX line to hold it HIGH (idle state) when disconnected.
- Ground Loops: If you are powering the two boards from different USB ports on a desktop PC, slight voltage differences in the PC's power rails can cause data corruption. Always ensure a thick, short GND wire connects the two boards directly.
To sniff and debug the physical bus without a $300 oscilloscope, buy a $12 USB Logic Analyzer (24MHz 8-channel Saleae clone) or a $5 USB-to-TTL adapter (FT232RL or CH340 chip). Connect the adapter's RX to your bus TX, open a terminal program like PuTTY, and verify the raw data stream.
UART Connection FAQ
Can I wire multiple devices to a single UART TX line?
Yes, but with strict limitations. Because UART is point-to-point, you can wire one Master TX to multiple Slave RX lines (a 'multi-drop' configuration). However, the slaves cannot share a single TX line back to the master without hardware AND gates or open-collector drivers, otherwise their transmissions will collide and short out. For true multi-device two-way communication, use RS-485 transceivers (like the MAX485 chip) which convert the UART signal to a differential bus capable of handling 32+ nodes over 1200 meters.
How do I sniff a UART connection without an oscilloscope?
The most cost-effective way to sniff a UART bus is using a cheap USB-to-Serial adapter (FTDI FT232RL or WCH CH340). Wire the adapter's RX pin to the target TX line (and share a common ground). Open a serial terminal like TeraTerm or the Arduino IDE Serial Monitor, set it to the suspected baud rate, and read the ASCII or HEX output. If you see garbage, step through standard baud rates (9600, 19200, 38400, 57600, 115200) until the text becomes legible. For visualizing the actual bit timing and start/stop bits, use a $12 logic analyzer with PulseView or Sigrok software.
Why does my UART connection drop characters at 115200 baud?
Dropped characters at high baud rates are usually caused by a buffer overflow, not a physical wire issue. The hardware UART peripheral on a microcontroller only has a tiny FIFO buffer (often just 1 or 2 bytes). If your main code loop takes too long to execute (e.g., blocking delays, long I2C sensor reads), the buffer overflows before the CPU can read it, and the new byte is discarded. Fix this by implementing a hardware interrupt (ISR) on the RX pin to instantly move incoming bytes into a larger software ring buffer, or use DMA (Direct Memory Access) if your chip supports it (like the ESP32 or STM32).
Do UART data lines need pull-up resistors like I2C?
No. This is a common point of confusion for makers moving from I2C to UART. I2C uses an 'open-drain' architecture where devices can only pull the line LOW; pull-up resistors are required to bring the line back HIGH. UART uses 'push-pull' CMOS drivers that actively drive the line both HIGH (idle) and LOW (active). Adding pull-up resistors to a standard UART line is unnecessary and can actually interfere with the signal edges at high baud rates. The only exception is if you need to hold an RX line HIGH to prevent noise triggers when the TX device is physically disconnected.






