SPI Bus Mechanics and Physical Layer Specifications

The Serial Peripheral Interface (SPI) is a synchronous, full-duplex communication bus that dominates high-speed peripheral interfacing in embedded systems. Unlike asynchronous protocols, SPI relies on a shared clock line to shift bits in and out simultaneously, allowing it to push data rates far beyond what I2C or standard UART can achieve. If you are searching for a definitive SPI bus wiki, the physical layer is where your design will either succeed or fail.

Before wiring a single jumper, you must understand how SPI compares to other common buses regarding speed, distance, and device scaling. The table below maps the physical realities of these protocols.

Table 1: Embedded Bus Mechanics and Physical Limits
Protocol Wires (Min) Typical Max Speed Addressing / Node Selection Max Distance (Unbuffered)
SPI 4 (Shared) + 1 per device 10 MHz - 80 MHz Hardware Chip Select (CS) per node ~30 cm (1 ft) without series termination
I2C 2 (SDA, SCL) 100 kHz / 400 kHz / 3.4 MHz 7-bit or 10-bit software address ~30 cm (1 ft) limited by bus capacitance (400pF)
UART 2 (TX, RX) 115.2 kbps to 3 Mbps None (Point-to-Point) ~15 m (50 ft) at 9600 bps
RS-485 2 (Differential pair) 10 Mbps Software (via UART payload) 1200 m (4000 ft) with proper termination
Protocol Selection Framework: Choose SPI when you need raw throughput (e.g., TFT displays, external flash, high-speed ADCs) and have short trace lengths. Choose I2C for low-speed sensor networks where minimizing pin count is critical. Choose RS-485 when your devices are separated by meters or located in electrically noisy industrial environments.

Wiring Requirements, Pull-Ups, and Signal Integrity

SPI utilizes four shared lines: SCK (Clock), MOSI/COPI (Master Out Slave In), MISO/CIPO (Master In Slave Out), and CS/SS (Chip Select). While the logic is straightforward, the physical wiring is where most hobbyist builds encounter signal integrity issues.

The Pull-Up Misconception

A frequent mistake documented across forums is applying I2C wiring logic to SPI. SPI does not require pull-up resistors on SCK, MOSI, or MISO lines. These lines are push-pull driven by the master or the selected slave. Adding pull-ups here will only increase rise times and limit your maximum clock speed.

However, you must place a 10kΩ pull-up resistor on every Chip Select (CS) line to VCC. When an ESP32 or Arduino boots, its GPIO pins float before the bootloader initializes them. Without a pull-up, a floating CS pin can accidentally enable a peripheral (like an SD card or flash chip), causing it to drive the MISO line and collide with other boot-strapping peripherals, resulting in a bus lockup or corrupted boot sequence.

Series Termination and Trace Length

At clock speeds above 10 MHz, the square wave edges of the SCK signal will reflect off the high-impedance input of the slave device, causing ringing that the slave might interpret as multiple clock pulses. If your jumper wires or PCB traces exceed 10 cm, solder a 33Ω to 47Ω series resistor on the SCK and MOSI lines as close to the master's GPIO pin as possible. This dampens the reflection. For a deep dive into SPI signal integrity and trace routing, refer to the Analog Devices SPI Interface Guide.

Minimal Working Exchange: ESP32 to W25Q128 Flash

Let’s look at a practical, data-dense exchange. We will read the JEDEC Manufacturer and Device ID from a W25Q128 SPI flash chip using an ESP32 DevKit v1. This confirms the physical layer is functioning before implementing a heavy filesystem library.

Table 2: ESP32 to W25Q128 Wiring Map
ESP32 DevKit v1 Pin W25Q128 Pin Function Notes
GPIO 18 (SCK) CLK (Pin 6) Clock Add 33Ω series resistor if wire > 10cm
GPIO 23 (MOSI) DI (Pin 5) Master Out Data from ESP32 to Flash
GPIO 19 (MISO) DO (Pin 2) Master In Data from Flash to ESP32
GPIO 5 (CS) CS (Pin 1) Chip Select Requires 10kΩ pull-up to 3.3V
3V3 VCC (Pin 8) & WP (Pin 3) & HOLD (Pin 7) Power & Control Tie WP and HOLD to VCC for standard mode
GND GND (Pin 4) Ground Keep ground return path short
Voltage Warning: The W25Q128 is strictly a 3.3V device. Never connect it to a 5V Arduino Uno without a bidirectional logic level converter (like the TXB0104), or you will destroy the silicon.

