When selecting communication protocols in embedded systems, the decision is dictated entirely by physical layer constraints: use I2C for multi-master sensor networks under 1 meter, SPI for high-speed local peripherals like TFT displays, and UART for point-to-point asynchronous links or PC debugging. Abstract protocol comparisons are useless without understanding the copper, the pull-up resistors, and the bus capacitance that actually make the silicon talk.
The Physical Layer: Wiring, Pull-Ups, and Bus Mechanics
Every embedded protocol is ultimately bound by physics. Before looking at software libraries, you must understand the electrical topology of the bus. I2C relies on open-drain outputs, meaning the microcontroller can only pull the line low; it requires external pull-up resistors to bring the line high. SPI and UART use push-pull outputs, actively driving the line both high and low, which allows for higher speeds but restricts bus topology.
Do not blindly use 10kΩ resistors for 3.3V I2C buses. The I2C specification (UM10204) dictates a maximum rise time of 300ns for Fast Mode (400kHz). If your bus has high capacitance (long wires, multiple sensors), 10kΩ will result in a sluggish RC rise time, causing data corruption. Drop to 4.7kΩ or even 2.2kΩ for 3.3V systems to ensure sharp edges, ensuring you stay above the minimum sink current ($I_{OL}$) of 3mA.
| Protocol | Wires Required | Max Speed (Typical) | Addressing Method | Max Practical Distance | Physical Termination / Pull-Up Requirement |
|---|---|---|---|---|---|
| I2C | 2 (SDA, SCL) | 100kHz / 400kHz / 1MHz | 7-bit or 10-bit hardware address | < 1 meter (Limited by ~400pF bus capacitance) | Open-drain. Requires 2.2kΩ - 10kΩ pull-ups to VCC on both SDA and SCL. |
| SPI | 4 (MOSI, MISO, SCK, CS) | 10MHz - 50MHz+ | Individual Chip Select (CS) line per device | < 0.5 meters (Signal integrity/crosstalk limits) | Push-pull. No pull-ups required on data lines. 10kΩ pull-up on CS (active low) prevents glitches during MCU boot. |
| UART | 2 (TX, RX) + GND | 9600 to 115200 baud (up to 3Mbps) | None (Point-to-Point) | < 15 meters (at 9600 baud); < 1m at 1Mbps | Push-pull. Idle state is HIGH. No pull-ups strictly required, but 10kΩ on RX prevents floating noise if TX is disconnected. |
| RS-485 | 2 (A, B differential) + GND | Up to 10 Mbps | None (Hardware layer only) | Up to 1200 meters | Differential driver. Requires 120Ω termination resistor across A and B at both ends of the cable. |
Protocol Selection: Matching Speed, Distance, and Device Count
Choosing the right protocol requires balancing pin count, throughput, and node quantity. Here is how to map your project requirements to the correct bus.
When to use I2C (Inter-Integrated Circuit)
Choose I2C when you need to connect multiple low-speed sensors (temperature, humidity, IMUs) to a single microcontroller using minimal GPIO pins. Because it uses a shared bus architecture, you can daisy-chain dozens of devices on just two wires (SDA and SCL). The limiting factor is the 7-bit address space (yielding 112 usable addresses) and bus capacitance. If you exceed 400pF of capacitance—roughly equivalent to 30cm of ribbon cable and three sensors—the signal edges degrade.
When to use SPI (Serial Peripheral Interface)
Choose SPI when throughput is critical. If you are driving an ST7789 TFT display, reading from an SD card, or sampling a high-speed ADC, SPI's push-pull architecture and separate data lines (MISO/MOSI) allow full-duplex communication at tens of megahertz. The trade-off is routing complexity: every target device requires its own Chip Select (CS) line. Connecting five SPI sensors to an ESP32 consumes five separate GPIO pins just for chip selection, which quickly exhausts your available I/O.
When to use UART (Universal Asynchronous Receiver-Transmitter)
Choose UART for simple point-to-point communication where clock synchronization is impossible or unnecessary. It is the standard for GPS modules, cellular modems (via AT commands), and debugging via USB-to-Serial adapters (like the CP2102 or CH340). UART is strictly a two-node protocol. If you need multiple devices on a UART bus over long distances, you must add RS-485 transceiver ICs (like the MAX485) to convert the single-ended logic levels to differential signals.
Minimal Working Exchange: I2C Byte Write
To ground this in reality, here is the exact physical and logical sequence for writing a single configuration byte to a BME280 sensor over I2C using an ESP32-WROOM-32.
- Wiring: ESP32 GPIO 21 to BME280 SDA; ESP32 GPIO 22 to BME280 SCL. 4.7kΩ pull-ups from SDA and SCL to 3.3V.
- Bus Sequence:
- START: ESP32 pulls SDA low while SCL remains high.
- ADDRESS + W: ESP32 clocks out 7 bits (e.g., 0x76 for BME280) + 1 write bit (0).
- ACK: BME280 pulls SDA low on the 9th clock pulse to acknowledge.
- REGISTER: ESP32 clocks out the target register address (e.g., 0xF4 for ctrl_meas).
- ACK: BME280 acknowledges.
- DATA: ESP32 clocks out the configuration byte (e.g., 0x27 for normal mode).
- ACK: BME280 acknowledges.
- STOP: ESP32 releases SDA to high while SCL is high.
The Classic Failures: Debugging and Sniffing the Bus
Embedded communication rarely fails at the software level; it fails at the physical layer. When your serial monitor prints garbage or your sensor returns 0xFF, use this decision tree to isolate the fault.
1. The Missing Pull-Up (I2C)
Symptom: The microcontroller hangs on Wire.endTransmission(), or the sensor reads return 0xFF or NaN.
The Physics: Without pull-up resistors, the open-drain lines float. When the MCU releases the line, parasitic capacitance keeps it low, or ambient EMI induces random noise. The logic analyzer will show a 'sawtooth' wave instead of a square wave.
The Fix: Solder 4.7kΩ resistors between SDA/SCL and VCC. If using a breakout board, check the back for unbridged solder pads labeled 'I2C PU'—many manufacturers omit the resistors to prevent parallel resistance issues when stacking multiple boards.
2. The Address Clash (I2C)
Symptom: I2C Scanner sketch reports no devices found, or reports an unexpected address.
The Physics: Many sensors share default addresses. Two MPU6050 IMUs on the same bus will both answer to 0x68, causing data collisions and NACKs.
The Fix: Check the datasheet for address pins (A0, A1, A2). On a PCF8574 I/O expander, bridging the A0 pad to ground shifts the address from 0x20 to 0x21. Always run an I2C scanner script before writing application code to verify the exact hex address the hardware is currently presenting.
3. Baud Rate Mismatch (UART)
Symptom: The serial monitor displays erratic, unreadable unicode characters (e.g., ÿÿÿ) instead of plain text.
The Physics: UART has no shared clock. If the transmitter sends at 115200 baud and the receiver listens at 9600 baud, the receiver samples the line at the wrong intervals, interpreting a single fast bit as multiple slow bits, resulting in framing errors.
The Fix: Verify both ends. If dealing with a proprietary module (like a cheap 433MHz RF transceiver), use a logic analyzer to measure the shortest pulse width of a known transmission. A 104µs pulse width indicates 9600 baud; an 8.68µs pulse width indicates 115200 baud.
How to Sniff and Debug the Bus
Do not rely solely on Serial.print() to debug hardware buses. You need to see the actual wire states. For professional work, a Saleae Logic Pro 8 is the industry standard, but for 95% of hobbyist and bench work, a $12 FX2LP-based 8-channel logic analyzer clone is perfectly adequate.
Download the open-source PulseView software (part of the sigrok project). Connect your logic analyzer ground to your circuit ground, and clip the probes to SDA, SCL, TX, or RX.
- Set the Sample Rate: For I2C at 400kHz, set the sample rate to at least 4 MHz (10x the bus speed) to accurately capture rise times and setup/hold violations.
- Add Decoders: In PulseView, add the 'I2C' or 'UART' protocol decoder. Map the SDA/SCL or TX/RX channels to the decoder inputs.
- Triggering: Set a trigger on the falling edge of the SCL line (for I2C) or the falling edge of the TX line (UART start bit). This ensures the capture starts exactly when a transaction begins, rather than filling the buffer with idle high states.
By inspecting the decoded hex values against your datasheet, you can instantly determine if the microcontroller is sending the wrong register address, if the sensor is NACKing due to a brownout, or if your SPI clock polarity (CPOL/CPHA) is inverted. Mastering the physical layer and the logic analyzer transforms embedded debugging from guesswork into a precise, measurable science.






