The Serial Peripheral Interface (SPI) is a synchronous, full-duplex, controller-peripheral protocol built for high-speed, short-distance chip-to-chip communication. If you need to push pixels to an ILI9341 TFT display, log data to an SD card, or read external flash memory, SPI is the undisputed king of the workbench. Unlike I2C, which chokes on high-bandwidth payloads, SPI separates data lines (in and out) and uses a dedicated clock, allowing it to routinely hit 24 MHz to 50 MHz in embedded designs.

The Physical Layer: Wiring the SPI Bus Protocol

You cannot debug SPI without understanding its physical topology. The bus relies on four shared or dedicated lines. Note that the open-source hardware community has recently pushed to rename MOSI/MISO to COPI/CIPO (Controller Out/In, Peripheral In/Out), though datasheets still heavily use the legacy terms.

SPI Bus Mechanics & Specifications
Parameter Specification / Detail
Wires SCK (Clock), MOSI/COPI (Data Out), MISO/CIPO (Data In), CS/SS (Chip Select)
Speed 1 MHz to 50+ MHz (Hobbyist sensors typically max at 8–10 MHz)
Addressing None. Hardware routing via individual Chip Select (CS) lines per device.
Distance < 30 cm for high speeds (>10 MHz); up to 1 meter at <1 MHz with proper termination.
Topology Shared Bus (SCK, MOSI, MISO) + Star routing (Individual CS to each peripheral).

Physical Wiring and Pull-Up Requirements

A common misconception is that SPI requires pull-up resistors on the data lines like I2C does. It does not. SCK, MOSI, and MISO are push-pull driven and should never have pull-ups attached; doing so will round off your signal edges and cause bit errors at high clock speeds.

The CS Pull-Up Rule: While data lines don't need pull-ups, every Chip Select (CS) line must have a 10kΩ pull-up resistor to VCC. During microcontroller boot (especially on the ESP32), GPIO pins float and toggle wildly as the bootloader runs. Without a pull-up, your SPI peripheral will see this noise as a valid CS assertion, latch onto garbage data, and lock up its internal state machine until you physically cut power.

Protocol Selection: When SPI Wins Over I2C and UART

Choosing the right bus comes down to a strict trade-off between distance, speed, and device count. Here is how the SPI bus protocol fits into the broader embedded ecosystem.

Communication Protocol Comparison Matrix
Criteria SPI I2C UART (Serial)
Best Fit For High speed, short distance, moderate device count (displays, SD cards, flash). Low speed, short distance, high device count (sensors, EEPROMs, OLEDs). Point-to-point telemetry, GPS modules, long-distance RS-485 networks.
Max Speed 50+ MHz 3.4 MHz (Fast Mode Plus) ~3 Mbps (practical limit for most MCUs)
Wiring Overhead 4 shared wires + 1 CS wire per device. 2 shared wires (SDA, SCL) for all devices. 2 wires (TX, RX) per point-to-point link.
Duplex Full-Duplex (Simultaneous send/receive). Half-Duplex. Full-Duplex.

Minimal Working Exchange: ESP32 to SPI Sensor

Let's look at a minimal, bare-metal SPI exchange. We will read the WHO_AM_I register (0x75) from an MPU-9250 IMU sensor using an ESP32-WROOM-32. This register should return 0x71, confirming the wiring and clock phase are correct.

Wiring Table

ESP32-WROOM-32 Pin MPU-9250 SPI Pin Function
GPIO 18SCKSPI Clock (VSPI)
GPIO 23SDI (MOSI)Controller Out
GPIO 19SDO (MISO)Peripheral Out
GPIO 5NCS (CS)Chip Select (Active LOW)
3V3VCCPower (with 100nF decoupling cap)
GNDGNDCommon Ground

Arduino C++ Exchange Code

#include <SPI.h>

const int CS_PIN = 5;
const uint8_t WHO_AM_I_REG = 0x75;
const uint8_t SPI_READ_BIT = 0x80; // Bit 7 high indicates a read operation

void setup() {
  Serial.begin(115200);
  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH); // Deselect peripheral immediately
  
  // Initialize VSPI bus at 1MHz, MSB first, SPI Mode 0
  SPI.begin(18, 19, 23, CS_PIN); 
  delay(100); // Allow sensor boot time
}

