UART (Universal Asynchronous Receiver-Transmitter) signals are the bedrock of embedded serial communication. Unlike SPI or I2C, UART is strictly asynchronous—there is no shared clock line. The transmitter and receiver must agree on timing beforehand, making the physical layer and baud rate configuration the single most common points of failure on the bench. At its core, a standard TTL UART link requires only three wires: TX (Transmit), RX (Receive), and a shared GND.

Because UART defines the data framing rather than the physical voltage levels, the same UART protocol can be pushed through entirely different physical layers depending on your distance and noise constraints. Below is the definitive breakdown of how UART signals map to the four most common physical layers you will encounter in 2026.

UART Physical Layers: TTL, RS-232, RS-422, and RS-485

When a datasheet says 'UART', it usually means 3.3V or 5V TTL logic. But when you need to run a serial link across a factory floor or between buildings, TTL will fail. You must map the UART TX/RX signals to a differential or high-voltage physical layer. Here is the bus mechanics and physical specification table to help you choose the right protocol variant for your distance, speed, and device count requirements.

Physical Layer Wires Required Max Speed (Practical) Max Distance Addressing / Topology Voltage Levels
TTL UART 2 (TX, RX) + GND 1 - 5 Mbps < 1 meter 1:1 (Point-to-Point) 0V to 3.3V / 5V
RS-232 2 (TX, RX) + GND 115.2 kbps (up to 1M) ~15 meters 1:1 (Point-to-Point) ±3V to ±15V
RS-422 4 (Differential pairs) + GND 10 Mbps ~1200 meters 1:10 (1 Driver, 10 Receivers) ±2V to ±6V (Diff)
RS-485 2 or 4 (Half/Full Duplex) + GND 10 Mbps (short) / 100kbps (long) ~1200 meters 1:32 to 1:256 (Multi-drop Bus) ±1.5V to ±5V (Diff)
Callout Tip: Physical Wiring & Pull-Up Requirements
Standard TTL UART lines are actively driven push-pull; they do not require I2C-style pull-up resistors on the data lines. However, if an RX pin is left floating when a cable is disconnected, ambient EMI can trigger phantom start bits. A 10kΩ pull-up on the RX line to VCC prevents this. Conversely, RS-485 requires bias resistors (typically 390Ω pull-up to VCC and 390Ω pull-down to GND) to keep the differential bus in a known 'idle' state when no node is transmitting, alongside 120Ω termination resistors at both ends of the cable.

Signal Framing and the Baud Rate Math

Because there is no clock line, UART signals rely on a rigidly timed frame structure. Every byte is packaged with a Start bit (always low), 5 to 9 Data bits, an optional Parity bit, and one or two Stop bits (always high).

The timing is dictated by the baud rate, which defines the number of signal transitions per second. For standard 8N1 (8 data bits, no parity, 1 stop bit) framing, a 10-bit frame is sent per byte. Let's look at the exact bit-time math for the two most common baud rates:

  • 9600 Baud: Each bit lasts exactly 104.16 µs. A full 10-bit frame takes 1.04 ms.
  • 115,200 Baud: Each bit lasts exactly 8.68 µs. A full 10-bit frame takes 86.8 µs.

If your transmitter is set to 115,200 baud but your receiver is listening at 9600 baud, the receiver will sample the first high-to-low transition (the start bit), wait 104 µs for the first data bit, and land squarely in the middle of the 12th bit of the actual transmission. The receiver's state machine will completely desynchronize, resulting in the classic 'garbage characters' (like ÿ, ð, or Ã) on your terminal.

Wiring a Minimal Working Exchange

Let's wire a minimal, functional UART exchange between an ESP32 DevKit v1 (3.3V logic) and an Arduino Nano v3 (5V logic).

Warning: Logic Level Mismatch
The ESP32 GPIO pins are strictly 3.3V tolerant. Feeding 5V from the Arduino Nano's TX pin directly into the ESP32's RX pin will degrade or destroy the ESP32 silicon over time. You must use a bidirectional logic level converter (like the BSS138-based modules) or a simple voltage divider on the Nano's TX line.

Physical Wiring Table

