Serial Peripheral Interface (SPI) is the workhorse of high-speed, short-distance embedded communication. If you are building a project with an ESP32, Arduino, or Raspberry Pi that involves SD cards, TFT displays, or external flash memory, you are using SPI. This practical SPI wiki cuts through the academic theory and gives you the exact physical layer requirements, wiring rules, and debugging techniques you need on the bench.
The direct answer: Use SPI when you need high throughput (1 MHz to 80 MHz) over short distances (under 1 meter) with a low device count (1 to 4 peripherals). If you need to daisy-chain 50 sensors across a room, use RS-485 or CAN. If you need moderate speed with minimal wiring for a dozen sensors, use I2C.
SPI Bus Mechanics & Physical Layer
Unlike I2C, which relies on open-drain lines and software addressing, SPI is a synchronous, full-duplex, push-pull bus. It uses dedicated hardware lines for clocking and data, and individual chip select lines for addressing.
| Parameter | SPI Standard | Practical Maker Limits |
|---|---|---|
| Wires Required | 4 (SCK, MOSI, MISO, CS) + GND | 3 (SCK, MOSI, CS) if read-only or write-only |
| Max Speed | Depends on peripheral (typically 10-50 MHz) | ESP32 hardware SPI supports up to 80 MHz |
| Addressing | Hardware Chip Select (CS/SS) lines | 1 GPIO per peripheral (no software addressing) |
| Max Distance | Not strictly defined by standard | ~30 cm at 10 MHz; ~10 cm at 50 MHz on ribbon cable |
| Topology | Master-Slave (Controller-Peripheral) | Single master, multiple peripherals (shared bus) |
The Pull-Up Reality: SPI vs I2C
A classic mistake when transitioning from I2C to SPI is adding pull-up resistors to the data and clock lines. Do not put pull-ups on MOSI, MISO, or SCK. These lines are driven push-pull by the master and peripheral. Adding pull-ups will cause current contention, slow down edge transitions, and increase EMI.
While data lines don't need pull-ups, the Chip Select (CS) line often does. When your MCU (like an ESP32) boots or resets, its GPIOs temporarily float (high-impedance). During this window, noise can accidentally trigger a peripheral's CS line, causing it to drive MISO and clash with other devices. Place a 10kΩ pull-up resistor to VCC on the CS line at the peripheral side to keep it deselected during MCU boot.
Protocol Selection Matrix: When to Use What
Choosing the right protocol prevents architectural dead-ends. Here is how SPI stacks up against I2C and UART for typical maker scenarios.
| Criteria | SPI | I2C | UART |
|---|---|---|---|
| Best For | High-speed data (Displays, SD cards, Flash) | Low-speed sensor arrays (Temp, IMU, EEPROM) | Point-to-point debug, GPS, Cellular modems |
| Device Count | Low (1-4) due to CS pin exhaustion | High (up to 112 on a single bus) | 1-to-1 (requires multiplexers for more) |
| Wiring Complexity | High (4 wires + 1 per extra device) | Low (2 wires total) | Lowest (2 wires: TX/RX) |
| Duplex | Full (Simultaneous TX/RX) | Half (Shared SDA line) | Full (Separate TX/RX lines) |
Minimal Working Exchange: ESP32 to W25Q32 Flash
Let's look at a concrete, working example. We will read the JEDEC Manufacturer ID from a Winbond W25Q32 SPI flash chip using an ESP32-WROOM-32 DevKit v1. This verifies physical wiring and SPI mode configuration.
Physical Wiring Table
| W25Q32 Pin | ESP32 DevKit v1 Pin | Notes |
|---|---|---|
| VCC (Pin 8) | 3V3 | Do NOT use 5V; W25Q32 is a 3.3V part |
| GND (Pin 4) | GND | Common ground required |
| CLK (Pin 6) | GPIO 18 (SCK) | Hardware SPI2 clock |
| DO (Pin 2) | GPIO 19 (MISO) | Data Out from Flash |
| DI (Pin 5) | GPIO 23 (MOSI) | Data In to Flash |
| CS (Pin 1) | GPIO 5 (SS) | Add 10k pull-up to 3V3 |
Arduino IDE Code (ESP32 Core)
#include <SPI.h>
// ESP32 Hardware SPI pins (VSPI / SPI2)
#define SCK_PIN 18
#define MISO_PIN 19
#define MOSI_PIN 23
#define CS_PIN 5
void setup() {
Serial.begin(115200);
delay(1000);
// Initialize SPI with explicit pin mapping
SPI.begin(SCK_PIN, MISO_PIN, MOSI_PIN, CS_PIN);
Serial.println('SPI Bus Initialized.');
}
void loop() {
// JEDEC ID command is 0x9F, returns 3 bytes: Mfg, MemType, Capacity
uint8_t jedec_id[3] = {0};
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_PIN, LOW);
SPI.transfer(0x9F); // Send Read JEDEC ID command
jedec_id[0] = SPI.transfer(0x00); // Read Mfg ID
jedec_id[1] = SPI.transfer(0x00); // Read Memory Type
jedec_id[2] = SPI.transfer(0x00); // Read Capacity
digitalWrite(CS_PIN, HIGH);
SPI.endTransaction();
// Validate response (0xFFFFFF usually means MISO is floating/disconnected)
if (jedec_id[0] == 0xFF && jedec_id[1] == 0xFF && jedec_id[2] == 0xFF) {
Serial.println('ERROR: Received 0xFFFFFF. Check MISO wiring and peripheral power.');
} else {
Serial.printf('JEDEC ID: 0x%02X 0x%02X 0x%02X\n', jedec_id[0], jedec_id[1], jedec_id[2]);
// Expected for W25Q32: 0xEF (Winbond) 0x40 (NOR Flash) 0x16 (32M-bit)
}
delay(2000);
}
Debugging the Bus: Classic Failures & Sniffing
When SPI fails, it fails silently. Unlike I2C, there are no ACK/NACK bits to tell you the peripheral ignored your data. Here is how to diagnose the classic failure modes.
1. CPOL and CPHA Mismatch (SPI Modes 0-3)
SPI defines four modes based on Clock Polarity (CPOL) and Clock Phase (CPHA). SPI Mode 0 (CPOL=0, CPHA=0) is the default for 90% of sensors and flash chips. If your peripheral expects Mode 3 (clock idles HIGH, data sampled on falling edge) and you send Mode 0, you will read garbage. Always check the peripheral datasheet's timing diagram.
2. Baud Rate vs. Wire Length (Ringing)
If you push 40 MHz over 20 cm of cheap Dupont jumper wires, the parasitic inductance and capacitance will cause signal ringing. The clock edge will overshoot, cross the logic threshold multiple times, and the peripheral will register multiple clock ticks for a single pulse. Fix: Drop the baud rate to 1 MHz to verify logic, then step up. If you need high speed over distance, use twisted pair or a differential driver like the MAX3490 RS-422 transceiver.
3. How to Sniff the Bus
Do not guess; use a logic analyzer. You don't need a $1,000 Saleae Logic Pro 8. A $15 FX2LA-based 8-channel USB logic analyzer running at 24 MHz is sufficient for most SPI debugging. Download PulseView (Sigrok), connect your SCK, MOSI, MISO, and CS probes, and set the decoder to SPI. PulseView will translate the hex bytes in real-time, allowing you to spot exactly where the master's command diverges from the peripheral's expected protocol.
Embedded SPI Wiki FAQ
Can I connect multiple SPI devices to the same ESP32 MISO/MOSI/SCK pins?
Yes, this is called a shared SPI bus. You share the SCK, MOSI, and MISO lines, but each peripheral must have its own dedicated CS (Chip Select) GPIO. The critical hardware requirement is that every peripheral's MISO output must be tri-stated (high-impedance) when its CS line is HIGH. If a peripheral lacks a tri-state MISO (common in cheap DIY modules), it will drag the shared MISO line low, corrupting data from the active device. In that case, use a 74HC125 tri-state buffer.
Why is my SPI transfer returning 0xFF or 0x00?
A continuous stream of 0xFF almost always means the MISO line is floating (disconnected, broken wire, or the peripheral is unpowered and its output is high-impedance). A continuous stream of 0x00 usually means MISO is shorted to ground, or you are reading a peripheral that actively drives MISO low when idle. Verify peripheral VCC with a multimeter and check for continuity on the MISO trace.
What is the maximum reliable cable length for an SPI bus?
For standard 3.3V single-ended logic on ribbon cables, expect reliable operation up to 30 cm at 10 MHz, and 10 cm at 40 MHz. SPI was designed for on-board communication, not inter-board cabling. If you must run SPI over a 2-meter cable to a remote sensor, you must convert the single-ended signals to differential RS-422 using line drivers at both ends to reject common-mode noise.
How do I calculate the exact SPI clock frequency for my sensor?
Start with the peripheral datasheet's 'Maximum SCK Frequency' spec (e.g., 20 MHz). Next, check your MCU's SPI clock divider. On an Arduino Uno (16 MHz ATmega328P), the hardware SPI divider yields exact speeds of 8 MHz, 4 MHz, 2 MHz, etc. You cannot output exactly 20 MHz from a 16 MHz clock. On an ESP32, the SPI.beginTransaction(SPISettings(20000000, ...)) command will configure the internal PLL divider to get as close to 20 MHz as possible. Always leave a 10-20% margin below the datasheet maximum to account for breadboard parasitic capacitance.






