SPI (Serial Peripheral Interface) is the go-to synchronous protocol for high-speed, short-distance chip-to-chip communication. Unlike I2C, it lacks a formal software addressing scheme, relying instead on individual Chip Select (CS) lines. If you are integrating high-throughput sensors, TFT displays, or external flash memory, SPI is usually your best option. However, its simplicity at the software level masks physical layer traps that routinely brick communication on the bench. This guide covers the exact physical wiring, pull-up nuances, and debugging workflows you need to get your bus running reliably.

The Physical Layer: Wiring, Pull-Ups, and Bus Mechanics

Before writing a single line of code, you must understand the physical constraints of the SPI bus. It is a four-wire, full-duplex, synchronous interface. Modern engineering bodies (including OSHA and the IEEE) are transitioning from the legacy MOSI/MISO naming to COPI/CIPO to use more inclusive terminology, though legacy datasheets still dominate.

Table 1: SPI Bus Mechanics & Specifications
Parameter SPI Standard Notes & Constraints
Wires Required 4 shared + 1 per device (CS) SCK, COPI (MOSI), CIPO (MISO) shared. CS is individual.
Max Speed 10 MHz to 80+ MHz Highly dependent on trace length and capacitive load. 80MHz requires PCB-level routing, not breadboards.
Addressing Hardware (Chip Select) No software addresses. Each target needs a dedicated MCU GPIO for CS.
Max Distance < 1 meter (typically < 30cm) Signal integrity degrades rapidly due to high clock edge rates. Use RS-422 transceivers for longer runs.
Duplex Mode Full-Duplex Data shifts in and out simultaneously on COPI and CIPO lines.

The Pull-Up Trap

The most common mistake hobbyists make is treating SPI like I2C and placing 4.7kΩ pull-up resistors on the clock and data lines. Do not do this. SPI is a push-pull architecture, not open-drain. Pull-ups on SCK or COPI will fight the MCU's GPIO drivers, causing excessive current draw and rounded signal edges.

However, you must use pull-ups in two specific scenarios:

  • Chip Select (CS) Lines: Place a 10kΩ pull-up to VCC on every CS line. During MCU boot or reset, GPIOs float. Without a pull-up, a floating CS pin can accidentally activate a peripheral, causing bus contention or corrupting external flash memory.
  • CIPO (MISO) with Unpowered Devices: If you have multiple devices on the same CIPO line and one device is powered down, its internal ESD diodes can leak current onto the bus, pulling the line high. A 10kΩ pull-down on CIPO (or a dedicated tri-state buffer like the 74LVC125A) prevents this ghosting.
Bench Tip: When wiring SPI on a breadboard, keep SCK, COPI, and CIPO wires as short and parallel as possible. Crossed wires at 20 MHz will induce crosstalk, causing the logic analyzer to decode phantom clock pulses.

Protocol Selection: When SPI Beats I2C and UART

Choosing the right protocol comes down to balancing distance, speed, and device count. Here is how SPI stacks up against the other embedded heavyweights.

Table 2: Protocol Selection Matrix
Criteria SPI I2C UART
Best Use Case High-speed, single-board data streaming (displays, ADCs) Low-speed sensor polling, multi-device configuration Point-to-point debugging, GPS modules, long-distance (RS-485)
Speed Very High (10-80 MHz) Low/Med (100 kHz - 3.4 MHz) Medium (9600 bps - 3 Mbps)
Device Count Low (Limited by MCU GPIO pins for CS) High (Up to 127 via software addressing) 1-to-1 (Requires multiplexers for more)
Wiring Complexity High (4 shared + N chip selects) Low (2 shared wires for all devices) Lowest (2 wires: TX/RX)

The Verdict: Choose SPI when you need raw bandwidth (e.g., driving an ILI9341 TFT display or reading a 24-bit ADC). Choose I2C when you have 10 low-speed environmental sensors and only have two GPIO pins available. Choose UART for off-board communication or when interacting with modems and GPS receivers.

Minimal Working Exchange: Hardware Map and Code

Below is a complete, minimal working exchange using an ESP32 and a BMP280 SPI pressure sensor. This demonstrates explicit pin mapping and proper SPI transaction handling.

Table 3: ESP32 to BMP280 SPI Wiring Map
ESP32 GPIO (VSPI Default) BMP280 SPI Pin Function
GPIO 18 SCK Serial Clock
GPIO 23 SDI (MOSI) Master Out, Slave In
GPIO 19 SDO (MISO) Master In, Slave Out
GPIO 5 CSB Chip Select (Active LOW)
3V3 VCC & SDO (if not using MISO) Power & I2C disable

