UART (Universal Asynchronous Receiver-Transmitter) is the bedrock of embedded communication. Unlike synchronous protocols that rely on a shared clock line, UART usage depends entirely on both devices agreeing to a strict timing contract—the baud rate—before a single bit is sent. It is a point-to-point, asynchronous protocol requiring only two data wires and a common ground. While it lacks the multi-device addressing of I2C or the raw throughput of SPI, its simplicity, long-distance capabilities (when paired with transceivers), and universal support across microcontrollers make it indispensable for GPS modules, cellular modems, and debug consoles.
The Physical Layer: Wiring, Logic Levels, and Bus Mechanics
Before writing a single line of code, you must understand the physical layer. UART is a push-pull architecture. The transmitting (TX) pin actively drives the line high and low. Because it is push-pull, UART does not require pull-up resistors—a common point of confusion for makers transitioning from I2C. The only strict physical requirement is a shared common ground (GND) between the two devices to establish a baseline voltage reference.
Wiring is always a crossover: the TX pin of Device A connects to the RX pin of Device B, and vice versa. Connecting TX to TX will result in a dead bus and potentially damaged GPIO pins if both drive opposite logic levels simultaneously.
Protocol Selection Matrix
To understand where UART fits in your design, compare its physical constraints against other common embedded buses. This table dictates which protocol fits your specific distance, speed, and device count requirements.
| Protocol | Wires Required | Max Practical Speed | Addressing | Max Distance | Device Topology |
|---|---|---|---|---|---|
| UART (TTL) | 2 (TX, RX) + GND | 1 Mbps (short runs) | None (Point-to-Point) | ~1 meter (TTL) | 1-to-1 |
| RS-485 (UART) | 2 (Differential) + GND | 10 Mbps | Software/None | 1,200 meters | Multi-drop (up to 32/256) |
| I2C | 2 (SDA, SCL) + GND | 3.4 Mbps (Ultra Fast) | 7-bit / 10-bit Hardware | ~30 cm (standard) | Multi-master / Multi-slave |
| SPI | 4 (MOSI, MISO, SCK, CS) | 50+ MHz | Chip Select (CS) lines | ~10 cm (high speed) | 1 Master / Multi-slave |
Modern microcontrollers like the ESP32 and STM32 operate at 3.3V logic. Legacy boards like the Arduino Uno use 5V. Feeding a 5V TX line directly into a 3.3V ESP32 RX pin will degrade the silicon over time or instantly brick the GPIO. Always use a bidirectional logic level converter (like a BSS138 MOSFET board) or a simple voltage divider (2kΩ and 3.3kΩ resistors) on the 5V-to-3.3V line.
Choosing the Right Protocol for Your Application
Deciding between UART, I2C, and SPI comes down to three variables: distance, throughput, and node count.
Choose UART when: You are connecting two distinct subsystems (e.g., a microcontroller to a SIM7600 4G modem or a u-blox M8N GPS). UART is also the only choice when you need to extend communication over long distances by adding RS-485 differential transceivers (like the MAX485), allowing TTL UART signals to travel over 1,000 meters of twisted-pair cable in noisy industrial environments.
Choose I2C when: You need to connect dozens of low-speed sensors (BME280, MPU6050, OLED displays) on the same PCB using only two wires. I2C handles multi-master arbitration and hardware addressing natively, but it is strictly bound to short distances due to bus capacitance limits.
Choose SPI when: You need high-bandwidth data transfer over short distances. SD card interfaces, TFT LCD screens, and high-speed ADCs rely on SPI's synchronous clocking to push megabytes of data per second without the overhead of start/stop bits.
Minimal Working Exchange: ESP32 to Arduino Uno
Let's build a robust point-to-point UART link between a 3.3V ESP32 DevKit V1 and a 5V Arduino Uno. We will use the ESP32's hardware UART2 and the Uno's SoftwareSerial to keep the Uno's hardware UART free for USB debugging.
Wiring and Level Shifting
| ESP32 Pin (3.3V) | Level Shifter / Divider | Arduino Uno Pin (5V) |
|---|---|---|
| GND | Direct Common Ground | GND |
| GPIO 17 (TX2) | Direct (3.3V is read as HIGH by 5V Uno) | Pin 10 (Software RX) |
| GPIO 16 (RX2) | Voltage Divider (2kΩ to GND, 3.3kΩ in series) | Pin 11 (Software TX) |
ESP32 Transmitter Code (HardwareSerial)
The ESP32 UART peripheral allows flexible pin mapping. We initialize Serial2 on GPIO 16 and 17 at 9600 baud.
// ESP32 DevKit V1 - Sender
#include <HardwareSerial.h>
// Define ESP32 Hardware Serial 2
HardwareSerial MySerial(2);
const int RX_PIN = 16;
const int TX_PIN = 17;
void setup() {
// Initialize USB serial for debug
Serial.begin(115200);
// Initialize UART2 at 9600 baud, 8N1
MySerial.begin(9600, SERIAL_8N1, RX_PIN, TX_PIN);
Serial.println("ESP32 UART Sender Ready.");
}
void loop() {
String payload = "SENSOR_DATA: 42.5";
MySerial.println(payload);
Serial.print("Sent: ");
Serial.println(payload);
delay(1000);
}
Arduino Uno Receiver Code (SoftwareSerial)
Using the Arduino Serial library, we listen on pins 10 and 11. Notice the baud rate matches exactly.
// Arduino Uno - Receiver
#include <SoftwareSerial.h>
// RX on Pin 10, TX on Pin 11
SoftwareSerial MySerial(10, 11);
void setup() {
// USB Serial for PC monitoring
Serial.begin(115200);
// Match ESP32 baud rate exactly
MySerial.begin(9600);
Serial.println("Uno UART Receiver Ready.");
}
void loop() {
if (MySerial.available()) {
String incoming = MySerial.readStringUntil('\n');
Serial.print("Received: ");
Serial.println(incoming);
}
}
Debugging the Bus: Classic Failures and Sniffing
When the serial monitor outputs garbage or stays completely blank, you are dealing with one of the classic embedded communication failures. Here is how to diagnose them systematically.
The Classic Failures by Protocol
- Baud Mismatch (UART): The most common UART failure. If the sender transmits at 9600 baud and the receiver listens at 115200, the receiver samples the bits at the wrong intervals, resulting in ASCII garbage (e.g.,
ÿÿÿ). Fix: Verify both.begin()calls match. Note that at high speeds (1Mbps), the Uno's software serial and even hardware UART clock dividers introduce timing jitter. Stick to standard rates (9600, 115200) for reliability. - Missing Common Ground (UART): If you connect TX and RX but forget the GND wire, the receiver has no voltage reference. The signal will float, causing intermittent data corruption or total silence. Fix: Always run a dedicated ground wire between the two boards.
- Address Clash (I2C): If you connect two identical sensors (e.g., two BME280s) without changing their I2C address via hardware jumpers, they will both respond to the same address, corrupting the SDA line. Fix: Run an I2C scanner sketch to verify unique addresses.
- Missing Pull-Up Resistors (I2C): I2C uses open-drain outputs. Without 4.7kΩ pull-up resistors on SDA and SCL to VCC, the bus cannot return to a HIGH state, and the microcontroller will hang indefinitely waiting for a clock edge. UART does not suffer from this.
How to Sniff and Decode the Bus
When software debugging fails, you must look at the physical signal. The best tool for this is a logic analyzer. You do not need a $400 Saleae Logic Pro; a $12 24MHz 8-channel clone based on the Cypress CY7C68013A chip works perfectly when paired with the open-source Sigrok / PulseView software.
- Connect the probes: Clip the logic analyzer ground to your circuit ground. Clip Channel 0 to the TX line and Channel 1 to the RX line.
- Set the sample rate: The Nyquist theorem dictates you need at least double the frequency, but for async serial, a minimum sampling rate of 4x to 10x the baud rate is required to accurately catch the start bit edge. For 115200 baud, set your logic analyzer to 1 MHz or 2 MHz.
- Trigger and Decode: Set a falling-edge trigger on the TX line (the start bit is always LOW). Capture the signal, then add the "Async Serial" protocol decoder in PulseView. Input your baud rate and parity settings.
The decoder will overlay the hexadecimal and ASCII values directly over the square waveforms. If the decoder shows valid text but your microcontroller doesn't receive it, your issue is software (wrong GPIO pin mapped in code). If the decoder shows framing errors or jagged voltage transitions, your issue is physical (crosstalk, missing ground, or lack of level shifting).






