Physical Layer & Bus Mechanics

Skip the abstract theory: Serial Peripheral Interface (SPI) is a synchronous, full-duplex, push-pull bus designed for high-speed, short-distance communication between a single controller (master) and one or more peripherals (slaves). Unlike I2C, which relies on open-drain lines and pull-up resistors, SPI uses dedicated push-pull outputs for data lines, allowing it to achieve significantly higher clock rates.

SPI Bus Mechanics Specification Sheet
Parameter Standard SPI Quad SPI (QSPI) Notes & Constraints
Wires Required 4 (SCK, MOSI, MISO, CS) 6 (SCK, CS, IO0-IO3) CS is individual per peripheral. QSPI multiplexes data lines.
Max Speed 10 MHz – 50 MHz 80 MHz – 133 MHz Speed drops as trace/jumper wire length increases due to capacitance.
Addressing None (Hardware CS) None (Hardware CS) Controller routes data via individual Chip Select (CS) lines.
Max Distance < 30 cm (1 foot) < 10 cm (on PCB) Requires RS-422/RS-485 differential line drivers for longer runs.
Topology Bus (Shared MISO/MOSI/SCK) Point-to-Point MISO lines from multiple peripherals are tri-stated when CS is high.

Wiring Rules & The Pull-Up Trap

The most common bench mistake when wiring SPI is treating it like I2C. Because SPI data lines (MOSI, MISO, SCK) are push-pull, they do not require pull-up resistors. Adding 4.7kΩ pull-ups to SCK or MOSI will actually degrade your signal rise times and limit your maximum baud rate.

However, the Chip Select (CS) line is the exception. Microcontroller GPIO pins (especially on the ESP32 during boot) often float before the firmware initializes them. If a peripheral's CS pin floats low, the peripheral will wake up and drive the MISO line. If another device is also trying to drive MISO, you create a direct short circuit that can fry the output buffers on both chips.

Bench Rule: Always place a 10kΩ external pull-up resistor between the CS line and VCC (3.3V or 5V) for every SPI peripheral. While some microcontrollers offer internal pull-ups, they are often too weak (30kΩ–50kΩ) to overcome noise on long jumper wires.

Logic Level Translation: If you are connecting a 5V peripheral (like a classic Arduino shield or an SD card module with an onboard LDO but no level shifters) to a 3.3V ESP32, you must use a bidirectional logic level shifter (like the BSS138 MOSFET-based modules or a CD4050 buffer). Feeding 5V into an ESP32 GPIO will permanently damage the silicon.

Minimal Working Exchange

Before writing code, hardwire your physical connections. Below is a standard mapping for an ESP32 DevKit V1 communicating with a generic SPI peripheral (e.g., an MCP3008 ADC or W25Q32 Flash chip).

ESP32 to SPI Peripheral Wiring Map
ESP32 GPIOSPI FunctionPeripheral Pin Label
GPIO 18SCK (Clock)SCK / CLK
GPIO 23MOSI (Master Out)MOSI / SDI / DIN
GPIO 19MISO (Master In)MISO / SDO / DOUT
GPIO 5CS (Chip Select)CS / SS / CE

The following C++ code demonstrates a robust, minimal SPI read transaction using the standard Arduino SPI library. It explicitly manages bus settings to prevent conflicts with other libraries (like an SPI display or WiFi stack on the ESP32).

#include <SPI.h>

const int CS_PIN = 5;

void setup() {
  Serial.begin(115200);
  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH); // Deselect peripheral immediately
  
  // Initialize the SPI bus with default VSPI pins on ESP32
  SPI.begin(); 
}

void loop() {
  // 1. Configure bus settings: 1MHz, MSB first, SPI_MODE0 (CPOL=0, CPHA=0)
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
  
  // 2. Assert Chip Select (Active LOW)
  digitalWrite(CS_PIN, LOW);
  
  // 3. Perform the exchange. SPI.transfer() sends and receives simultaneously.
  // Send 0x80 (dummy read command for many sensors), receive status byte.
  uint8_t status_byte = SPI.transfer(0x80);
  uint8_t data_byte = SPI.transfer(0x00); // Clock out the actual data
  
  // 4. Deassert Chip Select
  digitalWrite(CS_PIN, HIGH);
  
  // 5. Release the bus for other tasks
  SPI.endTransaction();
  
  Serial.printf("Status: 0x%02X, Data: 0x%02X\n", status_byte, data_byte);
  delay(1000);
}

