UART (Universal Asynchronous Receiver-Transmitter) is a two-wire, point-to-point serial protocol that transmits data one bit at a time without a shared clock signal. It relies on pre-agreed timing (baud rate) and a start/stop bit framing mechanism to synchronize the sender and receiver. If you are connecting a GPS module to an ESP32, linking a Raspberry Pi to a microcontroller, or debugging a cellular modem, you are using UART.
The Physical Layer and Bus Mechanics
Before writing a single line of code, you must understand the electrical reality of the bus. Unlike synchronous protocols that use a clock line to tell the receiver exactly when to sample a bit, UART is asynchronous. The receiver guesses the timing based on the agreed-upon baud rate the moment it sees the start bit drop low.
| Parameter | Standard UART (TTL) | RS-232 (Legacy/Industrial) |
|---|---|---|
| Wires Required | 3 (TX, RX, GND) | 3 to 5+ (TX, RX, GND, RTS, CTS) |
| Speed (Baud) | 9600 to 115200 typical (up to ~921600) | Up to 115200 typical |
| Addressing | None (Point-to-point only) | None (Point-to-point only) |
| Max Distance | ~1 meter (unshielded), up to 15m (RS-485 transceivers) | ~15 meters (at 19200 baud) |
| Logic Levels | 3.3V or 5V TTL (0V = Low, VCC = High) | +/- 3V to +/- 15V (Negative = High, Positive = Low) |
| Synchronization | Asynchronous (Start/Stop bits) | Asynchronous (Start/Stop bits) |
A critical distinction for modern makers is the voltage level. A microcontroller outputs 3.3V or 5V TTL logic. If you connect a 3.3V ESP32 TX pin directly to a legacy RS-232 port on an industrial PLC, you will not get data, and you may fry the ESP32's GPIO due to the negative voltage swings of RS-232. You must use a level-shifting transceiver like the MAX3232 or SP3232 to bridge TTL and RS-232 domains.
Wiring, Pull-ups, and a Minimal Working Exchange
The physical wiring for UART is notoriously simple but frequently botched. You need three connections:
- TX to RX: The transmitter pin of Device A connects to the receiver pin of Device B.
- RX to TX: The receiver pin of Device A connects to the transmitter pin of Device B.
- GND to GND: A common ground is mandatory. Without it, the voltage reference floats, and the receiver cannot distinguish a logic 1 from a logic 0.
Minimal ESP32 HardwareSerial Code
The ESP32 has three hardware UARTs. UART0 is tied to the USB serial port for flashing and debugging. For external devices, use UART1 or UART2. Below is a complete, compilable example using UART2 to read a GPS module or secondary sensor.
#include <HardwareSerial.h>
// Instantiate HardwareSerial on UART2
HardwareSerial gpsSerial(2);
// ESP32 DevKit V1 safe pins for UART2
const int RX_PIN = 16;
const int TX_PIN = 17;
void setup() {
// Initialize USB serial for debug output
Serial.begin(115200);
// Initialize UART2 at 9600 baud (standard for many GPS modules)
// SERIAL_8N1 = 8 data bits, No parity, 1 stop bit
gpsSerial.begin(9600, SERIAL_8N1, RX_PIN, TX_PIN);
Serial.println("UART2 initialized. Waiting for data...");
}
void loop() {
// Pass data from the external UART device to the USB serial monitor
while (gpsSerial.available()) {
char c = gpsSerial.read();
Serial.write(c);
}
// Pass commands from the USB serial monitor to the external device
while (Serial.available()) {
char c = Serial.read();
gpsSerial.write(c);
}
}
UART vs. I2C vs. SPI: Picking the Right Protocol
When designing a board or wiring a sensor network, you must choose the right tool for the job. Which protocol fits your distance, speed, and device count requirements?
| Criteria | UART | I2C | SPI |
|---|---|---|---|
| Topology / Device Count | Point-to-Point (1 to 1) | Multi-master / Multi-slave (up to 127) | Single Master / Multi-slave (requires CS per device) |
| Wires Required | 2 + GND | 2 (SDA, SCL) + GND | 4 (MOSI, MISO, SCK, CS) + GND |
| Max Speed | ~1 Mbps (practical limit) | 3.4 Mbps (High-Speed mode) | 10+ Mbps (limited by trace capacitance) |
| Best Used For | Off-board comms, GPS, Cellular, Debugging | On-board sensors, OLEDs, low-speed peripherals | High-speed on-board (SD cards, TFT displays, Flash) |
The Decision Framework: Choose UART when communicating between two distinct boards, modules, or over moderate distances (using RS-485 transceivers for long runs). Choose I2C when you have many low-speed sensors on the same PCB and want to save GPIO pins. Choose SPI when you need to push high-bandwidth data (like streaming pixels to a display or logging to an SD card) and have plenty of GPIO pins available for chip selects.
Debugging the Bus: Classic Failures and Sniffing
Every protocol has its classic failure mode. In I2C, it is an address clash or a missing pull-up resistor on the SDA/SCL lines. In SPI, it is usually a misconfigured clock polarity (CPOL/CPHA). In UART, because there is no addressing and no shared clock, the classic failures revolve around timing and reference voltages.
1. The Baud Rate Mismatch (Garbage Characters)
If your serial monitor outputs "ÿÿÿ" or random wingdings instead of text, your baud rates do not match, or your oscillator tolerance is too high. At 115200 baud, each bit is only 8.68µs long. If your microcontroller's internal RC oscillator has a 3% error, and the receiver has a 2% error in the opposite direction, you have a 5% cumulative timing drift. By the time the receiver samples the 10th bit (the stop bit), the sampling window has shifted by nearly 50%, causing a framing error. Fix: Use an external crystal oscillator for high-baud UART, or drop to 38400 or 9600 baud where timing margins are wider.
2. The Missing Common Ground
If you are powering Device A from a USB hub and Device B from a separate wall adapter, and you only connect TX and RX, the bus will fail. The voltage difference between the two isolated grounds creates a floating reference. Fix: Always run a dedicated ground wire between the two devices, ideally using twisted pair (like 24AWG CAT5 cable) for TX/GND and RX/GND to minimize EMI.
3. Sniffing the Bus with a Logic Analyzer
When Serial.println() isn't cutting it and you suspect hardware-level corruption, you need to look at the physical waveform. A $12 USB logic analyzer (a 24MHz 8-channel Saleae clone) running the open-source PulseView / sigrok software is the ultimate UART debugging tool.
How to sniff UART properly:
- Set Sample Rate: Your sample rate must be at least 4x to 10x your baud rate to accurately capture the bit edges. For 115200 baud, set the logic analyzer to 1 MHz or 2 MHz.
- Trigger on Falling Edge: The UART start bit is a high-to-low transition. Set your trigger to the falling edge on the TX line to ensure you capture the exact beginning of the frame.
- Decode: Add the UART decoder in PulseView. Set the exact baud rate, 8 data bits, no parity, 1 stop bit. If the decoder spits out red error markers over the stop bits, your hardware is suffering from signal ringing or ground bounce. Add a 33Ω to 100Ω series termination resistor close to the TX pin to dampen the ringing.
For deeper architectural details on the ESP32's specific UART FIFO buffers and interrupt thresholds, consult the ESP32 Technical Reference Manual. Mastering UART means moving past the software abstraction and understanding the microsecond-level reality of the copper traces on your workbench.






