The Serial Peripheral Interface (SPI) is a synchronous, full-duplex, four-wire serial bus used for short-distance, high-speed communication between a microcontroller and peripherals. Unlike I2C, which relies on open-drain pull-ups and software addressing, an SPI connection uses dedicated push-pull data lines and individual hardware chip-select wires for every target. If you need to move megabytes of data to a TFT display, an external flash chip, or a high-resolution ADC, SPI is the physical layer you want.
The SPI Connection at a Glance: Bus Mechanics
Before routing traces or plugging in Dupont wires, you need to understand the hard limits of the bus. SPI is fast, but it is not designed for long cable runs.
| Parameter | SPI Specification & Practical Limits |
|---|---|
| Wires Required | 4 shared (SCK, MOSI, MISO) + 1 dedicated CS per device |
| Max Speed | MCU dependent (ESP32 hardware SPI supports up to 80MHz); practical PCB limit is 10–20MHz without impedance matching |
| Addressing | None (Software). Routing is handled by toggling individual Chip Select (CS) GPIO pins low |
| Max Distance | < 30 cm (1 ft) at >10 MHz; up to 1 meter possible at <1 MHz with shielded twisted pairs |
| Duplex Mode | Full-duplex (simultaneous transmit and receive on MOSI/MISO) |
Physical Wiring: Pinouts, Pull-Ups, and Level Shifting
The physical layer of an SPI connection is where most hobbyist builds fail. Because SPI uses push-pull drivers rather than open-drain, you do not use pull-up resistors on SCK, MOSI, or MISO. However, the Chip Select (CS) line requires specific handling.
Level Shifting for 5V Peripherals
If you are connecting a 3.3V ESP32 to a 5V SPI device, do not use a BSS138-based bi-directional level shifter. Those rely on RC time constants that will round off your square waves and destroy signal integrity above 1 MHz. For SPI, use a dedicated CMOS level translator like the SN74LVC8T245 or the single-channel SN74LV1T34. These preserve the sharp nanosecond edges required for high-speed clocking.
The Classic Failures: Why Your SPI Connection Drops Data
When your logic analyzer shows garbage or your peripheral returns 0xFF for every byte, the issue almost always falls into one of these three categories.
- Clock Polarity and Phase (CPOL/CPHA) Mismatch: SPI defines four modes based on whether the clock idles high or low (CPOL) and whether data is sampled on the leading or trailing edge (CPHA). Mode 0 (CPOL=0, CPHA=0) and Mode 3 (CPOL=1, CPHA=1) account for 95% of devices. If your peripheral expects Mode 3 and you send Mode 0, the data will be shifted by exactly one bit. Fix: Check the peripheral datasheet and set the
SPISettingsmode accordingly. - MISO Bus Contention: If you have multiple devices on the same SPI bus, their MISO pins are tied together. When a device's CS is HIGH, it must tri-state (high-impedance) its MISO pin. Cheap breakout boards sometimes fail to implement this tri-state buffer, meaning they constantly drive MISO. Fix: If using a non-compliant module, route its MISO through a 74LVC125A tri-state buffer gated by its CS line.
- Capacitive Loading on Long Wires: Running 20cm Dupont wires at 40 MHz will result in severe signal ringing due to parasitic capacitance and inductance. Fix: Drop the clock speed to 10 MHz for breadboard prototypes, or keep traces under 5cm on a custom PCB for high-speed operation.
Sniffing and Debugging the SPI Bus
You cannot debug a 10 MHz SPI connection with a standard multimeter, and a basic serial monitor won't show you clock glitches. You need a logic analyzer.
For professional bench work, the Saleae Logic Pro 8 (~$400) offers sufficient sample rates (500 MS/s) to cleanly capture 80 MHz SPI edges. For hobbyists, a $15 clone of the original Saleae Logic (24 MHz max sample rate) running the open-source Sigrok / PulseView software is more than adequate for SPI buses running under 8 MHz.
Debugging workflow:
- Connect CH0 to SCK, CH1 to MOSI, CH2 to MISO, and CH3 to CS.
- Set the trigger to the falling edge of CS.
- Decode the first byte from the master (MOSI). If it is a read command (e.g.,
0x03), verify that the slave responds on MISO immediately after the address bytes are clocked out. - If MISO stays high (
0xFF), the peripheral is not waking up (check CS wiring and pull-ups).
Minimal Working Exchange: ESP32 to W25Q32 Flash
Below is a complete, wiring-verified example using an ESP32-WROOM-32 to read the JEDEC Manufacturer ID from a W25Q32JV SPI Flash chip. The JEDEC ID command is 0x9F.
| ESP32-WROOM-32 Pin | W25Q32JV Flash Pin | Function |
|---|---|---|
| GPIO 18 (VSPI SCK) | Pin 6 (CLK) | Serial Clock |
| GPIO 23 (VSPI MOSI) | Pin 5 (DI) | Master Out, Slave In |
| GPIO 19 (VSPI MISO) | Pin 2 (DO) | Master In, Slave Out |
| GPIO 5 (VSPI CS) | Pin 1 (/CS) | Chip Select (Add 10k pull-up to 3.3V) |
| 3V3 | Pin 8 (VCC), Pin 3 (/WP), Pin 7 (/HOLD) | Power & Tie control pins high |
| GND | Pin 4 (GND) | Ground |
#include <SPI.h>
// ESP32 VSPI defaults: SCK=18, MISO=19, MOSI=23, SS=5
const int CS_PIN = 5;
SPIClass vspi(VSPI);
void setup() {
Serial.begin(115200);
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect flash
// Initialize VSPI at 20MHz, MSB first, SPI Mode 0
vspi.begin();
delay(1000); // Wait for serial monitor
Serial.println("Reading W25Q32JV JEDEC ID...");
// Begin transaction
vspi.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_PIN, LOW);
vspi.transfer(0x9F); // JEDEC ID command
uint8_t mfr_id = vspi.transfer(0x00);
uint8_t mem_type = vspi.transfer(0x00);
uint8_t capacity = vspi.transfer(0x00);
digitalWrite(CS_PIN, HIGH);
vspi.endTransaction();
Serial.printf("Manufacturer: 0x%02X (Winbond=0xEF)\n", mfr_id);
Serial.printf("Memory Type: 0x%02X\n", mem_type);
Serial.printf("Capacity: 0x%02X (0x16 = 32Mbit)\n", capacity);
}
void loop() {
// Single-shot read
}
Protocol Decision Tree: When to Pick SPI
Choosing between SPI, I2C, and UART depends entirely on your bandwidth, distance, and pin-count constraints. Use this decision path to select your bus.
| Requirement | Protocol Choice | Why |
|---|---|---|
| Need > 10 Mbps throughput (TFTs, Audio, Flash) | SPI | Push-pull drivers and dedicated clock lines support 20-80 MHz easily. |
| Need > 50 sensors on minimal wires | I2C | Only 2 wires needed; software addressing supports 100+ nodes. |
| Need > 2 meters cable distance | RS-485 / UART | Differential signaling (RS-485) rejects noise over long twisted pairs. |
| Need simultaneous send/receive (Full Duplex) | SPI or UART | I2C is half-duplex; SPI and UART have dedicated TX/RX or MOSI/MISO lines. |
For deeper physical layer analysis and timing diagrams, refer to the SparkFun SPI Tutorial and the Analog Devices SPI Interface Guide.






