If you are wiring microcontrollers to sensors, displays, or memory, you need to pick the right bus. For short-distance, multi-device sensor networks on a single PCB, use I2C. For high-speed memory or TFT displays, use SPI. For point-to-point debug consoles or long-distance RS-485 networks, use UART. Choosing the wrong one leads to bus contention, bricked logic pins, or impossible wiring harnesses. This guide cuts through the software abstractions and focuses on the physical layer, pull-up math, and hardware realities of these serial protocols.
Bus Mechanics: I2C, SPI, and UART at the Silicon Level
Before writing a single line of code, you must understand the silicon constraints. Each protocol trades off wire count, speed, and addressing complexity. Here is the hardware reality of the big three serial protocols.
| Feature | I2C (Inter-Integrated Circuit) | SPI (Serial Peripheral Interface) | UART (Universal Asynchronous Receiver-Transmitter) |
|---|---|---|---|
| Wires Required | 2 (SDA, SCL) + GND | 4 (MOSI, MISO, SCK, CS) + GND | 2 (TX, RX) + GND |
| Max Speed (Typical) | 100 kHz / 400 kHz / 1 MHz | 10 MHz to 50+ MHz | 9600 to 115,200 baud (up to 1 Mbps) |
| Addressing | 7-bit or 10-bit hardware address | None (uses individual Chip Select lines) | None (strictly point-to-point) |
| Max Distance | < 1 meter (highly capacitance-limited) | < 0.5 meters (signal integrity degrades fast) | < 15m (RS-232) or 1200m (RS-485) |
| Topology | Multi-master, multi-slave bus | Single master, multi-slave (daisy-chain possible) | Point-to-point only |
Physical Wiring, Pull-Ups, and Logic Levels
The most common reason serial protocols fail on the bench is ignoring the physical layer. Software libraries assume perfect hardware; your breadboard does not.
I2C Pull-Up Resistor Math
I2C uses open-drain outputs. The microcontroller can pull the SDA and SCL lines to ground, but it cannot drive them high. Without pull-up resistors, the lines float, and the bus locks up. The NXP I2C Specification (UM10204) dictates that the pull-up resistor value depends on bus capacitance and speed.
- 100 kHz (Standard Mode): Use 4.7 kΩ pull-ups to VCC.
- 400 kHz (Fast Mode): Use 2.2 kΩ pull-ups to VCC to overcome bus capacitance and achieve fast rise times.
- 1 MHz (Fast Mode Plus): Use 1.0 kΩ pull-ups.
Hard limit: Standard I2C caps bus capacitance at 400 pF. If you run long wires or attach more than 4-5 devices, the capacitance exceeds this limit, rounding off your square waves into unusable slopes.
SPI Chip Select (CS) Management
SPI does not use addresses. Every slave needs its own Chip Select (CS) line. If you forget to wire a CS line, or leave it floating, the slave will drive the MISO line continuously, causing a bus collision when another device tries to talk. Always tie unused SPI MISO pins to a high-impedance state via the CS pin.
The 5V vs 3.3V Logic Level Trap
The Classic Failures: Debugging and Sniffing the Bus
When your sensor returns -1 or garbage data, do not rewrite your code. Check the physics. Here are the classic failure modes and how to sniff them.
1. Missing Pull-Ups (I2C)
Symptom: Wire library returns I2C_NACK or hangs indefinitely on Wire.endTransmission().
Measurement: Put your multimeter in DC voltage mode. Probe SDA and SCL. If they read 0.0V or fluctuate randomly instead of sitting firmly at VCC (3.3V or 5V), your pull-ups are missing or broken.
Fix: Solder 4.7 kΩ resistors from SDA to VCC and SCL to VCC.
2. Baud Rate Mismatch (UART)
Symptom: Serial monitor prints ??, 0xFF, or unreadable Wingdings.
Measurement: Hook a logic analyzer to the TX line. Measure the width of the start bit (the first low pulse). If the pulse is 104 µs, your baud rate is 9600. If it is 8.68 µs, your baud rate is 115200.
Fix: Match the host terminal to the measured baud rate. Ensure both devices share a common Ground (GND) reference.
3. Clock Polarity/Phase Mismatch (SPI)
Symptom: Data reads as all zeros, or shifted by one bit.
Measurement: Use a logic analyzer like the Saleae Logic Pro 8 or a DSLogic Plus running Sigrok PulseView. Decode the SPI traffic. Check if data is sampled on the rising or falling edge of the SCK line.
Fix: Adjust the SPI mode (CPOL and CPHA) in your microcontroller code to match the sensor's datasheet. Mode 0 (CPOL=0, CPHA=0) is most common, but SD cards often require Mode 3.
Minimal Working Exchange: ESP32 to BME280 via I2C
Here is a complete, copy-pasteable hardware and software setup to read a BME280 environmental sensor using an ESP32 DevKit v1. This proves the physical layer is working before you add software complexity.
Wiring Table
| ESP32 DevKit v1 Pin | BME280 Breakout Pin | Notes |
|---|---|---|
| 3V3 | VIN / VCC | Do not use 5V on a 3.3V sensor breakout. |
| GND | GND | Common ground is mandatory. |
| GPIO 21 (Default SDA) | SDA | Requires 4.7kΩ pull-up to 3V3. |
| GPIO 22 (Default SCL) | SCL | Requires 4.7kΩ pull-up to 3V3. |
Arduino C++ Code
#include <Wire.h>
#include <Adafruit_BME280.h>
Adafruit_BME280 bme;
void setup() {
Serial.begin(115200);
// Initialize I2C with explicit pins and 400kHz Fast Mode
if (!bme.begin(0x76, &Wire, 400000)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1) { delay(10); } // Halt execution safely
}
Serial.println("BME280 I2C Bus Initialized.");
}
void loop() {
Serial.print("Temperature = ");
Serial.print(bme.readTemperature());
Serial.println(" *C");
delay(2000);
}
The Decision Tree: Pick Your Protocol
Stop debating which protocol is 'best.' The physics of your project dictate the choice. Follow this decision path to terminate on a concrete hardware pick.
| If your requirement is... | Then choose... | Concrete Hardware Implementation |
|---|---|---|
| Distance > 10 meters | UART (RS-485) | Use a MAX485 transceiver chip. Wire twisted pair (Cat5e) with a 120Ω termination resistor at the far end. |
| Throughput > 5 Mbps (TFT displays, SD cards, external Flash) | SPI | Use hardware SPI pins. Keep MISO/MOSI/SCK traces under 10cm on the PCB. Add 100nF decoupling caps directly on the slave VCC pin. |
| Connecting >3 low-speed sensors on limited GPIO pins | I2C | Wire SDA/SCL with 4.7kΩ pull-ups. If you run out of I2C addresses, use a TCA9548A I2C multiplexer to create 8 separate sub-buses. |
| Need more UART ports but your MCU only has one | I2C to UART Bridge | Use an SC16IS750 IC. It acts as an I2C slave but provides a fully functional hardware UART TX/RX output. |
By respecting the physical layer—calculating pull-ups, managing logic levels, and matching bus capacitance to speed—you eliminate the 'ghost in the machine' errors that plague embedded development. Wire the hardware correctly, verify the clock edges with a logic analyzer, and your serial protocols will run flawlessly.






