SPI (Serial Peripheral Interface) is a synchronous, full-duplex, four-wire serial communication bus. Originally developed by Motorola in the 1980s, it operates on a strict controller-peripheral (historically master-slave) architecture. Unlike asynchronous protocols, SPI relies on a shared clock line to shift bits in and out simultaneously, making it the undisputed standard for high-speed, short-distance data transfer on the workbench. If you need to push megabytes of pixel data to a TFT display or read a 24-bit ADC at 30,000 samples per second, SPI is the physical layer doing the heavy lifting.
The Physical Layer: How SPI Works on the Wire
To understand how SPI works, you have to look past the software abstractions and examine the copper. A standard SPI bus requires four shared lines, plus one additional Chip Select (CS) line for every peripheral you add to the network. Data is shifted in and out on the edges of the clock signal, governed by two parameters: Clock Polarity (CPOL) and Clock Phase (CPHA), which combine to form SPI Modes 0 through 3.
| Parameter | SPI Specification | Practical Bench Reality |
|---|---|---|
| Wires | 4 shared (SCK, COPI, CIPO) + 1 CS per device | Pin count explodes on multi-device buses; requires a multiplexer or daisy-chain (MISO-to-MOSI) for >3 devices. |
| Speed | 1 MHz to 50 MHz typical | High-speed ADCs (like the TI ADS1256) push 8 MHz; TFT displays (ILI9341) easily consume 40 MHz clocks. |
| Addressing | Hardware routing via individual CS lines | No software addressing overhead. The controller asserts a specific CS pin LOW to talk to one peripheral. |
| Distance | < 1 meter (highly dependent on capacitance) | At 20 MHz, stray capacitance over 50cm of ribbon cable will ring the clock line and corrupt data. |
| Duplex | Full-duplex | Controller sends on COPI while simultaneously reading on CIPO. Highly efficient for register read/writes. |
Physical Wiring and Pull-Up Requirements
A common mistake when migrating from I2C to SPI is applying I2C physical layer rules to an SPI bus. SPI does not require pull-up resistors on SCK, COPI (MOSI), or CIPO (MISO). These lines are actively driven push-pull by the controller and peripheral. Adding pull-ups here will only increase rise/fall times and limit your maximum clock speed.
SPI vs. I2C vs. UART: The Protocol Decision Tree
Choosing the right protocol comes down to distance, speed, and device count. Use this decision matrix to terminate your design process with a concrete pick.
| Condition / Constraint | Protocol Pick | Concrete Part / Standard Example |
|---|---|---|
| Distance > 10 meters, noisy industrial environment | RS-485 / CAN | MAX485 transceiver, CAN2.0B |
| Device count > 10, speed < 400 kHz, only 2 wires available | I2C | BME280, PCA9685 PWM driver |
| Point-to-point debug console, no clock line available | UART | FTDI FT232RL, 115200 baud |
| Speed > 1 Mbps, distance < 1m, high data volume | SPI (Default Pick) | W25Q128 Flash, ILI9341 TFT |
The Verdict: If you are moving bulk data (audio, filesystems, display frames) across a single PCB or a short 10cm ribbon cable, default to SPI. If you are just polling a temperature sensor once a second, use I2C to save GPIO pins.
The Minimal Working Exchange: Wiring and Code
Let's wire an ESP32 DevKit V1 to a generic SPI peripheral (like an MAX31855 thermocouple amplifier or an SPI FRAM chip). We will use the ESP32's hardware VSPI bus to offload the bit-banging from the CPU.
Wiring Table (ESP32 VSPI to Peripheral)
| ESP32 Pin (VSPI) | SPI Peripheral Pin | Function |
|---|---|---|
| GPIO 18 (SCK) | SCK / CLK | Clock Signal |
| GPIO 23 (MOSI) | COPI / MOSI / SDI | Controller Out, Peripheral In |
| GPIO 19 (MISO) | CIPO / MISO / SDO | Controller In, Peripheral Out |
| GPIO 5 (CS) | CS / SS / CE | Chip Select (Active LOW) |
| 3V3 | VCC | Power (Check peripheral logic level!) |
| GND | GND | Common Ground |
ESP32 Arduino Core Code Example
This code initializes the hardware SPI bus, configures the transaction settings (Mode 0, 4 MHz), and executes a single-byte read/write exchange.
#include <SPI.h>
// ESP32 VSPI Hardware Pin Definitions
#define SPI_SCK 18
#define SPI_MISO 19
#define SPI_MOSI 23
#define SPI_CS 5
// Initialize the hardware SPI class for ESP32
SPIClass vspi(VSPI);
void setup() {
Serial.begin(115200);
// Configure CS pin as output and set HIGH (deselected)
pinMode(SPI_CS, OUTPUT);
digitalWrite(SPI_CS, HIGH);
// Start SPI bus with explicit pin mapping
vspi.begin(SPI_SCK, SPI_MISO, SPI_MOSI, SPI_CS);
Serial.println("SPI Bus Initialized.");
}
void loop() {
// Begin transaction: 4MHz, MSB first, SPI Mode 0 (CPOL=0, CPHA=0)
vspi.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
// Assert Chip Select (Active LOW)
digitalWrite(SPI_CS, LOW);
// Full-duplex exchange: Send 0x8F (read command), simultaneously receive status byte
uint8_t tx_byte = 0x8F;
uint8_t rx_byte = vspi.transfer(tx_byte);
// Deassert Chip Select
digitalWrite(SPI_CS, HIGH);
vspi.endTransaction();
Serial.printf("Sent: 0x%02X | Received: 0x%02X\n", tx_byte, rx_byte);
delay(1000);
}
Classic Bus Failures and Debugging with a Logic Analyzer
Every protocol has its Achilles heel. While I2C is notorious for address clashes and missing pull-ups, and UART routinely suffers from baud mismatch, SPI fails in entirely different ways. If your SPI bus is returning 0xFF or 0x00, check these three classic failure modes:
- CPOL/CPHA Mode Mismatch: SPI has four modes. Mode 0 (Clock idle LOW, sample on leading edge) is the most common, but many RF modules (like the nRF24L01) and SD cards require Mode 3. If you use Mode 0 on a Mode 3 device, the peripheral will sample the data line when the clock is transitioning, resulting in garbage data. Always check the peripheral's datasheet timing diagram.
- Baud Rate Exceeding Peripheral Limits: The ESP32 can easily generate an 80 MHz SPI clock. If you connect a cheap logic-level SD card module that maxes out at 25 MHz, the clock edges will ring, and the SD card controller will silently drop packets. Start your debugging at 1 MHz, verify communication, and then step up the speed.
- Missing Common Ground: Because SPI is single-ended (unlike differential RS-485), the voltage levels are referenced to ground. If you are powering a peripheral from a separate bench supply and forget to tie the GND pins together, the CIPO line will float relative to the ESP32, causing random bit flips.
How to Sniff and Debug the Bus
When software fails, you must look at the physical layer. Connect a logic analyzer (like a Saleae Logic Pro 8 or a budget DSLogic Plus) to the four SPI lines.
- Trigger: Set the trigger to the falling edge of the CS line.
- Sample Rate: Set your logic analyzer sample rate to at least 4x to 10x your SPI clock speed. For a 10 MHz SPI clock, you need a minimum 40 MS/s capture rate to accurately resolve setup and hold times.
- Decode: Use the analyzer's built-in SPI decoder. Map COPI and CIPO to their respective channels. If the decoded hex values match your `Serial.print()` output on the transmit side, but the receive side shows `0x00`, your peripheral is either unpowered, in the wrong SPI mode, or its CIPO pin is physically damaged.
The Final Verdict: When to Default to SPI
Do not overcomplicate your bus topology. Default to SPI for any local, high-throughput peripheral operating under 1 meter. Specifically, choose SPI when integrating TFT LCDs (ILI9341, ST7789), external Flash memory (W25Q series), or high-resolution ADCs. Reserve I2C for low-bandwidth environmental sensors (BME280, SHT40) where saving three GPIO pins is worth the 400 kHz speed limit. If your wires need to leave the enclosure and run across a room, abandon both and use RS-485. For everything else on the PCB, SPI's full-duplex, push-pull architecture remains the most robust and fastest tool in the embedded engineer's arsenal.






