SPI (Serial Peripheral Interface) is a synchronous, full-duplex, four-wire communication protocol used to move data between a microcontroller and peripherals at speeds up to 50 MHz over short distances. Unlike I2C, which relies on open-drain lines and pull-up resistors, SPI uses a push-pull physical layer. This means the master (controller) and slave (peripheral) actively drive the data lines high and low, allowing for much faster edge transitions and higher bandwidth, but strictly limiting the bus length due to trace capacitance and signal reflection.
The Physical Layer and Protocol Fit Matrix
Before wiring up a breakout board, you need to know if SPI is actually the right tool for your topology. SPI requires a dedicated Chip Select (CS) line for every single peripheral. If you are daisy-chaining 20 sensors, the CS pin explosion on your microcontroller will make SPI a nightmare. Use SPI when you need raw speed and have a low device count; use I2C for high device counts at moderate speeds; use UART for point-to-point asynchronous links.
| Protocol | Wires (Min) | Max Speed (Typical) | Addressing Method | Max Distance | Best Use Case |
|---|---|---|---|---|---|
| SPI | 4 (SCLK, MOSI, MISO, CS) | 10 MHz - 50 MHz+ | Hardware CS lines (No software addressing) | < 30 cm (high speed), < 1 m (low speed) | High-speed ADCs, SD cards, TFT displays, external flash |
| I2C | 2 (SDA, SCL) | 100 kHz - 3.4 MHz | 7-bit or 10-bit software address | < 1 m (highly dependent on pull-up sizing) | Low-speed sensors, OLEDs, EEPROMs, high device counts |
| UART | 2 (TX, RX) | 115.2 kbps - 2 Mbps | None (Point-to-point) | < 15 m (at 9600 bps), < 1 m (at 1 Mbps) | GPS modules, cellular modems, PC serial consoles |
| CAN | 2 (CANH, CANL) | 1 Mbps (Classic) | Message ID arbitration | Up to 40 m (at 1 Mbps) | Automotive, industrial robotics, noisy environments |
Physical Wiring and Pull-Up Requirements
Because SPI data (MOSI/MISO) and clock (SCLK) lines are push-pull, you do not need pull-up resistors on them. Adding pull-ups to SPI data lines will actually slow down the rise times and cause data corruption at higher baud rates.
However, the Chip Select (CS) line is a different story. CS is typically active-low. If your microcontroller reboots or its GPIO pins float during the boot sequence (a common issue with the ESP32-WROOM-32 before the bootloader finishes), a floating CS line can accidentally wake up a peripheral, causing it to drive the MISO line and collide with other SPI devices. Always place a 10kΩ pull-up resistor on every CS line to VCC to keep peripherals dormant during MCU boot.
Wiring the MCP3008 ADC and Minimal Code Exchange
To demonstrate a complete, working SPI exchange, we will wire a Microchip MCP3008 10-bit ADC to an ESP32. The MCP3008 is a classic SPI device that requires a precise 3-byte transaction to read an analog channel.
| MCP3008 Pin | Function | ESP32-WROOM-32 Pin | Notes |
|---|---|---|---|
| 16 | VDD | 3V3 | Power supply (2.7V to 5.5V) |
| 15 | VREF | 3V3 | Reference voltage for ADC scaling |
| 14 | AGND | GND | Analog Ground |
| 9 | DGND | GND | Digital Ground (tie to AGND) |
| 13 | CLK (SCLK) | GPIO 18 (SCK) | SPI Clock |
| 12 | DOUT (MISO) | GPIO 19 (MISO) | Master In, Slave Out |
| 11 | DIN (MOSI) | GPIO 23 (MOSI) | Master Out, Slave In |
| 10 | CS/SHDN | GPIO 5 (SS) | Chip Select (Active Low) + 10k pull-up |
Minimal Working Exchange (Arduino/ESP32 IDE)
This code uses SPI.beginTransaction() to lock the bus at 1 MHz, Mode 0. This is critical if you have multiple SPI devices (like an SD card and a display) sharing the same MOSI/MISO/SCLK lines but using different CS pins and requiring different clock speeds.
#include <SPI.h>
const int CS_PIN = 5;
SPISettings mcpSettings(1000000, MSBFIRST, SPI_MODE0); // 1MHz, Mode 0
void setup() {
Serial.begin(115200);
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect chip immediately
SPI.begin();
}
void loop() {
int adcValue = readMCP3008(0); // Read Channel 0
float voltage = adcValue * (3.3 / 1023.0);
Serial.printf("CH0 Raw: %d | Voltage: %.2f V\n", adcValue, voltage);
delay(500);
}
int readMCP3008(byte channel) {
byte commandBits = 0b00000001; // Start bit
byte configBits = (channel << 4) | 0b10000000; // Single-ended, channel select
SPI.beginTransaction(mcpSettings);
digitalWrite(CS_PIN, LOW);
SPI.transfer(commandBits);
byte msb = SPI.transfer(configBits);
byte lsb = SPI.transfer(0x00);
digitalWrite(CS_PIN, HIGH);
SPI.endTransaction();
// Combine the 10-bit result (2 bits from msb, 8 bits from lsb)
return ((msb & 0x03) << 8) | lsb;
}
Clock Modes, Classic Failures, and Sniffing the Bus
The most common reason an SPI bus "doesn't work" out of the box is a clock mode mismatch. SPI defines four modes based on Clock Polarity (CPOL) and Clock Phase (CPHA). If your master is configured for Mode 0, but the peripheral datasheet specifies Mode 3, the master will sample the data line on the wrong clock edge, resulting in garbage data or all zeros.
| Mode | CPOL | CPHA | Clock Idle State | Data Sampled On |
|---|---|---|---|---|
| 0 | 0 | 0 | Low | Rising Edge |
| 1 | 0 | 1 | Low | Falling Edge |
| 2 | 1 | 0 | High | Falling Edge |
| 3 | 1 | 1 | High | Rising Edge |
The Classic Failures
- Missing CS Pull-Up (Ghosting): You read random noise from an SD card even when you aren't addressing it. This happens because another SPI device's CS line floated low during a reset, causing it to drive the MISO line and corrupt the bus. Fix: Add 10kΩ pull-ups to all CS lines.
- Baud Rate Too High for Ribbon Cables: You get perfect data on a short breadboard jumper, but garbage data when you use a 20cm flat ribbon cable. Ribbon cables have high parasitic capacitance between adjacent wires, which rounds off the sharp square-wave edges of the SCLK at high frequencies. Fix: Drop the SPI clock from 10 MHz to 1 MHz, or use twisted-pair wiring with a dedicated ground return for SCLK.
- MISO/MOSI Reversal: Unlike I2C (SDA/SCL), SPI data lines are directional. MISO on the master must connect to MISO on the slave. If you wire MOSI to MOSI, the master and slave will drive the same line simultaneously, causing a short circuit that can overheat the GPIO drivers.
How to Sniff and Debug the Bus
When SPI.transfer() returns 0xFF or 0x00 consistently, stop guessing and look at the physical signals. You need a logic analyzer. A basic 8-channel USB logic analyzer (compatible with Saleae Logic 2 software or the open-source Sigrok/PulseView) costs about $15 and is mandatory for SPI debugging.
Clip the analyzer to SCLK, MOSI, MISO, and CS. Set the sample rate to at least 4x your SPI clock speed (e.g., 4 MS/s for a 1 MHz clock). Trigger on the falling edge of the CS line. Decode the SPI packet and verify:
- Is the CS line actually going low before the clock starts?
- Does the first byte sent on MOSI match your expected command register?
- Is the slave responding on MISO, or is the line floating (indicating the slave isn't powered or the chip is dead)?
- Are the clock edges clean, or is there severe ringing on the SCLK line?
Summary
SPI communication offers unmatched speed for short-distance embedded links, provided you respect its physical limitations. Keep your traces short, manage your Chip Select lines with pull-ups, verify the CPOL/CPHA mode in the peripheral datasheet, and always use SPI.beginTransaction() to protect shared bus configurations. When the data sheet and the code disagree, trust the logic analyzer.