Debugging the Bus: Sniffing & Classic Failures

When your SPI bus returns garbage data or hangs, do not guess. Hook up a logic analyzer. A standard $12 24MHz 8-channel clone (based on the Cypress CY7C68013A chip) running PulseView/sigrok is all you need. Connect SCK, MOSI, MISO, and CS to the analyzer channels, and set your trigger to the falling edge of the CS line.

The Classic SPI Failures

  • CPOL/CPHA Mismatch (Baud/Clock Error): This is the #1 killer of SPI projects. SPI has four modes (Mode 0 to 3) defining Clock Polarity (CPOL) and Clock Phase (CPHA). If your master clocks data out on the rising edge (Mode 0), but the peripheral datasheet specifies it reads on the falling edge (Mode 3), you will read shifted, corrupted bytes. Always verify the peripheral datasheet's timing diagram and set SPI_MODE0 through SPI_MODE3 accordingly.
  • Missing CS Pull-Up (Boot Contention): As mentioned earlier, if the CS line floats during MCU reset, the peripheral drives MISO. When the MCU boots and tries to read another device on the same MISO line, a bus contention occurs, pulling the voltage to an undefined ~1.5V and causing read failures.
  • The "Address Clash" Misconception: Beginners often ask how to resolve an "address clash" on SPI. SPI does not use software addressing. If two devices are responding at the same time, it is a hardware wiring fault: either you wired two CS lines to the same GPIO pin, or a CS line is floating/shorted to ground. Every peripheral must have its own dedicated CS wire routed back to the master.

Protocol Selection: SPI vs I2C vs UART

Choosing the right protocol depends entirely on your physical constraints: distance, speed requirements, and pin availability. Refer to the SparkFun SPI Tutorial for deeper signal integrity analysis, but use this decision matrix for rapid selection.

Embedded Protocol Decision Matrix
CriterionSPII2CUART
Best For High-speed, short-distance (Displays, Flash, ADCs) Many low-speed sensors on limited pins Long-distance, point-to-point, PC comms
Max Speed 50+ MHz 3.4 MHz (Fast Mode Plus) ~1 Mbps (Hardware dependent)
Distance Limit < 30 cm < 1 meter (with proper pull-ups) 15+ meters (at lower baud rates)
Device Count Low (Scales linearly with CS pins) High (Up to 127 on 2 wires) 1-to-1 (Requires multiplexers for more)
Wiring Complexity High (4 wires + 1 per extra device) Low (2 shared wires) Low (2 wires: TX/RX)

SPI Wiki FAQ

Is SPI faster than I2C according to standard wiki specs?

Yes, significantly. Standard I2C tops out at 100 kHz (Standard Mode) or 400 kHz (Fast Mode), with specialized Fast Mode Plus reaching 3.4 MHz. SPI routinely operates between 10 MHz and 50 MHz on standard microcontrollers, and can exceed 100 MHz on dedicated hardware interfaces. This makes SPI mandatory for high-bandwidth peripherals like TFT displays, audio DACs, and external flash memory.

Can I connect multiple SPI devices to the same MISO/MOSI pins?

Yes, this is the standard SPI bus topology. You share the SCK, MOSI, and MISO lines across all peripherals. However, every single peripheral must have its own dedicated Chip Select (CS) wire connected to a unique GPIO on the master. When a peripheral's CS line is HIGH, its internal MISO output goes into a high-impedance (tri-state) mode, effectively disconnecting it from the bus so other devices can transmit.

Why does my SPI bus fail when I add a third device?

Adding a third device increases the physical length of the shared jumper wires, which adds parasitic capacitance to the bus. This capacitance rounds off the sharp square edges of your SCK and MOSI signals. If the signal doesn't cross the logic threshold voltage before the next clock edge, the peripheral reads the wrong bit. Fix this by shortening your wires, lowering the SPI clock speed in SPISettings, or adding a 33Ω series termination resistor near the master's MOSI and SCK pins to dampen reflections.

Does SPI require pull-up resistors on SCK and MOSI?

No. Unlike I2C, which uses open-drain outputs that require external pull-ups to achieve a HIGH state, SPI uses push-pull outputs. The microcontroller actively drives the lines both HIGH and LOW. Adding pull-up resistors to SCK or MOSI is unnecessary and will actually slow down the signal's falling edge, limiting your maximum achievable baud rate. Only the CS line requires a pull-up resistor to prevent floating during microcontroller reset.