The serial peripheral interface protocol (SPI) is a synchronous, full-duplex, four-wire bus used for short-distance, high-speed communication between a microcontroller (controller) and peripheral ICs. If you need to move data faster than 1 Mbps across a single printed circuit board (PCB) and have the GPIO pins to spare, SPI is your default choice. Unlike I2C, which relies on software addressing and open-drain pull-ups, SPI uses dedicated hardware chip-select lines and push-pull logic, allowing it to hit clock speeds of 20 MHz to 100+ MHz on well-routed boards.
Bus Mechanics and Spec Sheet
Before wiring up a breakout board, you need to understand the physical limits of the bus. The table below defines the hard boundaries of the serial peripheral interface protocol in real-world hobbyist and prosumer environments.
| Parameter | Specification / Real-World Value |
|---|---|
| Wires Required | 4 shared (SCLK, MOSI/COPI, MISO/CIPO, GND) + 1 dedicated CS per peripheral |
| Topology | Controller-Peripheral (formerly Master-Slave); Multi-peripheral requires individual CS lines or a daisy-chain (MISO to MOSI) |
| Speed (Clock) | 1 MHz – 20 MHz (breadboard/jumper wires); 20 MHz – 100+ MHz (proper PCB impedance routing) |
| Addressing | None (Hardware routing via Chip Select / Slave Select lines) |
| Max Distance | < 30 cm (1 foot) at >10 MHz; up to 1 meter at <1 MHz (heavily dependent on parasitic capacitance) |
| Duplex | Full-duplex (simultaneous transmit and receive on every clock edge) |
Physical Wiring, Pull-Ups, and Signal Integrity
The physical layer of SPI is deceptively simple, which is exactly why it fails in unpredictable ways when builders ignore signal integrity. You are routing high-frequency digital square waves; treat them accordingly.
The Four Core Lines
- SCLK (Serial Clock): Driven exclusively by the controller. Dictates the data rate.
- MOSI / COPI (Controller Out, Peripheral In): Data sent from the microcontroller to the target.
- MISO / CIPO (Controller In, Peripheral Out): Data sent from the target back to the microcontroller.
- CS / SS (Chip Select): Active-LOW in 99% of devices. Pulling this line LOW wakes the peripheral and tells it to listen to SCLK and drive MISO.
While SCLK, MOSI, and MISO are push-pull and do not need pull-up resistors, the CS line absolutely does. If your microcontroller's GPIO defaults to high-impedance (floating) during boot or reset, the peripheral's CS pin will float. A floating active-LOW CS pin will randomly wake the peripheral, causing it to drive the MISO line and collide with other devices on the bus. Always place a 10kΩ pull-up resistor to VCC on every CS line.
Voltage Translation
Mixing 5V and 3.3V logic on an SPI bus will fry your peripheral or result in unreadable logic highs. Do not use passive resistor dividers for SPI; the RC time constant formed by the resistor and the wire's parasitic capacitance will round off your square waves, destroying the clock edges at anything above 1 MHz. Use a dedicated bidirectional logic level shifter like the Texas Instruments 74LVC1T45 or a 4-channel MOSFET-based breakout board.
The Classic SPI Failures (And How to Sniff Them)
When an I2C bus fails, it's usually an address clash or a missing pull-up. When the serial peripheral interface protocol fails, it manifests differently. Here is the troubleshooting decision path for a dead SPI bus.
1. The "Address Clash" (CS Contention)
Symptom: Data reads back as 0xFF or 0x00, or the bus locks up when a second peripheral is added.
Cause: Unlike I2C, SPI has no software addresses. If you wire two peripherals to the same CS pin, they will both try to drive the MISO line simultaneously when selected, causing a short circuit and data corruption.
Fix: Route a dedicated CS pin from the controller to every single peripheral. If you are out of GPIOs, use a 74HC138 3-to-8 line decoder to expand your CS lines.
2. The Baud Mismatch (Clock Phase and Polarity)
Symptom: The logic analyzer shows data, but the bytes are shifted by one bit or completely garbled.
Cause: SPI defines four "Modes" based on Clock Polarity (CPOL) and Clock Phase (CPHA). Mode 0 (CPOL=0, CPHA=0) is the most common, but many sensors (like the MAX31855 thermocouple amplifier) require Mode 1 or Mode 3. If the controller samples the MISO line on the wrong clock edge, you read garbage.
Fix: Check the peripheral datasheet's timing diagram. Set your microcontroller's SPI library to match. For Arduino/ESP32, this is SPI_MODE0 through SPI_MODE3.
3. Capacitance-Induced Baud Failure
Symptom: Works perfectly at 1 MHz, returns garbage at 10 MHz.
Cause: Long jumper wires act as capacitors. At 10 MHz, the SCLK edge cannot charge the wire capacitance fast enough to cross the logic threshold before the next clock edge.
Fix: Lower the baud rate, shorten the wires, or add a series termination resistor (typically 33Ω to 50Ω) at the source of the SCLK and MOSI lines to dampen ringing.
Sniffing the Bus
Never debug SPI with just a multimeter. You need a logic analyzer. A basic Saleae Logic 8 or a budget DreamSourceLab DSLogic U3Pro16 will capture the bus. Connect SCLK, MOSI, MISO, and CS to the analyzer, set the sample rate to at least 4x your expected SPI clock speed (e.g., 40 MS/s for a 10 MHz bus), and use the software's SPI decoder to read the hex bytes directly.
Minimal Working Exchange: ESP32 to SPI EEPROM
Below is a complete, copy-pasteable example of writing and reading a byte to a Microchip 25LC640 SPI EEPROM using an ESP32. This demonstrates proper CS management, transaction settings, and byte ordering.
Wiring Table
| ESP32 Pin (HSPI Bus) | 25LC640 EEPROM Pin | Notes |
|---|---|---|
| GPIO 14 (SCK) | Pin 6 (SCK) | Add 33Ω series resistor if wires > 10cm |
| GPIO 13 (MOSI) | Pin 5 (SI) | Controller Out, Peripheral In |
| GPIO 12 (MISO) | Pin 2 (SO) | Controller In, Peripheral Out |
| GPIO 15 (CS) | Pin 1 (CS) | Requires 10kΩ pull-up to 3.3V |
| 3.3V | Pin 8 (VCC), Pin 7 (HOLD), Pin 3 (WP) | Tie HOLD and WP high to disable those features |
| GND | Pin 4 (VSS) | Common ground is mandatory |
Arduino / ESP32 Code
#include <SPI.h>
// ESP32 HSPI pin definitions
#define CS_PIN 15
#define MOSI_PIN 13
#define MISO_PIN 12
#define SCK_PIN 14
// 25LC640 SPI Instructions
#define CMD_WRITE 0x02
#define CMD_READ 0x03
#define CMD_WREN 0x06
SPIClass spi_bus(HSPI);
void setup() {
Serial.begin(115200);
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect peripheral immediately
// Initialize HSPI bus with custom pins
spi_bus.begin(SCK_PIN, MISO_PIN, MOSI_PIN, CS_PIN);
// Write a test byte to address 0x0000
uint16_t target_addr = 0x0000;
uint8_t test_data = 0xA5;
spi_write_byte(target_addr, test_data);
delay(10); // EEPROM write cycle time is typically 5ms
// Read it back
uint8_t read_data = spi_read_byte(target_addr);
Serial.printf("Read back: 0x%02X\n", read_data);
}
void spi_write_byte(uint16_t addr, uint8_t data) {
// Send Write Enable command first
spi_bus.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_PIN, LOW);
spi_bus.transfer(CMD_WREN);
digitalWrite(CS_PIN, HIGH);
spi_bus.endTransaction();
// Send Write command, address, and data
spi_bus.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_PIN, LOW);
spi_bus.transfer(CMD_WRITE);
spi_bus.transfer((addr >> 8) & 0xFF); // Address MSB
spi_bus.transfer(addr & 0xFF); // Address LSB
spi_bus.transfer(data);
digitalWrite(CS_PIN, HIGH);
spi_bus.endTransaction();
}
uint8_t spi_read_byte(uint16_t addr) {
uint8_t result = 0;
spi_bus.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_PIN, LOW);
spi_bus.transfer(CMD_READ);
spi_bus.transfer((addr >> 8) & 0xFF);
spi_bus.transfer(addr & 0xFF);
result = spi_bus.transfer(0x00); // Clock out the data
digitalWrite(CS_PIN, HIGH);
spi_bus.endTransaction();
return result;
}
void loop() {
// Nothing to do here
}
Protocol Decision Tree: When to Pick SPI
Do not default to SPI for every sensor. Use this decision matrix to select the right bus for your specific hardware constraints. For deeper architectural theory, refer to the Analog Devices SPI Guide or the SparkFun SPI Tutorial.
| Constraint / Requirement | SPI | I2C | UART |
|---|---|---|---|
| Data Rate Needed | > 1 Mbps (High Speed) | 100 kbps to 3.4 Mbps | 9600 bps to 1 Mbps |
| Available GPIO Pins | Low (4 shared + 1 per device) | High (Only 2 shared for all) | Low (2 dedicated per pair) |
| Physical Distance | < 30 cm (Intra-board) | < 1 meter (Intra-system) | > 10 meters (RS-485/UART) |
| Device Count on Bus | 1 to 4 (CS routing gets messy) | Up to 127 (Software addressed) | 1-to-1 (Point-to-point) |
IF your peripheral requires high-speed data streaming (e.g., TFT displays, SD cards, ADCs sampling >100kSPS) AND the distance is under 30cm, THEN use the serial peripheral interface protocol.
Default Implementation: Route SPI on your PCB with 50-ohm impedance traces, keep SCLK under 20 MHz for breadboard prototypes, and use the 74LVC1T45 for any 5V-to-3.3V translation. If you are chaining more than 3 peripherals and running out of CS pins, switch to I2C or add a GPIO expander.