ESP32 DevKit (3.3V) Logic Level Converter Arduino Nano (5V)
GND GND (Both sides) GND
3V3 Pin LV (Low Voltage VCC) -
- HV (High Voltage VCC) 5V Pin
GPIO 17 (TX2) LV1 -> HV1 D10 (Software RX)
GPIO 16 (RX2) LV2 <- HV2 D11 (Software TX)

ESP32 Firmware (HardwareSerial)

Below is the complete, compilable ESP32 code using the native HardwareSerial library. We explicitly define the pins to avoid conflicts with the default USB-to-UART bridge on pins 1 and 3.

#include <HardwareSerial.h>

// Define UART1 on custom pins
HardwareSerial MySerial(1);
const int RX_PIN = 16;
const int TX_PIN = 17;

void setup() {
  // Initialize USB serial for debugging
  Serial.begin(115200);
  
  // Initialize hardware UART: 115200 baud, 8N1, RX pin, TX pin
  MySerial.begin(115200, SERIAL_8N1, RX_PIN, TX_PIN);
  Serial.println('ESP32 UART Initialized. Waiting for Arduino...');
}

void loop() {
  // Echo any data received from the Arduino back to the USB terminal
  if (MySerial.available()) {
    char c = MySerial.read();
    Serial.print('Received: ');
    Serial.println(c);
  }
  
  // Send a heartbeat ping every 2 seconds
  static unsigned long lastPing = 0;
  if (millis() - lastPing > 2000) {
    MySerial.println('PING');
    lastPing = millis();
  }
}

Sniffing, Debugging, and Classic Failures

When your terminal shows nothing, or just garbage, you need to look at the physical wire. A logic analyzer (like a Saleae Logic Pro 8 or a cheap $15 FX2LAP based clone running PulseView/Sigrok) is mandatory for debugging UART signals. Connect the ground clip to your shared GND, and probe the TX line of the transmitter.

Here is the decision tree for the three classic UART failures:

1. The Baud Mismatch (Garbage Output)

Symptom: You see data arriving, but it looks like random accented characters or inverted question marks.
Sniffing: The logic analyzer will decode the bits, but if you set the analyzer's baud rate to match the transmitter, the receiver's expected bit-width will visibly misalign with the actual pulses.
Fix: Verify the oscillator tolerances. Cheap ceramic resonators on clone Arduino boards can drift by 2-3%. At 115,200 baud, a 2% drift pushes the sampling point out of the valid bit window. Drop the baud rate to 38,400 or 57,600 to increase the timing margin.

2. Missing Common Ground (Corrupted Frames)

Symptom: Intermittent dropped bytes, or the receiver resets when the transmitter starts sending.
Sniffing: The logic analyzer (which is grounded to the PC) might see a clean signal, but the receiver sees the voltage referenced to a floating ground. If the ground potential between the two boards differs by more than 0.5V, the receiver's logic threshold is crossed unpredictably.
Fix: Run a dedicated, thick (20 AWG or larger) ground wire directly between the two boards. Never rely on the ground path through a USB hub or a long, thin ribbon cable.

3. Address Clash & Missing Bias (RS-485 Multi-drop)

Symptom: You are using UART over an RS-485 transceiver (like the MAX485) for a Modbus RTU sensor network. The bus locks up, or multiple nodes reply simultaneously, corrupting the packet.
Sniffing: An oscilloscope will show the differential A/B lines floating near 0V when idle, instead of sitting at a solid logic HIGH. You will also see overlapping waveforms when two nodes transmit.
Fix: First, ensure your master node has 390Ω bias pull-up/pull-down resistors on the A and B lines to keep the bus idle-high. Second, an address clash occurs if two slave devices are flashed with the same Modbus slave ID. Use a configuration tool to query and assign unique IDs (1 through 247) to each node before wiring them to the main bus.

For deeper physical layer design, particularly when extending UART over RS-485 for industrial environments, refer to the Texas Instruments RS-485 Design Guide for exact termination and bias calculations. For standard embedded UART implementation, the Espressif ESP-IDF UART API documentation provides exhaustive details on hardware FIFO thresholds and interrupt handling.