Every microcontroller project reaches the same fork: how do you connect a sensor, display, memory chip, or second MCU? UART (Serial), I2C, and SPI are the three workhorse protocols for chip-to-chip communication. Each trades wiring simplicity, speed, and scalability differently. Pick wrong, and you fight bus contention, slow refresh rates, or a pin count your MCU cannot support.
Quick Takeaways
- UART needs 2 wires (TX, RX), has no clock, and suits point-to-point links such as GPS modules, Bluetooth modules, and debug consoles.
- I2C needs 2 wires (SDA, SCL) and supports up to 127 addressable devices on one bus. It suits slow-to-medium sensors and small displays.
- SPI needs 4 wires plus one CS line per device. It is the fastest of the three (often 10-80 MHz), so use it for displays, SD cards, and flash memory.
- Choose by constraint: pin count favors I2C, throughput favors SPI, and simple two-device links favor UART.
| Feature | UART | I2C | SPI |
|---|---|---|---|
| Wires | 2 (TX, RX) | 2 (SDA, SCL) | 4+ (MOSI, MISO, SCK, CS) |
| Clock | Asynchronous | Synchronous (shared) | Synchronous (shared) |
| Typical speed | 9600-115200 baud (up to ~3 Mbps) | 100 kHz / 400 kHz / 1 MHz | 1-80+ MHz |
| Topology | Point-to-point | Multi-master, multi-slave | Single master, multi-slave |
| Addressing | None | 7-bit or 10-bit | Hardware CS pin |
| Duplex | Full | Half | Full |
| Typical range | Meters (RS-232/RS-485 for more) | Centimeters to ~1 m | Centimeters |
| Complexity | Low | Medium | Low-Medium |
How Serial Communication Works
Digital devices exchange bits in one of two ways. Parallel sends many bits at once over many wires. Serial sends bits one after another over few wires. All three protocols here are serial. They differ in how sender and receiver agree on bit timing.
Two categories matter:
- Asynchronous: No shared clock. Both sides agree on a baud rate in advance (UART).
- Synchronous: A clock line shares timing between devices (I2C, SPI).
UART (Universal Asynchronous Receiver-Transmitter)
How It Works
UART links exactly two devices. Cross the lines: one device’s TX connects to the other’s RX, and a GND wire is shared. Idle state is logic high. Each frame contains:
- One start bit (logic low)
- 5-9 data bits (usually 8, LSB first)
- Optional parity bit
- 1 or 2 stop bits (logic high)
The common setting is 8N1: 8 data bits, no parity, 1 stop bit. Both devices must match baud rate within about 2-3%, or you get framing errors and garbage characters.
Strengths and Limits
- No clock wire and simple hardware.
- Full duplex: TX and RX operate simultaneously.
- Only two devices per port without extra hardware.
- Frame overhead (start/stop bits) wastes about 20% of bandwidth in 8N1.
- Clock drift between two independent oscillators limits reliable baud rate.
Voltage Levels Matter
Logic-level UART runs at 3.3V or 5V. RS-232 uses ±3V to ±15V and is not directly compatible with MCU pins. A level shifter such as the MAX3232 is required. For long industrial runs, RS-485 (differential, up to ~1200 m) is the standard upgrade.
UART Code Example (Arduino C++)
// Read GPS NMEA sentences from a module on a second UART (ESP32 example)
#define GPS_RX 16 // ESP32 GPIO16 connects to GPS TX
#define GPS_TX 17 // ESP32 GPIO17 connects to GPS RX
void setup() {
Serial.begin(115200); // USB debug console
Serial2.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX); // GPS default: 9600 baud, 8N1
}
void loop() {
while (Serial2.available()) { // Bytes waiting in RX buffer?
char c = Serial2.read(); // Read one byte
Serial.write(c); // Forward to the PC terminal
}
}
I2C (Inter-Integrated Circuit)
How It Works
Philips (now NXP) created I2C for short on-board links. Two open-drain lines carry everything:
- SDA: Serial data (bidirectional)
- SCL: Serial clock (driven by the master)
Both lines need pull-up resistors to VCC because devices only pull lines low. Typical values: 4.7 kΩ at 100 kHz and 2.2 kΩ to 4.7 kΩ at 400 kHz on 3.3V or 5V systems.
A transaction follows this sequence:
- Master sends a START condition (SDA falls while SCL is high).
- Master sends a 7-bit address plus a R/W bit.
- The addressed slave pulls SDA low for an ACK.
- Data bytes move in the chosen direction, each followed by an ACK/NACK.
- Master sends a STOP condition (SDA rises while SCL is high).
Speed Modes
| Mode | Max Speed |
|---|---|
| Standard | 100 kbit/s |
| Fast | 400 kbit/s |
| Fast-mode Plus | 1 Mbit/s |
| High-speed | 3.4 Mbit/s |
Strengths and Limits
- Two wires serve many devices, which saves GPIO pins.
- Built-in addressing and acknowledgment.
- Multi-master capable, with arbitration.
- Slower than SPI, and half duplex.
- Bus capacitance is limited to 400 pF, which caps cable length and device count.
- Address conflicts happen when two identical modules share a fixed address (for example, two MPU6050 boards at 0x68).
Scanning the Bus (Arduino C++)
#include <Wire.h>
void setup() {
Serial.begin(115200);
Wire.begin(); // Join bus as master; ESP32 defaults SDA=GPIO21, SCL=GPIO22
Wire.setClock(400000); // Fast mode: 400 kHz
}
void loop() {
Serial.println("Scanning I2C bus...");
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) { // 0 = ACK received, device present
Serial.print("Found device at 0x");
Serial.println(addr, HEX);
}
}
delay(3000);
}
Common I2C Failures
| Symptom | Likely Cause | Fix |
|---|---|---|
| Scanner finds nothing | Missing pull-ups, swapped SDA/SCL | Add 4.7 kΩ pull-ups; check wiring |
| Random NACKs | Bus too long or too much capacitance | Shorten wires, lower clock, reduce pull-up value |
| Two devices clash | Identical fixed address | Use address-select pins or an TCA9548A multiplexer |
| Works at 3.3V, fails at 5V | Level mismatch | Use a bidirectional level shifter (BSS138-based) |
SPI (Serial Peripheral Interface)
How It Works
Motorola introduced SPI as a fast, simple, full-duplex bus. One master controls the clock, and slaves are selected by dedicated lines:
- MOSI: Master Out, Slave In
- MISO: Master In, Slave Out
- SCK: Serial clock
- CS/SS: Chip Select (active low, one per slave)
Because MOSI and MISO are separate, data moves in both directions on every clock cycle. No start/stop framing and no addressing overhead make SPI very efficient.
SPI Modes
The two settings CPOL (clock idle polarity) and CPHA (sampling edge) give four modes. Mismatched modes are the top cause of SPI failures.
| Mode | CPOL | CPHA | Clock Idle | Data Sampled On |
|---|---|---|---|---|
| 0 | 0 | 0 | Low | Rising edge |
| 1 | 0 | 1 | Low | Falling edge |
| 2 | 1 | 0 | High | Falling edge |
| 3 | 1 | 1 | High | Rising edge |
Check the device datasheet. Most sensors and SD cards use Mode 0 or Mode 3.
Strengths and Limits
- Highest throughput of the three, often 10-80 MHz.
- Full duplex and no protocol overhead.
- Simple slave hardware, since slaves need no address logic.
- Each added slave costs one extra CS pin.
- No acknowledgment, so the master cannot tell whether a slave exists.
- No standard error checking.
- Short range, typically on-board only.
SPI Code Example (Arduino C++)
#include <SPI.h>
const int CS_PIN = 5; // Chip select for the target device
void setup() {
Serial.begin(115200);
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect device (CS idles high)
SPI.begin(); // Default ESP32 VSPI: SCK=18, MISO=19, MOSI=23
}
byte readRegister(byte reg) {
// 8 MHz clock, MSB first, SPI Mode 0
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_PIN, LOW); // Select device
SPI.transfer(reg | 0x80); // Many chips set bit 7 to signal a read
byte value = SPI.transfer(0x00); // Send dummy byte, clock in the reply
digitalWrite(CS_PIN, HIGH); // Deselect device
SPI.endTransaction();
return value;
}
void loop() {
Serial.println(readRegister(0x0F), HEX); // Example: WHO_AM_I register
delay(1000);
}
Head-to-Head: Choosing the Right Protocol
| Criterion | Winner | Why |
|---|---|---|
| Fewest wires for many devices | I2C | 2 wires, up to 127 devices |
| Maximum speed | SPI | Tens of MHz, no overhead |
| Simplest two-device link | UART | No clock, no addressing |
| Longest distance | UART (with RS-485) | Differential signaling reaches ~1200 m |
| Lowest power on idle bus | I2C / UART | No active clock when idle |
| Easiest to debug | UART | Readable on any serial terminal |
| Full duplex streaming | SPI / UART | Separate data lines |
| Hot-add devices | I2C | Address-based, no extra CS pins |
Decision Guide
- Need to stream high-bandwidth data (TFT display, SD card, camera, flash)? Use SPI.
- Reading several low-speed sensors (temperature, IMU, RTC, OLED)? Use I2C.
- Talking to a GPS, Bluetooth/Wi-Fi AT module, or another MCU? Use UART.
- Running short of GPIO? Use I2C, or an I/O expander.
- Crossing a long cable in a noisy environment? Use UART over RS-485 (or CAN).
Real-World Project: Multi-Protocol Environmental Logger
One ESP32 can use all three protocols at once. This build logs air data to an SD card and shows it on a display.
| Component | Protocol | Pins (ESP32) |
|---|---|---|
| BME280 (temp/humidity/pressure) | I2C, addr 0x76 | SDA GPIO21, SCL GPIO22 |
| SSD1306 OLED display | I2C, addr 0x3C | Shared with BME280 |
| MicroSD card module | SPI | SCK GPIO18, MISO GPIO19, MOSI GPIO23, CS GPIO5 |
| NEO-6M GPS | UART2 | RX GPIO16, TX GPIO17 |
Wiring Notes
- Power the BME280, SSD1306, and NEO-6M from 3.3V. Many SD modules include a 5V regulator and level shifter, so check yours before connecting.
- Both I2C devices share one bus because their addresses differ.
- Add a 100 nF decoupling capacitor near each IC’s VCC pin.
- Keep SPI wires short (under about 15 cm) to avoid ringing at higher clock rates.
Firmware Skeleton
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <Adafruit_BME280.h>
Adafruit_BME280 bme;
const int SD_CS = 5;
void setup() {
Serial.begin(115200);
Serial2.begin(9600, SERIAL_8N1, 16, 17); // UART: GPS
Wire.begin(21, 22); // I2C: sensor + display
if (!bme.begin(0x76)) Serial.println("BME280 not found");
if (!SD.begin(SD_CS)) Serial.println("SD init failed"); // SPI: storage
}
void loop() {
float t = bme.readTemperature(); // I2C read
File f = SD.open("/log.csv", FILE_APPEND); // SPI write
if (f) {
f.printf("%.2f\n", t);
f.close();
}
delay(5000);
}
Troubleshooting Checklist
UART
- Cross TX to RX. Straight TX-to-TX gives silence.
- Confirm matching baud rate and 8N1 settings.
- Join grounds between devices.
- Never connect a 5V TX directly to a 3.3V RX pin.
I2C
- Verify pull-up resistors exist (one pair per bus, not per device).
- Run a bus scanner to confirm addresses.
- Drop to 100 kHz while debugging.
- Check a logic analyzer for a stuck-low SDA after a crashed transaction.
SPI
- Match CPOL/CPHA to the datasheet.
- Make sure only one CS is low at a time.
- Keep CS high during boot to prevent bus contention.
- Lower the clock speed if reads are intermittent.
A cheap 8-channel logic analyzer with Sigrok/PulseView decodes all three protocols and saves hours of guesswork.
FAQ
Is I2C faster than SPI?
No. SPI is faster. Standard I2C tops out at 100-400 kHz (3.4 MHz in High-speed mode), while SPI commonly runs 10-80 MHz with no addressing or acknowledgment overhead.
Can I use I2C, SPI, and UART together on the same microcontroller?
Yes. They use separate peripherals and pins, so they run concurrently. Most MCUs, including the ESP32, STM32, and ATmega328P-class boards, have all three built in.
How many devices can I connect to each protocol?
I2C supports up to 127 devices with 7-bit addressing, limited in practice by 400 pF bus capacitance. SPI supports as many devices as you have CS pins. UART supports one device per port, or several using RS-485 multi-drop.
Which protocol is best for beginners?
UART is the easiest, since you can read traffic in any serial monitor. I2C comes next because the libraries handle addressing for you. SPI takes more care with modes and chip-select wiring.




