The Universal Asynchronous Receiver-Transmitter (UART) is the oldest and most straightforward serial communication protocol in embedded systems. Unlike synchronous buses that rely on a shared clock line, UART operates asynchronously, embedding timing directly into the data stream via agreed-upon baud rates. It is a strictly point-to-point interface, meaning one transmitter speaks to exactly one receiver. If you need to connect a microcontroller to a GPS module, a cellular modem, or a legacy PC serial port, the UART interface is your default tool.
UART Interface Bus Mechanics and Physical Layer
Before wiring any pins, you must understand where UART fits in the embedded protocol landscape. The table below contrasts the physical and logical mechanics of the four most common microcontroller buses.
| Protocol | Wires (Excl. GND) | Max Speed (Typical) | Addressing / Topology | Max Distance (Typical) |
|---|---|---|---|---|
| UART (TTL) | 2 (TX, RX) | ~1 Mbps (Usually 115200) | None (Point-to-Point) | < 1 meter |
| I2C | 2 (SDA, SCL) | 100 kHz to 3.4 MHz | 7-bit / 10-bit (Multi-master) | < 1 meter |
| SPI | 3 shared + CS | 10 MHz to 50+ MHz | Hardware CS lines (Master-Slave) | < 1 meter |
| CAN Bus | 2 (CANH, CANL) | 1 Mbps (Classic CAN) | Message ID Arbitration (Multi-node) | 40m @ 1Mbps / 1km @ 50kbps |
Physical Wiring and the "Missing Pull-Up" Myth
A standard TTL UART interface requires three physical connections: TX (Transmit), RX (Receive), and GND (Common Ground). The most critical rule of UART wiring is that the TX pin of Device A must connect to the RX pin of Device B, and vice versa.
Beginners often ask if UART requires external pull-up resistors. It does not. This is a common confusion carried over from I2C. I2C uses open-drain outputs, requiring external pull-ups to bring the line high. UART TX lines are actively driven push-pull outputs by the microcontroller's internal UART peripheral. The line idles HIGH (at VCC) and is actively pulled LOW to signal a start bit. Adding external pull-ups to a UART TX line is unnecessary and can actually cause contention if the driving MCU attempts to pull the line low against a strong external resistor.
Unlike differential buses (RS-485, CAN), TTL UART is single-ended. The receiver measures the voltage on the RX pin relative to its own ground. If you do not connect the GND pins of both devices, the voltage reference floats, resulting in garbage data or missed frames. Always run a ground wire alongside your TX/RX pair.
Protocol Selection and Classic Bus Failures
Choosing the right protocol depends entirely on your distance, speed, and device count requirements. Choose UART when you have exactly two devices communicating over a short distance (<1m) at low-to-medium speeds, and you want to avoid the overhead of clock routing or address management. Choose I2C for connecting dozens of low-speed sensors on the same two bus wires. Choose SPI when you need high bandwidth (e.g., TFT displays, SD cards). Choose CAN for noisy industrial environments or long-distance multi-node networks.
Diagnosing the Classic Failures
When a bus fails, the symptoms usually map directly to the protocol's physical layer design. Here is how the classic failures break down across protocols:
- Baud Mismatch (The UART Killer): Because UART lacks a clock line, both devices must agree on the exact bit timing (baud rate). A 9600 bps transmitter talking to a 115200 bps receiver will yield pure garbage. Edge case: Cheap CH340 or CP2102 USB-to-Serial clone chips often suffer from clock divider math errors. When you request 115200 baud, the clone chip might actually run at 115000 baud. If your microcontroller is running at a true 115200, the 0.17% drift over an 11-bit frame (start + 8 data + stop) can push the sampling point past the bit boundary, causing intermittent framing errors. Fix this by dropping both sides to 57600 or 38400 baud.
- Missing Pull-Up (The I2C Killer): As noted, this does not apply to UART. If your I2C bus is stuck low or reading 0xFF, check your 4.7kΩ pull-ups to VCC.
- Address Clash (The I2C/SPI Killer): UART is point-to-point, so address clashes are physically impossible. If you are using I2C and two sensors share the same hardcoded 7-bit address (e.g., two BME280s both at 0x76), the bus will deadlock. You must use an I2C multiplexer (like the TCA9548A) or pull the SDO pin high on one sensor to shift its address to 0x77.
Wiring the UART Interface and Minimal Working Exchange
Let's wire a 3.3V ESP32 to a 5V Arduino Pro Mini. Because the ESP32 GPIOs are strictly 3.3V tolerant, feeding a 5V TX signal into an ESP32 RX pin will degrade or destroy the silicon over time. We use a bidirectional logic level converter (BSS138 MOSFET-based) to translate the voltages safely.
| ESP32 (3.3V Side) | Level Shifter | Arduino Pro Mini (5V Side) |
|---|---|---|
| GPIO 17 (TX2) | LV1 → HV1 | Pin 10 (RX) |
| GPIO 16 (RX2) | LV2 → HV2 | Pin 11 (TX) |
| GND | GND (Both sides) | GND |
| 3V3 Out | LV (Low Voltage Ref) | - |
| - | HV (High Voltage Ref) | VCC (5V) |
Minimal Working Code Exchange
The ESP32 uses its hardware UART2 peripheral, while the Arduino uses SoftwareSerial to free up the hardware UART for USB debugging.
// --- ESP32 Transmitter Code (Hardware Serial2) ---
#define ESP_TX 17
#define ESP_RX 16
void setup() {
// Initialize hardware UART2 at 9600 baud
Serial2.begin(9600, SERIAL_8N1, ESP_RX, ESP_TX);
}
void loop() {
Serial2.println("PING: Sensor Data OK");
delay(1000);
}
// --- Arduino Pro Mini Receiver Code (SoftwareSerial) ---
#include <SoftwareSerial.h>
// RX is Pin 10, TX is Pin 11
SoftwareSerial mySerial(10, 11);
void setup() {
Serial.begin(9600); // USB debug monitor
mySerial.begin(9600); // UART interface to ESP32
}
void loop() {
if (mySerial.available()) {
String data = mySerial.readStringUntil('\n');
Serial.print("Received from ESP32: ");
Serial.println(data);
}
}
Sniffing and Debugging the Bus
When your serial terminal shows empty space or Wingdings-style garbage, you must move from software debugging to physical layer inspection. According to the Espressif ESP-IDF UART documentation, the hardware peripheral handles framing automatically, meaning errors are almost always electrical or configuration-based.
Step 1: The Multimeter Idle Test
Set your multimeter to DC voltage. With the devices powered but idle (no data being sent), probe the TX and RX lines. Both should read close to VCC (3.3V or 5V). If a line reads 0V while idle, the MCU pin is configured as an output and stuck low, or the line is shorted to ground. If it reads ~1.5V, you likely have a missing common ground or a floating pin.
Step 2: Logic Analyzer Decoding
A standard logic analyzer (like a Saleae Logic or a cheap $10 24MHz 8-channel clone) running Sigrok/PulseView is the ultimate UART debugging tool. Connect the analyzer ground to your bus ground, and clip Channel 0 to the TX line.
- Capture the Start Bit: UART data is LSB (Least Significant Bit) first. The line will drop from HIGH to LOW for exactly one bit period. At 9600 baud, one bit is ~104µs.
- Read the Data Bits: You will see 8 transitions representing the ASCII character. For example, the letter 'U' (0x55) will toggle HIGH/LOW/HIGH/LOW perfectly, creating a square wave.
- Verify the Stop Bit: The line must return HIGH and stay high for at least one bit period. If the line stays low after the 8th data bit, you have a framing error, usually caused by a baud rate mismatch where the receiver's sampling window has drifted into the next bit.
By visually measuring the width of the start bit in your logic analyzer software, you can calculate the exact baud rate the transmitter is using. Divide 1 second by the measured pulse width in seconds. If the transmitter is outputting 115000 baud instead of 115200, you will see the discrepancy immediately on the scope, saving hours of software troubleshooting. Always verify the physical layer before rewriting your firmware.






