The Serial Peripheral Interface (SPI) is the undisputed workhorse for high-speed, short-distance communication between a microcontroller and local peripherals like SD cards, TFT displays, and flash memory. Unlike protocols that prioritize bus topology or long-distance transmission, SPI prioritizes raw throughput and simplicity. However, its lack of built-in hardware handshaking means physical layer mistakes will silently corrupt your data.
This primer bypasses the abstract theory and goes straight to the bench: physical wiring rules, protocol selection, a working ESP32 exchange, and how to sniff the bus when things go wrong.
The Physical Layer: Wiring and Bus Mechanics
SPI is a synchronous, full-duplex, push-pull bus. It relies on four primary logic lines. While modern datasheets sometimes use COPI/CIPO (Controller Out/In Peripheral In/Out), the legacy MOSI/MISO terminology remains dominant in 2026 schematic captures and silkscreens.
| Parameter | SPI Specification | Practical Bench Notes |
|---|---|---|
| Wires | 4 shared (SCK, MOSI, MISO) + 1 per device (CS) | Wire count scales linearly with device count due to individual Chip Select lines. |
| Speed | 1 MHz to 50+ MHz (Peripheral dependent) | SD cards typically run at 4 MHz (init) up to 25 MHz (High Speed). TFTs often push 40-80 MHz. |
| Addressing | None (Hardware routed via CS pins) | No software addresses (like I2C's 0x68). Routing is handled by the master asserting specific GPIOs. |
| Distance | < 30 cm (1 foot) at high speeds | Parasitic capacitance destroys signal integrity above 10 MHz on long, unshielded jumper wires. |
Protocol Selection: When to Use SPI vs. I2C vs. UART
Choosing the right protocol depends entirely on your constraints regarding distance, speed, and device count. Here is the decision framework for local embedded buses.
| Criteria | SPI | I2C | UART | RS-485 |
|---|---|---|---|---|
| Max Speed | Very High (50+ MHz) | Medium (400 kHz - 3.4 MHz) | Low/Med (115.2 kbps - 2 Mbps) | Medium (up to 10 Mbps) |
| Max Distance | Short (< 30 cm) | Short (< 1 meter) | Medium (< 15 meters) | Long (up to 1200 meters) |
| Device Count | Low (Limited by CS GPIOs) | High (up to 127 via addresses) | Point-to-Point (1 to 1) | High (up to 32/256 nodes) |
| Best Use Case | SD Cards, Displays, ADCs | Sensors, EEPROMs, OLEDs | GPS, Cellular, PC Debug | Industrial, HVAC, Long runs |
The Verdict: Choose SPI when you need to move large blocks of data (like filesystem reads or framebuffer updates) quickly across a single PCB or short ribbon cable. Choose I2C when you have dozens of low-bandwidth sensors and want to save GPIO pins. Choose UART/RS-485 for off-board, long-distance, or inter-system communication.
Minimal Working Exchange: ESP32 to SD Card Module
Below is a complete, copy-pasteable implementation for initializing an SD card over SPI using an ESP32. This uses the default VSPI hardware pins.
Physical Wiring Map
| ESP32 Pin (VSPI) | GPIO Number | SD Card Module Pin | Notes |
|---|---|---|---|
| VSPI SCK | GPIO 18 | SCK (CLK) | Clock signal |
| VSPI MISO | GPIO 19 | MISO (DO) | Data from SD to ESP32 |
| VSPI MOSI | GPIO 23 | MOSI (DI) | Data from ESP32 to SD |
| VSPI CS | GPIO 5 | CS (SS) | Add 10kΩ pull-up to 3.3V |
| 3V3 | - | VCC | Ensure module has a 3.3V LDO |
| GND | - | GND | Common ground required |
Arduino Framework Code
#include <SPI.h>
#include <SD.h>
// ESP32 VSPI Default Pins
#define SCK_PIN 18
#define MISO_PIN 19
#define MOSI_PIN 23
#define CS_PIN 5
void setup() {
Serial.begin(115200);
while(!Serial) { delay(10); }
Serial.println("Initializing SD card...");
// Explicitly define SPI pins for ESP32 to avoid routing errors
SPI.begin(SCK_PIN, MISO_PIN, MOSI_PIN, CS_PIN);
if (!SD.begin(CS_PIN)) {
Serial.println("[ERROR] Card Mount Failed. Check wiring and CS pull-up.");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("[ERROR] No SD card attached.");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC) Serial.println("MMC");
else if(cardType == CARD_SD) Serial.println("SDSC");
else if(cardType == CARD_SDHC) Serial.println("SDHC");
Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
}
void loop() {
// Main application logic here
}
Sniffing the Bus and Debugging Classic Failures
When your SPI peripheral returns garbage data or fails to initialize, a multimeter is useless. You need a logic analyzer (like a Saleae Logic or a DSLogic Plus) sampling at least 4 to 10 times faster than your SCK frequency. If your SPI clock is 4 MHz, set your analyzer to sample at 24 MHz or higher to accurately capture edge transitions.
Here are the classic SPI failures and how to identify them:
- The 'Address Clash' (MISO Contention): SPI doesn't use software addresses, so an 'address clash' actually means a Chip Select routing error. If two devices share the MISO line and both have their CS lines pulled low simultaneously, both will try to drive MISO. On a logic analyzer, you will see the MISO line stuck at a mid-level voltage (around 1.5V on a 3.3V system) or erratic square waves. Fix: Ensure every peripheral has a dedicated CS pin, and verify no CS pins are accidentally tied together on the PCB.
- Missing CS Pull-Up (Boot Glitch): If your peripheral initializes randomly after a power cycle, capture the CS line during MCU boot. You will likely see the CS line fluttering before the MCU's GPIO driver takes control. Fix: Solder a 10kΩ resistor between the CS line and VCC.
- Baud Mismatch and Capacitance: If your code works on a 5cm breadboard but fails on a 30cm ribbon cable, you are hitting parasitic capacitance. The logic analyzer will show the SCK and MOSI lines looking like 'shark fins' (rounded edges) rather than crisp square waves, causing the peripheral to misread bits. Fix: Lower the SPI clock speed (e.g., from 20 MHz to 4 MHz) or use a specialized buffer IC like the 74LVC125A for long runs.
- CPOL/CPHA Mismatch (SPI Modes): SPI defines 4 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 your logic analyzer decodes the correct hex bytes but your microcontroller reads them backwards or shifted by one bit, you are sampling on the wrong clock edge. Fix: Check the peripheral datasheet and configure your MCU's SPI mode accordingly (e.g.,
SPI_MODE3for most SD cards).
Serial Peripheral Interface SPI Frequently Asked Questions
Can I connect multiple devices to the same serial peripheral interface SPI bus?
Yes, but with strict wiring rules. You can share the SCK, MOSI, and MISO lines among dozens of devices, provided every single device has its own dedicated Chip Select (CS) line routed back to the master. The master must only pull one CS line LOW at a time. If you run out of GPIO pins for CS lines, you can use a 3-to-8 line decoder (like the 74HC138) to expand your CS routing using only 3 master GPIOs.
What is the maximum cable length for a serial peripheral interface SPI connection?
There is no hard standard, but practically, SPI is limited to about 30 cm (12 inches) at high speeds (10+ MHz) due to parasitic capacitance and crosstalk. If you must run SPI over a longer distance (up to 1-2 meters), you must drop the clock speed to 1 MHz or lower, use twisted-pair wiring (pairing SCK with GND, and MOSI/MISO with GND), and terminate the lines. For anything beyond 2 meters, abandon SPI and use RS-485 or CAN bus.
Why is my serial peripheral interface SPI device returning all 0xFF or 0x00 bytes?
Reading all 0xFF usually means the MISO line is floating high (the peripheral is not connected, is unpowered, or its CS line is not being asserted). Reading all 0x00 typically means the MISO line is shorted to ground, or the peripheral is held in a reset state. Hook up a logic analyzer to the CS and MISO pins: if CS goes LOW but MISO never transitions, the peripheral is either dead, unpowered, or wired to the wrong MISO pin.






