The Serial Peripheral Interface (SPI) is the undisputed workhorse for high-speed, short-distance communication between a microcontroller and local peripherals like SD cards, TFT displays, and flash memory. Unlike protocols that prioritize bus topology or long-distance transmission, SPI prioritizes raw throughput and simplicity. However, its lack of built-in hardware handshaking means physical layer mistakes will silently corrupt your data.

This primer bypasses the abstract theory and goes straight to the bench: physical wiring rules, protocol selection, a working ESP32 exchange, and how to sniff the bus when things go wrong.

The Physical Layer: Wiring and Bus Mechanics

SPI is a synchronous, full-duplex, push-pull bus. It relies on four primary logic lines. While modern datasheets sometimes use COPI/CIPO (Controller Out/In Peripheral In/Out), the legacy MOSI/MISO terminology remains dominant in 2026 schematic captures and silkscreens.

Parameter SPI Specification Practical Bench Notes
Wires 4 shared (SCK, MOSI, MISO) + 1 per device (CS) Wire count scales linearly with device count due to individual Chip Select lines.
Speed 1 MHz to 50+ MHz (Peripheral dependent) SD cards typically run at 4 MHz (init) up to 25 MHz (High Speed). TFTs often push 40-80 MHz.
Addressing None (Hardware routed via CS pins) No software addresses (like I2C's 0x68). Routing is handled by the master asserting specific GPIOs.
Distance < 30 cm (1 foot) at high speeds Parasitic capacitance destroys signal integrity above 10 MHz on long, unshielded jumper wires.
The Pull-Up Resistor Nuance: Unlike I2C, SPI is a push-pull architecture. You do not need pull-up resistors on MOSI, MISO, or SCK. However, you must place a 10kΩ pull-up resistor on every CS (Chip Select) line tied to VCC. During microcontroller boot, GPIOs float; without a pull-up, a peripheral's CS pin may dip low, causing the device to wake up and drive the MISO line while the MCU is still initializing, leading to bus contention or brownouts.

Protocol Selection: When to Use SPI vs. I2C vs. UART

Choosing the right protocol depends entirely on your constraints regarding distance, speed, and device count. Here is the decision framework for local embedded buses.

Criteria SPI I2C UART RS-485
Max Speed Very High (50+ MHz) Medium (400 kHz - 3.4 MHz) Low/Med (115.2 kbps - 2 Mbps) Medium (up to 10 Mbps)
Max Distance Short (< 30 cm) Short (< 1 meter) Medium (< 15 meters) Long (up to 1200 meters)
Device Count Low (Limited by CS GPIOs) High (up to 127 via addresses) Point-to-Point (1 to 1) High (up to 32/256 nodes)
Best Use Case SD Cards, Displays, ADCs Sensors, EEPROMs, OLEDs GPS, Cellular, PC Debug Industrial, HVAC, Long runs

The Verdict: Choose SPI when you need to move large blocks of data (like filesystem reads or framebuffer updates) quickly across a single PCB or short ribbon cable. Choose I2C when you have dozens of low-bandwidth sensors and want to save GPIO pins. Choose UART/RS-485 for off-board, long-distance, or inter-system communication.

Minimal Working Exchange: ESP32 to SD Card Module

Below is a complete, copy-pasteable implementation for initializing an SD card over SPI using an ESP32. This uses the default VSPI hardware pins.

Physical Wiring Map

ESP32 Pin (VSPI) GPIO Number SD Card Module Pin Notes
VSPI SCKGPIO 18SCK (CLK)Clock signal
VSPI MISOGPIO 19MISO (DO)Data from SD to ESP32
VSPI MOSIGPIO 23MOSI (DI)Data from ESP32 to SD
VSPI CSGPIO 5CS (SS)Add 10kΩ pull-up to 3.3V
3V3-VCCEnsure module has a 3.3V LDO
GND-GNDCommon ground required

Arduino Framework Code

#include <SPI.h>
#include <SD.h>

// ESP32 VSPI Default Pins
#define SCK_PIN  18
#define MISO_PIN 19
#define MOSI_PIN 23
#define CS_PIN   5

void setup() {
  Serial.begin(115200);
  while(!Serial) { delay(10); }
  
  Serial.println("Initializing SD card...");

  // Explicitly define SPI pins for ESP32 to avoid routing errors
  SPI.begin(SCK_PIN, MISO_PIN, MOSI_PIN, CS_PIN);

  if (!SD.begin(CS_PIN)) {
    Serial.println("[ERROR] Card Mount Failed. Check wiring and CS pull-up.");
    return;
  }
  
  uint8_t cardType = SD.cardType();
  if(cardType == CARD_NONE){
    Serial.println("[ERROR] No SD card attached.");
    return;
  }

  Serial.print("SD Card Type: ");
  if(cardType == CARD_MMC) Serial.println("MMC");
  else if(cardType == CARD_SD) Serial.println("SDSC");
  else if(cardType == CARD_SDHC) Serial.println("SDHC");
  
  Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
  Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
}

void loop() {
  // Main application logic here
}

Sniffing the Bus and Debugging Classic Failures

When your SPI peripheral returns garbage data or fails to initialize, a multimeter is useless. You need a logic analyzer (like a Saleae Logic or a DSLogic Plus) sampling at least 4 to 10 times faster than your SCK frequency. If your SPI clock is 4 MHz, set your analyzer to sample at 24 MHz or higher to accurately capture edge transitions.

Here are the classic SPI failures and how to identify them:

  • The 'Address Clash' (MISO Contention): SPI doesn't use software addresses, so an 'address clash' actually means a Chip Select routing error. If two devices share the MISO line and both have their CS lines pulled low simultaneously, both will try to drive MISO. On a logic analyzer, you will see the MISO line stuck at a mid-level voltage (around 1.5V on a 3.3V system) or erratic square waves. Fix: Ensure every peripheral has a dedicated CS pin, and verify no CS pins are accidentally tied together on the PCB.
  • Missing CS Pull-Up (Boot Glitch): If your peripheral initializes randomly after a power cycle, capture the CS line during MCU boot. You will likely see the CS line fluttering before the MCU's GPIO driver takes control. Fix: Solder a 10kΩ resistor between the CS line and VCC.
  • Baud Mismatch and Capacitance: If your code works on a 5cm breadboard but fails on a 30cm ribbon cable, you are hitting parasitic capacitance. The logic analyzer will show the SCK and MOSI lines looking like 'shark fins' (rounded edges) rather than crisp square waves, causing the peripheral to misread bits. Fix: Lower the SPI clock speed (e.g., from 20 MHz to 4 MHz) or use a specialized buffer IC like the 74LVC125A for long runs.
  • CPOL/CPHA Mismatch (SPI Modes): SPI defines 4 modes based on Clock Polarity (CPOL) and Clock Phase (CPHA). Mode 0 (CPOL=0, CPHA=0) and Mode 3 (CPOL=1, CPHA=1) are the most common. If your logic analyzer decodes the correct hex bytes but your microcontroller reads them backwards or shifted by one bit, you are sampling on the wrong clock edge. Fix: Check the peripheral datasheet and configure your MCU's SPI mode accordingly (e.g., SPI_MODE3 for most SD cards).

Serial Peripheral Interface SPI Frequently Asked Questions

Can I connect multiple devices to the same serial peripheral interface SPI bus?

Yes, but with strict wiring rules. You can share the SCK, MOSI, and MISO lines among dozens of devices, provided every single device has its own dedicated Chip Select (CS) line routed back to the master. The master must only pull one CS line LOW at a time. If you run out of GPIO pins for CS lines, you can use a 3-to-8 line decoder (like the 74HC138) to expand your CS routing using only 3 master GPIOs.

What is the maximum cable length for a serial peripheral interface SPI connection?

There is no hard standard, but practically, SPI is limited to about 30 cm (12 inches) at high speeds (10+ MHz) due to parasitic capacitance and crosstalk. If you must run SPI over a longer distance (up to 1-2 meters), you must drop the clock speed to 1 MHz or lower, use twisted-pair wiring (pairing SCK with GND, and MOSI/MISO with GND), and terminate the lines. For anything beyond 2 meters, abandon SPI and use RS-485 or CAN bus.

Why is my serial peripheral interface SPI device returning all 0xFF or 0x00 bytes?

Reading all 0xFF usually means the MISO line is floating high (the peripheral is not connected, is unpowered, or its CS line is not being asserted). Reading all 0x00 typically means the MISO line is shorted to ground, or the peripheral is held in a reset state. Hook up a logic analyzer to the CS and MISO pins: if CS goes LOW but MISO never transitions, the peripheral is either dead, unpowered, or wired to the wrong MISO pin.