SPI (Serial Peripheral Interface) is a synchronous, full-duplex, 4-wire serial bus used to move data between a microcontroller and peripherals like flash memory, TFT displays, and ADCs. On the bench, standard SPI runs between 1 MHz and 20 MHz, though high-speed flash chips can push 80 MHz or more using multi-line (Quad/Octal) variants. Unlike I2C, SPI does not use software addressing; it relies on individual hardware Chip Select (CS) lines for each target. This makes it significantly faster and simpler to debug at the physical layer, but the wiring overhead scales linearly with every device you add.

Bus Mechanics and Protocol Selection

Before routing traces or grabbing jumper wires, you need to know if SPI is actually the right tool for the job. The table below maps the physical and logical constraints of the most common embedded buses. Use this to decide which protocol fits your specific distance, speed, and device count requirements.

Protocol Wires (Min) Max Speed (Typical) Addressing Max Distance Topology
SPI 4 (Shared) + 1 per CS 10 - 50 MHz Hardware CS lines ~30 cm (high speed) Master-Slave (Daisy chain possible)
I2C 2 (SDA, SCL) 100 kHz / 400 kHz / 1 MHz 7-bit or 10-bit Software ~1 meter (with pull-ups) Multi-Master Bus
UART 2 (TX, RX) per link 115.2 kbps - 3 Mbps None (Point-to-Point) ~15 meters (RS-485 phys layer) Point-to-Point / Multi-drop
CAN 2 (CAN_H, CAN_L) 1 Mbps (Classic) / 8 Mbps (FD) Message ID Arbitration Up to 40 meters (at 1 Mbps) Multi-Master Differential Bus
Decision Framework: Choose SPI when you need high throughput (e.g., streaming audio, updating a 320x240 TFT display, or logging to flash) and your devices are clustered on the same PCB or a short breadboard. Choose I2C when you have many low-speed sensors (temperature, IMUs) and want to save GPIO pins. Choose UART/RS-485 for long-distance point-to-point telemetry, and CAN for noisy environments like automotive or motor-control applications where differential signaling is mandatory.

Physical Layer: Wiring, Modes, and Pull-Up Realities

The physical layer of SPI consists of four shared wires, plus individual Chip Select (CS) lines. Understanding the exact behavior of these lines prevents the most common hardware bugs.

  • SCK (Serial Clock): Generated exclusively by the master. It dictates the data shift rate.
  • MOSI (Master Out, Slave In): Data sent from the microcontroller to the peripheral.
  • MISO (Master In, Slave Out): Data sent from the peripheral back to the microcontroller.
  • CS / SS (Chip Select / Slave Select): Active-LOW. The master pulls this pin LOW to wake up a specific peripheral and enable its MISO output driver.

The Pull-Up Myth

A classic mistake makers port over from I2C is adding 4.7k pull-up resistors to SPI data and clock lines. Do not do this. SPI uses push-pull output drivers, not open-drain. Pull-ups on MOSI, MISO, or SCK will fight the microcontroller's GPIO drivers, causing excess current draw and rounding off the square wave edges at high baud rates.

The only exception is the CS line. During microcontroller boot (especially on ESP32 and Raspberry Pi), GPIO pins float before the firmware initializes them. If a peripheral's CS line floats, it may interpret noise as a valid select signal, causing bus contention. A 10k pull-up resistor on each CS line to VCC ensures peripherals stay deselected during boot.

SPI Modes: CPOL and CPHA

SPI defines four modes based on Clock Polarity (CPOL) and Clock Phase (CPHA). According to Analog Devices' SPI primer, mismatching these modes is the #1 reason a bus 'looks' active on an oscilloscope but returns garbage data.

  • Mode 0 (CPOL=0, CPHA=0): Clock idles LOW. Data is sampled on the rising edge. (Most common: W25Q flash, MCP3008 ADC).
  • Mode 3 (CPOL=1, CPHA=1): Clock idles HIGH. Data is sampled on the rising edge. (Common in some displays and RF modules).

Minimal Working Exchange: ESP32 to W25Q128 Flash

Let's look at a concrete, working exchange. We will read the JEDEC Manufacturer and Device ID from a W25Q128 SPI flash chip using an ESP32. The JEDEC ID command (0x9F) requires the master to send one command byte, then clock out three dummy bytes while the slave shifts the ID data back on MISO.

Wiring Pinout