ESP32 SPI Code Implementation

The following code uses the hardware SPI bus (VSPI on ESP32) to send the 0x9F (Read JEDEC ID) command and parses the 3-byte response. For more on ESP32 SPI bus allocation, consult the Espressif SPI Master API Documentation.

#include <SPI.h>

// VSPI pins on ESP32 DevKit: SCK=18, MISO=19, MOSI=23, CS=5
#define CS_PIN 5
#define SPI_CLOCK_SPEED 10000000 // 10 MHz

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

  // Initialize hardware SPI
  SPI.begin(18, 19, 23, CS_PIN);
  delay(100); // Allow flash chip to power up
}

void loop() {
  uint8_t manufacturerID, memoryType, capacity;

  SPI.beginTransaction(SPISettings(SPI_CLOCK_SPEED, MSBFIRST, SPI_MODE0));
  digitalWrite(CS_PIN, LOW); // Select chip

  SPI.transfer(0x9F); // JEDEC ID Command
  manufacturerID = SPI.transfer(0x00);
  memoryType = SPI.transfer(0x00);
  capacity = SPI.transfer(0x00);

  digitalWrite(CS_PIN, HIGH); // Deselect chip
  SPI.endTransaction();

  Serial.printf("Manufacturer: 0x%02X, Type: 0x%02X, Capacity: 0x%02X\n",
                manufacturerID, memoryType, capacity);
  // Expected output for W25Q128: Manufacturer: 0xEF, Type: 0x40, Capacity: 0x18

  delay(2000);
}

Debugging the Bus: Sniffing, Classic Failures, and Fixes

When your SPI peripheral returns garbage data, 0xFF, or 0x00, guessing is a waste of time. You must look at the physical signals. A logic analyzer (like a Saleae Logic Pro 8 or a DSLogic Plus) running Sigrok/PulseView protocol decoders is mandatory for serious debugging.

How to Sniff the Bus

  1. Ground First: Connect the logic analyzer ground to the MCU ground. Never probe floating.
  2. Trigger Setup: Set your trigger to the falling edge of the CS line. This ensures you capture the exact moment the transaction begins.
  3. Sample Rate: Set the sample rate to at least 4x to 10x your SPI clock speed. If SCK is 10 MHz, sample at 50 MS/s minimum to accurately resolve edge timings.

The Classic Failures and How to Fix Them

1. The Baud Rate Mismatch (Reading 0xFF or 0x00)
If your MISO line is stuck high (0xFF) or low (0x00), the slave isn't responding. This is often a baud rate issue. The master is clocking data faster than the slave's internal logic can shift it. Fix: Drop the SPI clock from 20 MHz down to 1 MHz. If the data appears, your parasitic capacitance or wire length is filtering the high-frequency edges. Add series termination resistors or shorten the wires.

2. CPOL and CPHA (SPI Mode) Mismatch
SPI defines four 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 you use Mode 0 on a peripheral that expects Mode 3, the master will sample the MISO line on the wrong clock edge, resulting in bit-shifted, garbage data. Fix: Check the peripheral datasheet's timing diagram. If the data is valid on the falling edge of SCK, you likely need Mode 3. Change SPI_MODE0 to SPI_MODE3 in your SPISettings.

3. "Address Clash" and CS Contention
Unlike I2C, SPI doesn't use software addressing, so "address clashes" manifest as MISO bus contention. If you have two SPI devices sharing the same MISO line, and one device lacks tri-state logic (or its CS line is accidentally held low due to a missing pull-up), it will continuously drive MISO, preventing the second device from sending data. Fix: Verify with a multimeter that all unselected CS lines sit at VCC (3.3V or 5V). If a peripheral doesn't release MISO when CS is high, it is either defective or lacks tri-state capability; route its MISO through a 74HC125 tri-state buffer.