void loop() {
  uint8_t result = 0;
  
  // Begin transaction with specific speed and mode
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
  
  digitalWrite(CS_PIN, LOW); // Assert Chip Select
  
  // Send register address with read bit set
  SPI.transfer(WHO_AM_I_REG | SPI_READ_BIT);
  
  // Clock out the data (send dummy byte 0x00 to receive)
  result = SPI.transfer(0x00);
  
  digitalWrite(CS_PIN, HIGH); // Deassert Chip Select
  SPI.endTransaction();
  
  Serial.printf("WHO_AM_I Register: 0x%02X\n", result);
  delay(2000);
}

Debugging the Classic Failures

When your SPI bus returns garbage, do not guess. Hook up a logic analyzer. A basic 24MHz 8-channel clone (like a DSLogic Plus or Saleae Logic 8) is mandatory for embedded work. Connect SCK, MOSI, MISO, and CS, set the decoder to SPI, and look for these three classic failures.

1. Baud Mismatch and SPI Modes

SPI has no auto-baud negotiation. If your peripheral's datasheet specifies a 4 MHz maximum clock and your SPISettings requests 8 MHz, the peripheral's shift register will fail to keep up, and MISO will return 0xFF or 0x00. Furthermore, verify your SPI Mode (CPOL and CPHA). Mode 0 (Clock idle LOW, sample on leading edge) is standard, but thermocouple amplifiers like the MAX31855 require Mode 1 or 3. Check the timing diagram in the Analog Devices SPI Guide to confirm the clock polarity.

2. The 'Address Clash' (Chip Select Collisions)

Because SPI lacks software addressing, an 'address clash' actually means a Chip Select collision. This happens when two devices share a single CS pin, or when you forget to configure unused CS pins on your microcontroller as OUTPUT and set them HIGH. If a CS pin is left as a floating input, ambient noise can accidentally activate a peripheral, causing it to drive the MISO line and corrupt data from the device you are actually trying to talk to.

3. MISO Bus Contention (Missing Tri-State)

When a peripheral's CS pin is HIGH, it is supposed to disconnect its MISO pin from the bus (high-impedance tri-state). Cheap or poorly designed breakout boards sometimes fail to implement this tri-state buffer. If you have multiple devices on one bus and one lacks tri-state logic, it will physically fight the active device for control of the MISO line. The fix is to route a separate MISO wire to the offending device or use a 74LVC125 tri-state buffer IC.

SPI Bus Protocol FAQ

What is the maximum cable length for the SPI bus protocol?

For standard 3.3V logic at speeds above 10 MHz, keep traces and wires under 30 cm (12 inches). The fast edge rates of SPI clocks cause severe signal reflection and ringing on long, unterminated wires. If you must run SPI over a meter, drop the clock speed to 1 MHz, use twisted-pair wiring with a shared ground, and consider adding 33Ω series termination resistors on the SCK and MOSI lines near the controller to dampen reflections.

How do I connect multiple devices to one SPI bus on an ESP32?

Wire the SCK, MOSI, and MISO lines in parallel to all devices. Then, route a dedicated CS wire from a unique GPIO pin to each device. The ESP32 has two usable hardware SPI buses (HSPI and VSPI). If you exceed the capacitive load limit of a single bus (usually around 3 or 4 devices at high speeds), move the overflow devices to the second SPI bus. The Espressif SPI Master Documentation details how to map these buses to specific GPIO matrices.

What are SPI Mode 0, 1, 2, and 3 (CPOL and CPHA)?

SPI modes define the clock polarity (CPOL) and clock phase (CPHA).
Mode 0: Clock idles LOW, data sampled on the rising (leading) edge. (Most common).
Mode 1: Clock idles LOW, data sampled on the falling (trailing) edge.
Mode 2: Clock idles HIGH, data sampled on the falling (leading) edge.
Mode 3: Clock idles HIGH, data sampled on the rising (trailing) edge. Always check your peripheral's datasheet; using Mode 0 on a Mode 3 device will result in shifted, corrupted bytes.

Why is my SPI MISO line returning 0xFF or 0x00?

A solid 0xFF usually means the MISO line is floating (pulled high by a multimeter or internal weak pull-up) and the peripheral is not responding. This is caused by a broken CS connection, the peripheral being in a sleep state, or a baud rate that is too fast for the chip. A solid 0x00 often means the MISO line is shorted to ground, or the peripheral is actively driving the line low because it is stuck in a fault state. Use a logic analyzer to verify that the CS line is actually pulling low during the transaction.