The Serial Peripheral Interface (SPI) is a synchronous, full-duplex, 4-wire bus used for high-speed, short-distance communication between a microcontroller and peripherals like flash memory, TFT displays, and ADCs. Unlike asynchronous protocols, SPI relies on a shared clock line to shift bits in and out simultaneously, making it the default choice when you need to move bulk data quickly across a single printed circuit board (PCB) or a very short cable run.
The SPI Bus at a Glance: Mechanics and Limits
Before wiring up a breakout board, you need to know where SPI fits in the embedded ecosystem. The table below maps the physical and logical limits of SPI against I2C and UART to help you decide which protocol fits your distance, speed, and device count requirements.
| Feature | SPI | I2C | UART |
|---|---|---|---|
| Wires Required | 4 shared + 1 CS per device | 2 shared (SDA, SCL) | 2 per pair (TX, RX) |
| Typical Speed | 1 MHz – 50 MHz (up to 100+ MHz) | 100 kHz – 3.4 MHz | 9600 bps – 1 Mbps |
| Addressing | Hardware Chip Select (CS) lines | 7-bit or 10-bit software address | None (point-to-point) |
| Max Distance | < 1 meter (PCB-level preferred) | < 1 meter (highly capacitance-limited) | Up to 15m (RS-232/485 variants) |
| Duplex | Full (simultaneous TX/RX) | Half | Full |
| Device Count | Limited by available MCU GPIOs for CS | Up to 127 (theoretical) | 1-to-1 (requires multiplexers) |
The Verdict: Choose SPI when you need raw throughput (e.g., streaming audio to a DAC or updating a 320x240 TFT display at 30fps). Choose I2C when you have many low-speed sensors (like BME280 or MPU6050) and want to save GPIO pins. Choose UART for off-board, point-to-point links like GPS modules or PC serial consoles.
Physical Layer: Wiring, Pull-Ups, and the Tri-State Rule
The physical layer of SPI consists of four primary signals. While modern standards bodies are pushing the terms COPI (Controller Out, Peripheral In) and CIPO (Controller In, Peripheral Out), the legacy MOSI and MISO acronyms still dominate silicon datasheets and silkscreens.
- SCK (Serial Clock): Generated by the master. Dictates the shift rate.
- MOSI (Master Out, Slave In): Data sent from the MCU to the peripheral.
- MISO (Master In, Slave Out): Data sent from the peripheral to the MCU.
- CS / SS (Chip Select / Slave Select): Active-LOW signal. Tells a specific peripheral it is being addressed.
Unlike I2C, SPI data and clock lines are push-pull and do not require pull-up resistors. However, CS lines absolutely need 10kΩ pull-ups to VCC. When your ESP32 or Arduino boots, its GPIO pins float before the SPI library initializes. Without a pull-up, a peripheral might see a random LOW glitch on the CS line during boot, causing it to drive the MISO line and create a bus contention that can damage the silicon.
The Tri-State Rule: When a peripheral's CS line is HIGH (deselected), its MISO pin must enter a high-impedance (tri-state) mode. If you are wiring multiple SPI devices to the same MISO bus and one peripheral fails to tri-state (a common flaw in cheap, unbranded logic level shifters or damaged modules), it will short-circuit against the active peripheral's MISO output. If your logic analyzer shows corrupted MISO data but clean MOSI/SCK, suspect a tri-state failure on one of your deselected slaves.
Clock Modes and the Minimal Working Exchange
SPI does not have a universal standard for clock idle states or sampling edges. This is defined by the Clock Polarity (CPOL) and Clock Phase (CPHA), creating four distinct SPI modes. Using the wrong mode is the number one cause of "garbage data" on the bench.
| Mode | CPOL (Idle Clock) | CPHA (Sampling Edge) | Common Use Cases |
|---|---|---|---|
| Mode 0 | LOW (0) | Leading (Rising) | Most common (SD cards, W25Q flash, MCP3008) |
| Mode 1 | LOW (0) | Trailing (Falling) | Some Maxim/Dallas sensors |
| Mode 2 | HIGH (1) | Leading (Falling) | Specific RF transceivers (e.g., nRF24L01 edge cases) |
| Mode 3 | HIGH (1) | Trailing (Rising) | Many TFT display controllers (ILI9341) |
Below is a minimal, robust working exchange using an ESP32 DevKit V1 reading from an MCP3008 (10-bit, 8-channel ADC). The MCP3008 operates in Mode 0,0 and handles 3.3V logic natively, avoiding the need for a level shifter that would otherwise be required if connecting to a 5V Arduino Uno.
| ESP32 GPIO | MCP3008 Pin | Function |
|---|---|---|
| GPIO 18 | 13 (CLK) | SCK |
| GPIO 23 | 11 (DIN) | MOSI |
| GPIO 19 | 12 (DOUT) | MISO |
| GPIO 5 | 10 (CS) | Chip Select |
| 3V3 | 16 (VDD), 15 (VREF) | Power & Reference |
| GND | 14 (AGND), 9 (DGND) | Common Ground |
#include <SPI.h>
const int CS_PIN = 5;
// MCP3008 max clock is ~3.6MHz at 5V, ~1.3MHz at 3.3V. We use 1MHz for safety.
SPISettings spiSettings(1000000, MSBFIRST, SPI_MODE0);
void setup() {
Serial.begin(115200);
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect immediately
SPI.begin();
}
uint16_t readADC(uint8_t channel) {
if (channel > 7) return 0;
uint8_t command = 0b00000001; // Start bit
uint8_t config = (channel << 4) | 0b10000000; // Single-ended mode
SPI.beginTransaction(spiSettings);
digitalWrite(CS_PIN, LOW);
SPI.transfer(command);
uint8_t msb = SPI.transfer(config);
uint8_t lsb = SPI.transfer(0x00);
digitalWrite(CS_PIN, HIGH);
SPI.endTransaction();
// Mask and combine the 10-bit result
return ((msb & 0x03) << 8) | lsb;
}
void loop() {
Serial.printf("Channel 0: %d\n", readADC(0));
delay(500);
}
Debugging the Bus: Sniffing and Classic Failure Modes
When your code compiles but the peripheral returns 0xFFFF or garbage, stop guessing and sniff the bus. You need a logic analyzer. A $15 FX2LP-based clone running the open-source Sigrok / PulseView software is perfectly adequate for SPI debugging up to 10 MHz. For higher speeds, a Saleae Logic 8 or Digilent Analog Discovery is required.
Sniffing Rule of Thumb: Set your logic analyzer sample rate to at least 4x to 10x your SPI clock frequency. If your SCK is 1 MHz, sample at 8 MHz minimum to accurately resolve the CPHA sampling edges.
Here are the classic SPI failure modes you will encounter on the bench, ranked by probability:
- Baud / Mode Mismatch (Garbage Data): If the logic analyzer shows clean square waves but the decoded hex values make no sense, you have a CPOL/CPHA mismatch. Fix: Check the peripheral's datasheet timing diagram. If the data changes on the falling edge but is sampled on the rising edge, you likely need Mode 3 instead of Mode 0.
- Logic Level Clash (Fried MCU): Connecting a 5V Arduino SPI bus directly to a 3.3V ESP32 or Raspberry Pi without a bidirectional level shifter (like the BSS138 or TXS0108E) will backfeed 5V into the 3.3V MISO pin, eventually bricking the GPIO bank. Fix: Always use a level shifter or stick to native 3.3V peripherals when using modern MCUs.
- Missing Common Ground: If you are powering the peripheral from a separate bench supply and forgot to tie the supply ground to the MCU ground, the logic analyzer will show erratic SCK/MOSI signals. Fix: Every signal wire requires a shared ground reference.
- CS Glitching on Boot: The peripheral wakes up in the middle of a transaction because the MCU's CS pin floated during the bootloader phase. Fix: Add a 10kΩ pull-up resistor on the CS line, and add a 100ms delay in your
setup()before callingSPI.begin().
For deeper timing analysis and peripheral-specific quirks, always refer to the silicon manufacturer's reference. The Espressif SPI Master API documentation provides excellent insights into DMA-backed SPI transfers and hardware CS routing limitations on the ESP32, while Analog Devices' SPI interface guides remain the gold standard for understanding the analog implications of digital clock routing and ground bounce at high frequencies.






