A UART baud rate defines the symbol transmission speed in bits per second (bps), but successfully matching it between two microcontrollers requires accounting for hardware clock drift. While standard rates like 9600 and 115200 bps are universally supported, running 115200 bps on a 16MHz ATmega328P (Arduino Uno/Nano) yields a 2.1% timing error that frequently corrupts data, whereas the ESP32’s 80MHz APB clock and fractional dividers hit the target exactly. To establish a reliable serial link, you must align the baud rate, verify the physical voltage levels, and ensure a common ground reference.
Bus Mechanics: UART vs I2C vs SPI at the Physical Layer
Before configuring registers, you must understand the physical layer. UART (Universal Asynchronous Receiver-Transmitter) is an asynchronous, point-to-point protocol. It requires a minimum of three wires: TX (Transmit), RX (Receive), and GND (Ground). Unlike synchronous buses, UART does not share a clock line; both devices must independently agree on the timing (baud rate) beforehand.
Because UART is point-to-point, it lacks the multi-drop addressing of I2C or the high-speed chip-select routing of SPI. If you need to connect multiple sensors to a single microcontroller, I2C or SPI is the correct choice. If you need to bridge two microcontrollers over a moderate distance (up to 15 meters using RS-485 transceivers, or ~1 meter for raw TTL logic), UART is the most robust and simplest to implement.
| Protocol | Wires (Min) | Typical Speed | Addressing | Max Distance (Raw) | Pull-up Resistors? |
|---|---|---|---|---|---|
| UART (TTL) | 3 (TX, RX, GND) | 9600 - 115200 bps | None (Point-to-Point) | ~1 meter | No |
| I2C | 2 (SDA, SCL) + GND | 100 kHz - 400 kHz | 7-bit / 10-bit Address | ~30 cm (high capacitance) | Yes (2.2kΩ - 4.7kΩ) |
| SPI | 4 (MOSI, MISO, SCK, CS) | 1 MHz - 20 MHz | Hardware CS Lines | ~20 cm (signal integrity) | No (usually push-pull) |
Decision framework: Choose UART when you only need to talk to one device (like a GPS module or a secondary MCU) and want simple wiring. Choose I2C for multiple low-speed sensors on the same board. Choose SPI for high-throughput data (SD cards, TFT displays).
UART Baud Rate Math, Standard Values, and Clock Drift
In UART, one symbol equals one bit, so the baud rate and bit rate are numerically identical. However, the physical generation of that baud rate depends entirely on the microcontroller's system clock and its internal baud rate generator (usually a hardware divider register).
| Baud Rate (bps) | Bit Duration (µs) | 10-bit Frame Time (ms) | Best Use Case |
|---|---|---|---|
| 9600 | 104.16 µs | 1.04 ms | Legacy devices, long cables, low-speed sensors |
| 38400 | 26.04 µs | 0.26 ms | Standard GPS modules (NMEA sentences) |
| 57600 | 17.36 µs | 0.17 ms | Bootloaders (e.g., older Arduino Optiboot) |
| 115200 | 8.68 µs | 0.086 ms | Default for ESP32/STM32, high-speed MCU-to-MCU |
| 921600 | 1.08 µs | 0.0108 ms | Audio streaming, high-res telemetry (requires DMA) |
The 115200 Baud Trap on 16MHz AVRs
The most common embedded troubleshooting headache stems from clock tolerance. The ATmega328P calculates its baud rate using the formula: UBRR = (F_CPU / (16 * Baud)) - 1.
If you target 115200 bps on a 16MHz Arduino Uno:
UBRR = (16,000,000 / (16 * 115200)) - 1 = 8.68 - 1 = 7.68- The hardware register must be an integer, so it rounds to 8.
- Plugging 8 back into the formula yields an actual baud rate of 111,111 bps.
- This results in a -3.55% error.
Most UART receivers tolerate up to ±2% drift. At -3.55%, the receiver's sampling point drifts into the wrong bit cell by the end of the byte, resulting in framing errors and garbage characters. The Arduino Serial library handles this silently, but the data will corrupt. The ESP32, documented in the Espressif UART API reference, uses an 80MHz APB clock and a fractional divider, allowing it to hit 115200 bps with near-zero error. If communicating between an Uno and an ESP32 at 115200 bps, drop the speed to 57600 bps or 38400 bps to ensure both chips stay within the ±2% tolerance window.
Wiring, Code, and a Minimal Working Exchange
Never connect a 5V Arduino TX pin directly to a 3.3V ESP32 RX pin. The ESP32 GPIOs are not 5V tolerant; doing so will degrade or destroy the input buffer over time. Use a bidirectional logic level shifter (like a BSS138 MOSFET-based module or a CD4050 buffer) or a simple resistor voltage divider on the Arduino's TX line.
| ESP32 DevKit V1 (3.3V) | Logic Level Shifter | Arduino Nano (5V) |
|---|---|---|
| GND | GND (Both sides) | GND |
| 3V3 Pin | LV (Low Voltage Ref) | - |
| 5V / VIN Pin | HV (High Voltage Ref) | 5V Pin |
| GPIO 17 (TX2) | LV1 -> HV1 | D10 (RX via SoftwareSerial) |
| GPIO 16 (RX2) | LV2 <- HV2 | D11 (TX via SoftwareSerial) |
Minimal Working Exchange Code
ESP32 (Sender): Uses HardwareSerial2 to send a structured JSON-like payload every second.
#include <HardwareSerial.h>
HardwareSerial MySerial(2); // Use UART2
void setup() {
// ESP32 baud rate set to 38400 to avoid AVR clock drift issues
MySerial.begin(38400, SERIAL_8N1, 16, 17); // RX=16, TX=17
}
void loop() {
float temp = 24.5 + (random(-10, 10) / 10.0);
MySerial.printf("{\"t\":%.1f}\n", temp);
delay(1000);
}
Arduino Nano (Receiver): Uses SoftwareSerial to read the payload. Note that SoftwareSerial on AVR struggles above 38400 bps, making 38400 the practical ceiling for this setup.
#include <SoftwareSerial.h>
SoftwareSerial MySerial(10, 11); // RX=10, TX=11
void setup() {
Serial.begin(9600); // PC Monitor
MySerial.begin(38400); // Match ESP32
}
void loop() {
if (MySerial.available()) {
char c = MySerial.read();
Serial.print(c); // Forward to PC monitor
}
}
Debugging the Bus: Sniffing and Fixing Classic Failures
When serial communication fails, the symptoms are highly specific. Unlike I2C, where a missing 4.7kΩ pull-up resistor or an address clash (two sensors hardcoded to 0x3C) halts the entire bus, UART is point-to-point. Your classic UART failures are strictly physical and timing-based.
The Classic Failure Modes
- Baud Mismatch (Garbage Characters): If your serial monitor outputs characters like
ÿ,ð, or random squares, your baud rates do not match, or the AVR clock drift is too high. Fix: Drop to 9600 bps to verify the link, then step up to 38400 bps. - Missing Common Ground: If you only connect TX and RX between two boards powered by different USB supplies, the signal reference floats. You will see intermittent framing errors or total silence. Fix: Always connect GND to GND.
- TX-to-TX Wiring Error: The most common bench mistake. TX must go to RX. If both TX pins are driving the line simultaneously, you create a bus contention that can overheat the GPIO drivers. Fix: Swap the TX/RX wires.
How to Sniff and Debug the Bus
When code and wiring checks fail, you must look at the physical signal. Do not rely on a multimeter; a UART pulse at 115200 bps lasts only 8.68 microseconds, which a multimeter will average out to a meaningless DC voltage.
Use a logic analyzer. A $12 FX2LAE 8-channel clone or a professional Saleae Logic Pro will decode the bus instantly. According to the Saleae Async Serial Analyzer guide, you must set your sample rate correctly to capture the edges.
By combining strict adherence to clock tolerance math, proper voltage level shifting, and logic analyzer verification, you can eliminate serial communication guesswork and build robust multi-microcontroller systems.






