The Serial Peripheral Interface (SPI) is a synchronous, full-duplex, controller-peripheral protocol built for high-speed, short-distance chip-to-chip communication. If you need to push pixels to an ILI9341 TFT display, log data to an SD card, or read external flash memory, SPI is the undisputed king of the workbench. Unlike I2C, which chokes on high-bandwidth payloads, SPI separates data lines (in and out) and uses a dedicated clock, allowing it to routinely hit 24 MHz to 50 MHz in embedded designs.
The Physical Layer: Wiring the SPI Bus Protocol
You cannot debug SPI without understanding its physical topology. The bus relies on four shared or dedicated lines. Note that the open-source hardware community has recently pushed to rename MOSI/MISO to COPI/CIPO (Controller Out/In, Peripheral In/Out), though datasheets still heavily use the legacy terms.
| Parameter | Specification / Detail |
|---|---|
| Wires | SCK (Clock), MOSI/COPI (Data Out), MISO/CIPO (Data In), CS/SS (Chip Select) |
| Speed | 1 MHz to 50+ MHz (Hobbyist sensors typically max at 8–10 MHz) |
| Addressing | None. Hardware routing via individual Chip Select (CS) lines per device. |
| Distance | < 30 cm for high speeds (>10 MHz); up to 1 meter at <1 MHz with proper termination. |
| Topology | Shared Bus (SCK, MOSI, MISO) + Star routing (Individual CS to each peripheral). |
Physical Wiring and Pull-Up Requirements
A common misconception is that SPI requires pull-up resistors on the data lines like I2C does. It does not. SCK, MOSI, and MISO are push-pull driven and should never have pull-ups attached; doing so will round off your signal edges and cause bit errors at high clock speeds.
Protocol Selection: When SPI Wins Over I2C and UART
Choosing the right bus comes down to a strict trade-off between distance, speed, and device count. Here is how the SPI bus protocol fits into the broader embedded ecosystem.
| Criteria | SPI | I2C | UART (Serial) |
|---|---|---|---|
| Best Fit For | High speed, short distance, moderate device count (displays, SD cards, flash). | Low speed, short distance, high device count (sensors, EEPROMs, OLEDs). | Point-to-point telemetry, GPS modules, long-distance RS-485 networks. |
| Max Speed | 50+ MHz | 3.4 MHz (Fast Mode Plus) | ~3 Mbps (practical limit for most MCUs) |
| Wiring Overhead | 4 shared wires + 1 CS wire per device. | 2 shared wires (SDA, SCL) for all devices. | 2 wires (TX, RX) per point-to-point link. |
| Duplex | Full-Duplex (Simultaneous send/receive). | Half-Duplex. | Full-Duplex. |
Minimal Working Exchange: ESP32 to SPI Sensor
Let's look at a minimal, bare-metal SPI exchange. We will read the WHO_AM_I register (0x75) from an MPU-9250 IMU sensor using an ESP32-WROOM-32. This register should return 0x71, confirming the wiring and clock phase are correct.
Wiring Table
| ESP32-WROOM-32 Pin | MPU-9250 SPI Pin | Function |
|---|---|---|
| GPIO 18 | SCK | SPI Clock (VSPI) |
| GPIO 23 | SDI (MOSI) | Controller Out |
| GPIO 19 | SDO (MISO) | Peripheral Out |
| GPIO 5 | NCS (CS) | Chip Select (Active LOW) |
| 3V3 | VCC | Power (with 100nF decoupling cap) |
| GND | GND | Common Ground |
Arduino C++ Exchange Code
#include <SPI.h>
const int CS_PIN = 5;
const uint8_t WHO_AM_I_REG = 0x75;
const uint8_t SPI_READ_BIT = 0x80; // Bit 7 high indicates a read operation
void setup() {
Serial.begin(115200);
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect peripheral immediately
// Initialize VSPI bus at 1MHz, MSB first, SPI Mode 0
SPI.begin(18, 19, 23, CS_PIN);
delay(100); // Allow sensor boot time
}
void loop() {
uint8_t result = 0;
// Begin transaction with specific speed and mode
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_PIN, LOW); // Assert Chip Select
// Send register address with read bit set
SPI.transfer(WHO_AM_I_REG | SPI_READ_BIT);
// Clock out the data (send dummy byte 0x00 to receive)
result = SPI.transfer(0x00);
digitalWrite(CS_PIN, HIGH); // Deassert Chip Select
SPI.endTransaction();
Serial.printf("WHO_AM_I Register: 0x%02X\n", result);
delay(2000);
}
Debugging the Classic Failures
When your SPI bus returns garbage, do not guess. Hook up a logic analyzer. A basic 24MHz 8-channel clone (like a DSLogic Plus or Saleae Logic 8) is mandatory for embedded work. Connect SCK, MOSI, MISO, and CS, set the decoder to SPI, and look for these three classic failures.
1. Baud Mismatch and SPI Modes
SPI has no auto-baud negotiation. If your peripheral's datasheet specifies a 4 MHz maximum clock and your SPISettings requests 8 MHz, the peripheral's shift register will fail to keep up, and MISO will return 0xFF or 0x00. Furthermore, verify your SPI Mode (CPOL and CPHA). Mode 0 (Clock idle LOW, sample on leading edge) is standard, but thermocouple amplifiers like the MAX31855 require Mode 1 or 3. Check the timing diagram in the Analog Devices SPI Guide to confirm the clock polarity.
2. The 'Address Clash' (Chip Select Collisions)
Because SPI lacks software addressing, an 'address clash' actually means a Chip Select collision. This happens when two devices share a single CS pin, or when you forget to configure unused CS pins on your microcontroller as OUTPUT and set them HIGH. If a CS pin is left as a floating input, ambient noise can accidentally activate a peripheral, causing it to drive the MISO line and corrupt data from the device you are actually trying to talk to.
3. MISO Bus Contention (Missing Tri-State)
When a peripheral's CS pin is HIGH, it is supposed to disconnect its MISO pin from the bus (high-impedance tri-state). Cheap or poorly designed breakout boards sometimes fail to implement this tri-state buffer. If you have multiple devices on one bus and one lacks tri-state logic, it will physically fight the active device for control of the MISO line. The fix is to route a separate MISO wire to the offending device or use a 74LVC125 tri-state buffer IC.
SPI Bus Protocol FAQ
What is the maximum cable length for the SPI bus protocol?
For standard 3.3V logic at speeds above 10 MHz, keep traces and wires under 30 cm (12 inches). The fast edge rates of SPI clocks cause severe signal reflection and ringing on long, unterminated wires. If you must run SPI over a meter, drop the clock speed to 1 MHz, use twisted-pair wiring with a shared ground, and consider adding 33Ω series termination resistors on the SCK and MOSI lines near the controller to dampen reflections.
How do I connect multiple devices to one SPI bus on an ESP32?
Wire the SCK, MOSI, and MISO lines in parallel to all devices. Then, route a dedicated CS wire from a unique GPIO pin to each device. The ESP32 has two usable hardware SPI buses (HSPI and VSPI). If you exceed the capacitive load limit of a single bus (usually around 3 or 4 devices at high speeds), move the overflow devices to the second SPI bus. The Espressif SPI Master Documentation details how to map these buses to specific GPIO matrices.
What are SPI Mode 0, 1, 2, and 3 (CPOL and CPHA)?
SPI modes define the clock polarity (CPOL) and clock phase (CPHA).
Mode 0: Clock idles LOW, data sampled on the rising (leading) edge. (Most common).
Mode 1: Clock idles LOW, data sampled on the falling (trailing) edge.
Mode 2: Clock idles HIGH, data sampled on the falling (leading) edge.
Mode 3: Clock idles HIGH, data sampled on the rising (trailing) edge.
Always check your peripheral's datasheet; using Mode 0 on a Mode 3 device will result in shifted, corrupted bytes.
Why is my SPI MISO line returning 0xFF or 0x00?
A solid 0xFF usually means the MISO line is floating (pulled high by a multimeter or internal weak pull-up) and the peripheral is not responding. This is caused by a broken CS connection, the peripheral being in a sleep state, or a baud rate that is too fast for the chip. A solid 0x00 often means the MISO line is shorted to ground, or the peripheral is actively driving the line low because it is stuck in a fault state. Use a logic analyzer to verify that the CS line is actually pulling low during the transaction.