Note: The BMP280 has an I2C/SPI mode select pin (CSB). Ensure it is pulled to VCC via a 10k resistor if you want it to default to I2C, but for SPI, the CS line handles activation. Always tie the unused SDO pin appropriately per the Bosch BMP280 datasheet.

#include <SPI.h>
#include <Adafruit_BMP280.h>

// Explicit pin definitions for ESP32 VSPI
#define BMP_SCK  18
#define BMP_MISO 19
#define BMP_MOSI 23
#define BMP_CS   5

Adafruit_BMP280 bmp(BMP_CS, BMP_MOSI, BMP_MISO, BMP_SCK);

void setup() {
  Serial.begin(115200);
  while(!Serial); // Wait for serial monitor
  
  // Initialize hardware SPI with explicit settings
  if (!bmp.begin()) {
    Serial.println("Could not find a valid BMP280 sensor, check wiring!");
    while (1) delay(10);
  }

  // Configure sensor sampling
  bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
                  Adafruit_BMP280::SAMPLING_X2,  // Temp oversampling
                  Adafruit_BMP280::SAMPLING_X16, // Pressure oversampling
                  Adafruit_BMP280::FILTER_X16,
                  Adafruit_BMP280::STANDBY_MS_500);
}

void loop() {
  Serial.print("Temperature = ");
  Serial.print(bmp.readTemperature());
  Serial.println(" *C");
  
  Serial.print("Pressure = ");
  Serial.print(bmp.readPressure() / 100.0F); // Convert Pa to hPa
  Serial.println(" hPa");
  
  delay(2000);
}

Debugging the Bus: Sniffing and Classic Failures

When your SPI device returns garbage data or hangs the MCU, guessing is a waste of time. You need to sniff the bus. Use a logic analyzer like the Saleae Logic Pro 8 or a modern oscilloscope with serial decoding (like the Siglent SDS1204X-E). Hook up all four lines, trigger on the falling edge of CS, and decode the hex bytes.

The Classic Failures

  1. Baud and Mode Mismatch (CPOL/CPHA): SPI defines four modes (0 through 3) based on Clock Polarity (CPOL) and Clock Phase (CPHA). Mode 0 (CPOL=0, CPHA=0) is the most common, meaning the clock idles LOW and data is sampled on the rising edge. If your MCU is set to Mode 0 but the sensor expects Mode 3, every byte will be shifted by one bit, resulting in complete data corruption. Always check the sensor datasheet's timing diagram.
  2. The "Address Clash" (CS Contention): Because SPI lacks software addressing, an "address clash" means you wired two devices to the same CS pin, or you forgot to drive the CS pin HIGH on an inactive device before talking to a second device. If two devices drive the CIPO (MISO) line simultaneously, you create a short circuit between their output drivers, which can permanently silicon-damage the sensors.
  3. Missing Pull-Up Glitching: If your peripheral initializes randomly after a power cycle, check your CS line on a scope during the MCU boot sequence. The ESP32 and Arduino bootloaders toggle GPIOs rapidly before your setup() function runs. Without a 10kΩ hardware pull-up on CS, the peripheral sees these toggles as valid clock/select commands and enters an undefined state.

SPI Guidelines FAQ

Can I use SPI over long distances like RS-485?

Standard CMOS SPI is strictly for short distances (under 30cm on a breadboard, up to 1 meter on a well-designed PCB). The high-frequency clock edges suffer from capacitive loading and reflections over long wires. If you must run SPI over several meters (e.g., to a remote weather station), you cannot use raw GPIO pins. You must use differential line drivers like the MAX490 (RS-422) to convert the single-ended SPI signals into differential pairs, which reject common-mode noise and preserve signal integrity over long cable runs.

What happens if I connect two SPI masters to the same bus?

Standard SPI does not support multi-master arbitration like I2C does. If two microcontrollers attempt to drive the SCK and COPI lines simultaneously, you will cause a bus collision, potentially damaging the GPIO drivers due to shoot-through current. If you absolutely need two masters to access the same SPI peripheral (e.g., an ESP32 and a Raspberry Pi sharing an external SPI flash chip), you must use an external hardware multiplexer or tri-state buffers (like the 74HC125) controlled by a dedicated "bus request" handshake line to ensure only one master drives the bus at a time.

Why is my SPI device returning all 0xFF or 0x00?

Reading all 0xFF usually means the CIPO (MISO) line is floating or being pulled high, and the peripheral is not actually driving the bus. This happens if the CS line is not being pulled LOW correctly, or if the peripheral is unpowered. Conversely, reading all 0x00 typically means the CIPO line is being shorted to ground, or you are sampling the data on the wrong clock edge (CPHA mismatch). Hook a logic analyzer to the CS and CIPO lines; if CS goes LOW but CIPO remains flat, the peripheral is either dead, unpowered, or wired to the wrong MISO pin.