The practical speed of UART depends entirely on the physical layer. Over standard TTL/CMOS logic on a breadboard, modern microcontrollers like the ESP32 or STM32 can push hardware UART to 1 Mbps or even 3 Mbps. Over RS-232, 115,200 baud is the reliable ceiling. However, raw baud rate is only half the equation; clock tolerance, cable capacitance, and framing overhead dictate your actual throughput. With standard 8N1 framing (8 data bits, no parity, 1 stop bit), a 115,200 baud connection yields exactly 11,520 bytes per second.
UART Bus Mechanics and Speed Limits
Unlike synchronous protocols that rely on a shared clock line, UART is asynchronous. The receiver must sample the data line at the exact center of each bit period based on a pre-agreed baud rate. As the speed of UART increases, the bit duration shrinks, making the bus highly susceptible to cable capacitance and clock jitter.
| Baud Rate | Bit Duration | 8N1 Byte Rate | Max Practical Distance | Clock Tolerance Required |
|---|---|---|---|---|
| 9600 | 104.16 µs | 960 B/s | ~15 meters (50 ft) | ±2.0% |
| 115200 | 8.68 µs | 11,520 B/s | ~1.5 meters (5 ft) | ±1.5% |
| 921600 | 1.08 µs | 92,160 B/s | ~0.5 meters (1.5 ft) | ±1.0% |
| 3000000 | 0.33 µs | 300,000 B/s | PCB traces only (<10 cm) | ±0.5% (Crystal required) |
Hardware Note: If you attempt 3 Mbps on an Arduino Nano using its standard 16MHz ceramic resonator, the inherent frequency drift will cause framing errors. For speeds above 921,600 baud, you must use a microcontroller with a precision crystal oscillator, or shift to a differential physical layer like RS-485 using a transceiver like the MAX485 with a 120-ohm termination resistor.
Protocol Selection: UART vs. I2C vs. SPI vs. CAN
Choosing the right bus requires balancing distance, speed, and device count. UART is strictly point-to-point (or multi-drop with RS-485), while I2C and SPI are board-level multi-device buses.
| Protocol | Wires Required | Max Speed | Addressing / Topology | Max Distance |
|---|---|---|---|---|
| UART (TTL) | 2 (TX/RX) + GND | ~3 Mbps | None (Point-to-Point) | <1.5m (TTL), 1200m (RS-485) |
| I2C | 2 (SDA/SCL) + GND | 3.4 Mbps (Ultra Fast) | 7-bit / 10-bit Address | ~1 meter (Capacitance limited) |
| SPI | 4 (MOSI/MISO/SCK/CS) | >50 MHz | Hardware CS (Chip Select) | <0.5 meters (Signal integrity) |
| CAN Bus | 2 (CANH/CANL) + GND | 1 Mbps (Classic) | 11-bit / 29-bit Message ID | 40m @ 1Mbps, 5km @ 125kbps |
Decision Framework: Choose UART when connecting two distinct systems (e.g., an ESP32 to a GPS module or a PC serial console). Choose I2C for low-speed onboard sensors sharing the same ground plane. Choose SPI when you need to move large blocks of data quickly (e.g., TFT displays, SD cards). Choose CAN for noisy industrial or automotive environments where noise immunity and long distances are mandatory.
Physical Wiring, Minimal Exchange, and Classic Failures
Standard TTL UART requires three connections: TX to RX, RX to TX, and a shared GND. Crucially, UART does not require pull-up resistors on the data lines. It uses push-pull CMOS outputs. Adding pull-ups to a standard UART line will cause excessive current draw and logic level distortion. (For a deep dive into the physical layer, see SparkFun's UART tutorial).
Wiring: ESP32 GPIO 17 (TX2) → Arduino Pin 0 (RX). ESP32 GPIO 16 (RX2) → Arduino Pin 1 (TX). Connect GND to GND.
Note: Disconnect Arduino pins 0/1 when uploading sketches via USB, as they share the hardware UART.
// ESP32 Transmitter (Hardware Serial 2)
#define RXD2 16
#define TXD2 17
void setup() {
// Initialize UART at 115200 baud, 8N1 framing
Serial2.begin(115200, SERIAL_8N1, RXD2, TXD2);
}
void loop() {
Serial2.println("SENSOR_DATA: 42.5");
delay(1000);
}
// ---------------------------------------------------------
// Arduino Uno Receiver (Hardware Serial 0)
void setup() {
Serial.begin(115200);
}
void loop() {
if (Serial.available() > 0) {
String incoming = Serial.readStringUntil('\n');
// Basic error handling: check if string is empty or timed out
if (incoming.length() > 0) {
// Process data
}
}
}
The Classic Failures
- Baud Mismatch (UART): If the transmitter sends at 115200 and the receiver listens at 9600, you will see garbage characters like
ÿor???. The receiver is sampling the start bit at the wrong time, misaligning the entire byte. Fix: Verify bothSerial.begin()arguments. If using high baud rates, check your microcontroller's clock source tolerance. - Missing Pull-Up (I2C): Unlike UART, I2C uses open-drain outputs. If you forget the 4.7kΩ pull-up resistors on SDA and SCL, the lines will float, the bus will hang, and
Wire.endTransmission()will stall indefinitely. - Address Clash (I2C): If you wire two identical sensors (e.g., two BME280s) without changing the SDO/CSB pin state on one of them, they will both answer to
0x76. The bus will corrupt the data via arbitration loss. Fix: Run an I2C scanner sketch and ensure all expected addresses appear exactly once.
Sniffing and Debugging the Bus
When your serial monitor shows nothing, or worse, intermittent corrupted data, you must look at the physical signal. Do not rely solely on software serial monitors for debugging hardware faults.
- Get a Logic Analyzer: A $15 Saleae-compatible 8-channel USB logic analyzer is sufficient for UART speeds up to 3 Mbps. For higher speeds or analog noise inspection, use a digital storage oscilloscope (DSO) with at least 100 MHz bandwidth.
- Software Setup: Use PulseView (the GUI for the Sigrok project). Connect Channel 0 to the TX line and the ground clip to the shared GND.
- Trigger and Decode: Set the trigger to the falling edge on Channel 0 (this catches the UART Start Bit, which is always a transition from HIGH to LOW). Add the UART protocol decoder, set it to 8N1, and input your expected baud rate.
- Measure True Baud Rate: If the decoder shows framing errors, use the cursor tool to measure the exact time from the start bit to the first data bit. If your measured bit time is 8.8 µs instead of the expected 8.68 µs for 115200 baud, your actual speed is ~113,600 baud. Adjust your receiver's baud rate to match the transmitter's actual clock output, or fix the transmitter's clock source.
By treating the speed of UART as a physical constraint rather than just a software parameter, you eliminate the most common embedded communication bottlenecks and ensure reliable data transfer across your projects.






