Communication SPI (Serial Peripheral Interface) is a synchronous, push-pull, 4-wire protocol optimized for high-speed, short-distance chip-to-chip data transfer. Unlike asynchronous protocols, SPI uses a shared clock line to keep master and slave perfectly synchronized, routinely achieving 10 MHz to 50+ MHz on modern microcontrollers. If you need to move bulk sensor data, drive a TFT display, or interface with an SD card, SPI is your physical layer of choice.
The Physical Layer: SPI Bus Mechanics vs I2C and UART
Before wiring a single pin, you must understand where SPI fits in the embedded ecosystem. Choosing the right protocol depends entirely on your distance, speed, and device count requirements. SPI wins on raw throughput but loses on pin count. I2C saves pins but bottlenecks speed. UART is simple but strictly point-to-point.
| Feature | SPI | I2C | UART |
|---|---|---|---|
| Wires Required | 4 shared (SCK, COPI, CIPO) + 1 CS per device | 2 shared (SDA, SCL) | 2 (TX, RX) |
| Practical Speed | 10 MHz - 50+ MHz | 100 kHz - 3.4 MHz | 9600 bps - 1 Mbps |
| Addressing | Hardware Chip Select (CS) pin per target | 7-bit / 10-bit software address | None (Point-to-Point) |
| Max Distance | < 1 meter (parasitic capacitance kills edges) | < 1 meter | Up to 15m (via RS-485 PHY) |
| Topology | Multi-slave, Single Master (typically) | Multi-master, Multi-slave | Strictly Point-to-Point |
The Decision Framework: Choose SPI when you need high bandwidth (e.g., streaming 9-DOF IMU data or writing to flash memory) and have enough GPIO pins for individual Chip Select lines. Choose I2C when you are pin-constrained and polling low-bandwidth sensors (like a BME280). Choose UART (specifically RS-485) when you need to run a bus across a room or between separate PCBs.
Wiring the Bus: Pinouts, Push-Pull, and Pull-Up Requirements
SPI relies on four shared bus lines, though the naming convention is currently shifting in the industry. The OSHWA (Open Source Hardware Association) now recommends COPI/CIPO over the legacy MOSI/MISO terminology, though you will still see the legacy terms on most 2026 breakout boards.
- SCK (Serial Clock): Generated by the master. Toggles to shift bits in and out.
- COPI / MOSI (Controller Out, Peripheral In): Data line from Master to Slave.
- CIPO / MISO (Controller In, Peripheral Out): Data line from Slave to Master.
- CS / SS (Chip Select / Slave Select): Active-LOW line. The master pulls this LOW to talk to a specific device.
The CS Pull-Up Exception: While the data lines don't need pull-ups, your CS lines absolutely do. When an ESP32 or Arduino boots or resets, its GPIO pins float momentarily before initializing as outputs. If a CS pin floats LOW during boot, the SPI peripheral will think it is being addressed and may drive the CIPO line, causing bus contention. Always place a 10kΩ pull-up resistor between VCC (3.3V) and the CS pin of every SPI slave.
The Classic Failures: Clock Modes, MISO Contention, and Debugging
Every protocol has its signature failure mode. I2C famously suffers from address clashes (two sensors hardcoded to 0x68). UART suffers from baud rate mismatches (115200 vs 9600 resulting in garbage characters). SPI is immune to both, but it introduces two equally frustrating physical layer failures:
1. CPOL and CPHA (Clock Mode) Mismatch
SPI defines four clock modes based on Clock Polarity (CPOL - idle state of the clock) and Clock Phase (CPHA - whether data is sampled on the leading or trailing edge). If your master is configured for Mode 0, but your sensor expects Mode 3, you will read shifted, corrupted, or entirely zeroed data.
| SPI Mode | CPOL (Idle Clock) | CPHA (Sample Edge) | Common Devices |
|---|---|---|---|
| Mode 0 | 0 (LOW) | 0 (Leading / Rising) | nRF24L01+, SD Cards, ADXL345 (Alt) |
| Mode 1 | 0 (LOW) | 1 (Trailing / Falling) | MAX31856, some TFT displays |
| Mode 2 | 1 (HIGH) | 0 (Leading / Falling) | Rare in modern hobbyist ICs |
| Mode 3 | 1 (HIGH) | 1 (Trailing / Rising) | ADXL345 (Default), W25Q Flash |
2. MISO Bus Contention
Because CIPO/MISO is a shared wire connecting all slaves, only one device is allowed to drive it at a time. When a device's CS pin is HIGH, its internal CIPO buffer must enter a high-impedance (tri-state) mode. If you buy a cheap, unbranded breakout board with poor level-shifter design, the CIPO line might not tri-state properly. The result? Two devices driving the bus simultaneously, shorting VCC to GND internally, and corrupting all reads.
How to Sniff and Debug the Bus:
When your SPI.transfer() returns 0xFF or 0x00 consistently, stop guessing and look at the physical layer. You need a logic analyzer. A basic 8-channel USB logic analyzer (like a Saleae Logic 8 or a DSLogic Plus) running at a 24 MHz sample rate is mandatory for debugging 10 MHz SPI. Connect SCK, COPI, CIPO, and CS to the analyzer. Use the software's SPI protocol decoder to verify:
- Is the CS line actually pulling LOW before the clock starts?
- Is the clock idle state matching your CPOL setting?
- Is the slave actually driving the CIPO line, or is it floating?
For deeper electrical issues (like ringing on long wires), you must step up to a digital storage oscilloscope (DSO) to check signal integrity and edge rise times. See the SparkFun SPI Tutorial for excellent visual breakdowns of these timing diagrams.
Minimal Working Exchange: ESP32 to ADXL345 Accelerometer
Let's build a minimal, bulletproof SPI exchange. We will interface an ESP32-WROOM-32 with an Analog Devices ADXL345 3-axis accelerometer. The ADXL345 operates at 3.3V, matching the ESP32 natively, and defaults to SPI Mode 3.
Wiring Table:
| ESP32 GPIO | ADXL345 Pin | Notes |
|---|---|---|
| GPIO 18 (SCK) | SCL | Clock |
| GPIO 23 (COPI) | SDI | Master Out, Slave In |
| GPIO 19 (CIPO) | SDO | Master In, Slave Out |
| GPIO 5 (CS) | CS | Add 10k pull-up to 3.3V! |
| 3V3 | VCC | Power |
| GND | GND | Common Ground |
The Code:
We will read the DEVID register (Address 0x00). In SPI, to read a register on the ADXL345, we must set bit 7 (the read bit) HIGH. Therefore, we transmit 0x80 | 0x00. The expected return byte is 0xE5.
#include <SPI.h>
#define CS_PIN 5
#define DEVID_REG 0x00
#define EXPECTED_ID 0xE5
void setup() {
Serial.begin(115200);
delay(1000);
// Initialize CS pin HIGH to prevent floating during SPI.begin()
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH);
// Initialize default VSPI bus on ESP32
SPI.begin();
// ADXL345 SPI Max Clock is 5MHz, Mode 3, MSB First
SPISettings adxlSettings(5000000, MSBFIRST, SPI_MODE3);
Serial.println("Attempting to read ADXL345 DEVID register...");
SPI.beginTransaction(adxlSettings);
digitalWrite(CS_PIN, LOW);
// Send Read Command (Bit 7 = 1) + Register Address (0x00)
SPI.transfer(0x80 | DEVID_REG);
// Clock out the response byte
byte deviceId = SPI.transfer(0x00);
digitalWrite(CS_PIN, HIGH);
SPI.endTransaction();
if (deviceId == EXPECTED_ID) {
Serial.print("Success! Device ID: 0x");
Serial.println(deviceId, HEX);
} else {
Serial.print("Failure. Read: 0x");
Serial.println(deviceId, HEX);
Serial.println("Check wiring, pull-ups, and SPI_MODE3 setting.");
}
}
void loop() {
// Keep loop empty for this primer
}
Verification & Next Steps:
Upload this to your ESP32. If the Serial Monitor prints Success! Device ID: 0xE5, your physical layer is solid. You have correctly managed the push-pull bus, the CS pull-up, and the CPOL/CPHA clock mode. From here, you can proceed to configure the data rate registers and read the X, Y, and Z acceleration bytes, applying the same multi-byte read technique (setting bit 6 HIGH for auto-increment) detailed in the Analog Devices datasheet.