ESP32 DevKit v1 Pin W25Q128 Flash Pin Function Notes
GPIO 18 Pin 6 (CLK) SCK Default VSPI Clock
GPIO 23 Pin 5 (DI) MOSI Default VSPI MOSI
GPIO 19 Pin 2 (DO) MISO Default VSPI MISO
GPIO 5 Pin 1 (CS) Chip Select Add 10k pull-up to 3.3V
3.3V Pin 8 (VCC), Pin 3, 7 Power & Hold/Reset Tie WP and HOLD to 3.3V

ESP32 Arduino Core Code

This code uses the hardware VSPI bus. As noted in the Espressif SPI Master API documentation, utilizing the hardware SPI peripheral rather than software bit-banging is mandatory for speeds above 1 MHz to avoid watchdog resets and timing jitter.

#include <SPI.h>

// Define the Chip Select pin
const int CS_PIN = 5;

// Use the hardware VSPI bus on ESP32
SPIClass vspi(VSPI);

void setup() {
  Serial.begin(115200);
  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH); // Deselect flash

  // Initialize VSPI at 10 MHz, Mode 0
  vspi.begin();
}

void loop() {
  uint8_t manufacturer_id, memory_type, capacity;

  // Begin SPI transaction (locks bus, sets speed/mode)
  vspi.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));
  
  digitalWrite(CS_PIN, LOW); // Select the flash chip
  
  vspi.transfer(0x9F); // Send JEDEC ID command
  
  // Clock out 3 bytes while sending dummy data (0x00)
  manufacturer_id = vspi.transfer(0x00);
  memory_type = vspi.transfer(0x00);
  capacity = vspi.transfer(0x00);
  
  digitalWrite(CS_PIN, HIGH); // Deselect
  vspi.endTransaction();

  Serial.printf("JEDEC ID: 0x%02X 0x%02X 0x%02X\n", 
                manufacturer_id, memory_type, capacity);
  // Expected output for W25Q128: 0xEF 0x40 0x18
  
  delay(2000);
}

Sniffing the Bus and Classic Failure Modes

When your serial monitor prints 0xFF 0xFF 0xFF or 0x00 0x00 0x00, it is time to stop guessing and sniff the physical layer. You do not need a $500 oscilloscope; a $15 FX2LA-based 24MHz logic analyzer running PulseView / Sigrok is the standard bench tool for SPI debugging.

How to Sniff

  1. Connect the logic analyzer ground to your circuit ground.
  2. Clip the four channels to SCK, MOSI, MISO, and CS.
  3. Set the sample rate to at least 4x your SPI clock speed (e.g., 10 MHz SPI requires a 40+ MHz sample rate to accurately resolve edge timing and CPOL/CPHA states).
  4. Set the trigger to the falling edge of the CS line. This ensures you capture the exact start of the transaction.

The Classic SPI Failures

If the logic analyzer shows clock pulses but the data is garbage, walk through this ranked checklist of classic failures:

  • Baud Rate Too High for the Physical Medium: If you are using long breadboard jumper wires, parasitic capacitance will round off your 20 MHz square waves into sine waves, causing the peripheral to misread bits. Fix: Drop the baud rate to 1 MHz to test, then step up.
  • CPOL/CPHA Mismatch: If the first bit read is always wrong, or shifted by one position, your SPI Mode is wrong. Fix: Check the peripheral datasheet. If it requires Mode 3, change SPI_MODE0 to SPI_MODE3 in your SPISettings.
  • MISO/MOSI Swap: A highly common error. Master-Out must go to Slave-In. If your peripheral board silkscreen labels the pins from the peripheral's perspective (e.g., labeling its MISO pin as 'MISO'), you must connect Master-MOSI to Peripheral-MOSI. Fix: Verify data flow direction, not just the pin labels.
  • CS Contention (The SPI equivalent of an I2C address clash): If you have multiple devices on the bus and forget to initialize their CS pins as HIGH in setup(), multiple MISO drivers will turn on simultaneously, shorting a 3.3V logic HIGH to a 0V logic LOW. Fix: Ensure all unused CS pins are pulled HIGH via GPIO or 10k resistors.
  • Missing 'Dummy' Bytes: SPI is a shift register exchange. To read 3 bytes from a sensor, you must transmit 3 bytes (even if they are just 0x00) to generate the clock pulses the sensor needs to shift its data out. Fix: Ensure your transfer() loop matches the expected byte count.
Bench Trick: If your logic analyzer shows the correct MOSI command going out, but MISO stays stubbornly HIGH (0xFF), your peripheral is either unpowered, held in reset, or the CS line is not pulling low enough. Measure the CS pin with a multimeter during the transaction; it must drop below 0.8V for standard 3.3V logic to register as a valid LOW.