Serial UART (Universal Asynchronous Receiver-Transmitter) is the bedrock of embedded debugging and point-to-point peripheral control. Unlike synchronous protocols that rely on a shared clock line, UART depends entirely on pre-agreed timing. If your microcontrollers do not share the exact same baud rate and ground reference, your data turns into garbage. This primer strips away the abstraction and gives you the exact physical layer requirements, timing math, and wiring diagrams needed to get your ESP32 and Arduino talking reliably.

Serial UART Bus Mechanics & Physical Layer

UART is fundamentally a point-to-point, asynchronous protocol. It requires no pull-up resistors (unlike I2C) and no chip-select lines (unlike SPI). However, because it lacks a clock signal, the physical layer demands strict adherence to voltage levels and timing tolerances.

Table 1: UART Bus Mechanics Overview
Parameter UART Specification
Wires Required 2 data (TX, RX) + 1 common Ground (GND)
Speed (Baud) Typically 9600 to 115200; up to ~1 Mbps practical
Addressing None (strictly point-to-point)
Topology Full-duplex, single master/single slave
Max Distance ~15m at 9600 baud; <1m at 115200 baud (unshielded)

The most critical physical layer detail often ignored by hobbyists is logic level matching. An Arduino Nano outputs 5V logic on its TX pin. If you wire that directly to an ESP32's RX pin (which is strictly 3.3V tolerant), you will eventually degrade or destroy the ESP32's GPIO pin. You must use a logic level shifter (like a BSS138 MOSFET board) or a simple resistor voltage divider.

Baud Rate Timing & Distance Limits

Because there is no clock line, the receiver samples the data line in the middle of each bit period. Higher baud rates compress these windows, making the signal highly susceptible to capacitance and wire length.

Table 2: Standard Baud Rate Timing & Distance Limits
Baud Rate Bit Duration 10-Bit Frame Time Max Practical Distance (Unshielded)
9600 104.16 µs 1.04 ms ~15 meters
57600 17.36 µs 173.6 µs ~3 meters
115200 8.68 µs 86.8 µs ~1 meter
921600 1.08 µs 10.8 µs < 30 cm (PCB trace only)

For a deep dive into the electrical characteristics of serial lines, refer to the SparkFun Serial Communication guide, which details how signal degradation impacts high-speed UART.

The Classic UART Failures (And How to Fix Them)

Every communication protocol has its signature failure mode. While I2C networks typically suffer from address clashes and missing pull-up resistors, UART's classic failures are rooted in timing and ground references.

The Big Three UART Killers:
  1. Baud Mismatch: If the sender transmits at 115200 and the receiver listens at 9600, the receiver will interpret the start bit and subsequent fast transitions as multiple garbage characters (often displaying as ÿ or ??). Always verify both devices are using the exact same baud rate. Note that internal RC oscillators on some cheap AVR clones can have a 3-5% timing error, which is enough to cause framing errors at 115200 baud.
  2. Missing Common Ground: The silent killer. If the ESP32 and Arduino do not share a common GND wire, their voltage references float relative to each other. The receiver will see random noise, resulting in intermittent framing errors. Always run a dedicated ground wire alongside your TX/RX pair.
  3. Swapped TX/RX: TX must always connect to RX, and RX to TX. If you connect TX to TX, the bus is dead. If in doubt, swap the two data wires and test again.

How to Sniff and Debug the Bus

When serial terminals show garbage, do not guess—measure. Use a USB logic analyzer (like a Saleae Logic 8 or a DSLogic Plus). Connect the probes to the TX and RX lines, and ensure the ground clip is attached to the common ground.

Set your logic analyzer sample rate to at least 4 times the baud rate (e.g., 1 MS/s for 115200 baud). Use a protocol decoder (like those available in Sigrok/PulseView) to automatically parse the start bits, data bytes, parity, and stop bits. If you see the decoder reporting 'Framing Errors', your baud rates are mismatched or your ground is floating. If the signal edges look rounded or sloped instead of sharp squares, your wire is too long or has too much parasitic capacitance for the chosen baud rate.

Minimal Working Exchange: ESP32 to Arduino Nano

Below is a complete, bench-tested setup for sending a string from an Arduino Nano (5V) to an ESP32 (3.3V). We use a resistor voltage divider to protect the ESP32. For a detailed breakdown of why this works, see the Adafruit Level Shifting tutorial.

Physical Wiring

Arduino Nano (5V) Component ESP32 DevKit (3.3V)
D11 (Software TX) Direct wire GPIO16 (Hardware UART2 RX)
D10 (Software RX) 2kΩ resistor (series) GPIO17 (Hardware UART2 TX)
N/A 3.3kΩ resistor (to GND) GPIO17 (Junction with 2kΩ)
GND Direct wire GND

Note: The 2kΩ and 3.3kΩ resistors form a voltage divider that drops the Nano's 5V TX output down to a safe ~3.1V for the ESP32's RX pin. The ESP32's 3.3V TX output is already high enough to be read reliably by the Nano's 5V RX pin, so no divider is needed on that line.

Arduino Nano Code (Sender)

#include <SoftwareSerial.h>

// Nano RX on D10, TX on D11
SoftwareSerial mySerial(10, 11);

void setup() {
  mySerial.begin(115200);
}

void loop() {
  mySerial.println("Nano Sensor Data: 42");
  delay(1000);
}

ESP32 Code (Receiver)

// ESP32 Hardware UART2 uses GPIO16 (RX) and GPIO17 (TX) by default
#define RXD2 16
#define TXD2 17

void setup() {
  Serial.begin(115200); // USB debug monitor
  Serial2.begin(115200, SERIAL_8N1, RXD2, TXD2);
  Serial.println("ESP32 Ready to receive...");
}

void loop() {
  if (Serial2.available()) {
    String incoming = Serial2.readStringUntil('\n');
    Serial.print("Received from Nano: ");
    Serial.println(incoming);
  }
}

Protocol Selection: When to Choose UART vs I2C vs SPI

UART is not always the right tool. When designing a system with multiple sensors or high-speed data requirements, you must evaluate distance, speed, and device count.

Table 3: Protocol Selection Matrix
Criteria UART I2C SPI
Wires 2 (TX/RX) + GND 2 (SDA/SCL) + GND 4 (MOSI/MISO/SCK/CS) + GND
Speed Low to Med (~1 Mbps) Low to Med (400kHz - 3.4MHz) High (10 MHz - 50+ MHz)
Device Count 1-to-1 only Multi-drop (up to 127 addresses) Multi-drop (requires 1 CS pin per device)
Best Use Case Debugging, GPS modules, cellular modems, long-distance low-speed links On-board sensors (temp, IMU), OLED displays, EEPROM High-speed ADCs, SD cards, TFT displays, external flash
The Decision Framework:
Choose UART when you are connecting two distinct microcontrollers, communicating with a modem/GPS, or need to run a wire more than 2 meters at low speeds.
Choose I2C when you have 5+ low-speed sensors on the same PCB and want to save GPIO pins.
Choose SPI when you need to push large blocks of data (like an image to a display or audio to a DAC) as fast as the microcontroller's clock will allow.