The Serial Peripheral Interface (SPI) relies on four primary wires to achieve high-speed, full-duplex communication between a microcontroller and its peripherals: SCK (Clock), MOSI (Master Out Slave In), MISO (Master In Slave Out), and CS (Chip Select). Unlike asynchronous protocols, SPI is clocked and push-pull, meaning it can push data rates from 10 MHz up to 80 MHz on short PCB traces, but it demands strict physical wiring discipline to avoid signal degradation.
The SPI Bus Mechanics & Protocol Comparison
Before routing your SPI wires, you need to know if SPI is actually the right tool for your topology. Makers often default to SPI for displays and SD cards, but I2C or UART might be better for sensor networks spread across a chassis. Here is how the physical layers compare in real-world bench conditions.
| Criteria | SPI (Serial Peripheral Interface) | I2C (Inter-Integrated Circuit) | UART (Universal Asynchronous Receiver-Transmitter) |
|---|---|---|---|
| Core Wires | 4 shared + 1 CS per device | 2 shared (SDA, SCL) | 2 point-to-point (TX, RX) |
| Max Practical Speed | 10 MHz (breadboard) / 80 MHz (PCB) | 100 kHz / 400 kHz / 1 MHz (Fast+) | 115,200 bps / 921,600 bps |
| Addressing | None (Hardware CS lines) | 7-bit or 10-bit software addresses | None (Point-to-point only) |
| Max Distance | < 30 cm (highly dependent on capacitance) | ~1 meter (at 100 kHz with proper pull-ups) | ~15 meters (RS-232) / 1200m (RS-485) |
| Topology | Master-Slave (Multi-slave via CS fanout) | Multi-Master / Multi-Slave bus | Point-to-Point |
The Verdict: Choose SPI when you need raw bandwidth (TFT displays, external flash, high-speed ADCs) and your devices are clustered on the same PCB or a single breadboard. Choose I2C for low-speed sensor arrays where you want to minimize wire count. Choose UART (or RS-485) when you need to cross physical distances between enclosures.
| Wire Name | Alt Names | Direction (from Master) | Function & Idle State |
|---|---|---|---|
| SCK | SCLK, CLK | Output | Clock signal. Idle state depends on CPOL (Mode 0/1 = Low, Mode 2/3 = High). |
| MOSI | SDO, DIN, TX | Output | Master sends data to Slave. Idle state is typically Low. |
| MISO | SDI, DOUT, RX | Input | Slave sends data to Master. High-impedance (tri-state) when CS is HIGH. |
| CS | SS, CE, NCS | Output | Activates specific slave. Active LOW. Must be pulled HIGH to deselect. |
Physical Wiring, Routing, and the 'Missing Pull-Up' Myth
When makers transition from I2C to SPI, they often look for the classic I2C failures: address clashes and missing pull-up resistors. SPI doesn't use either. Because SPI outputs are push-pull (they actively drive both HIGH and LOW), you do not need pull-up resistors on SCK, MOSI, or MISO. Adding them will only increase rise times and limit your maximum clock speed.
The actual physical layer failures in SPI look different:
- CS Wire Spaghetti (The 'Address Clash' Equivalent): Since SPI lacks software addressing, every slave needs its own dedicated CS wire back to the master. On an ESP32 with 30+ available GPIOs, this is manageable. On an ATtiny85, you run out of pins immediately. If you accidentally leave two CS lines LOW simultaneously, both slaves will drive the MISO wire, causing a short circuit and data corruption.
- Parasitic Capacitance & Trace Length: SPI wires act as antennas and capacitors. On a breadboard, long Dupont jumper wires add capacitance that rounds off the sharp edges of your 10 MHz clock signal. If your SCK wire is 20 cm long but your MISO wire is 5 cm long, the signals will arrive at the receiver out of phase. Rule of thumb: Keep all four SPI wires the exact same physical length, and keep them under 15 cm for breadboard prototyping.
- Missing MISO Tri-State: Some cheap, off-brand sensor modules fail to implement the tri-state buffer on the MISO line. If you put multiple of these modules on the same SPI bus, they will fight for the MISO line even when their CS is HIGH. The fix is to add a 74LVC125A tri-state buffer IC between the module's MISO and the master's MISO, gated by the CS line.
On the classic ESP32-WROOM-32, the default VSPI hardware pins are SCK (GPIO 18), MISO (GPIO 19), MOSI (GPIO 23), and CS (GPIO 5). While you can route SPI to almost any pin via the GPIO matrix, using these default hardware pins bypasses the matrix and yields cleaner signal timing at speeds above 20 MHz.
Minimal Working Exchange & Pin Mapping
Let's wire an ESP32 to a standard RC522 RFID module. This module operates at 3.3V and maxes out around 10 MHz, making it a perfect baseline test for SPI bus integrity.
| RC522 Pin | ESP32-WROOM-32 Pin | Wire Color (Suggested) | Notes |
|---|---|---|---|
| 3V3 | 3V3 | Red | Do NOT use 5V. The RC522 logic is strictly 3.3V. |
| GND | GND | Black | Common ground is mandatory. |
| RST | GPIO 22 | Blue | Active LOW reset. Pulled HIGH in software. |
| SCK | GPIO 18 (VSPI SCK) | Yellow | Clock signal. |
| MOSI | GPIO 23 (VSPI MOSI) | Green | Master to Slave data. |
| MISO | GPIO 19 (VSPI MISO) | Orange | Slave to Master data. |
| SDA (CS) | GPIO 5 (VSPI CS) | Purple | Chip select. Active LOW. |
Here is the minimal Arduino-framework code to initialize the bus, assert the CS line, and perform a raw byte exchange. This bypasses heavy libraries to show the bare-metal SPI transaction.
#include <SPI.h>
// ESP32 VSPI Hardware Pins
const int CS_PIN = 5;
const int RST_PIN = 22;
// RC522 Command: Read Version Register (0x37)
const byte READ_VERSION_CMD = 0x37;
void setup() {
Serial.begin(115200);
pinMode(CS_PIN, OUTPUT);
pinMode(RST_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect slave initially
digitalWrite(RST_PIN, HIGH); // Take RC522 out of reset
// Initialize SPI at 4 MHz, Mode 0 (CPOL=0, CPHA=0)
SPI.begin(18, 19, 23, 5); // SCK, MISO, MOSI, CS
SPI.setClockDivider(SPI_CLOCK_DIV4);
delay(100);
}
void loop() {
digitalWrite(CS_PIN, LOW); // Assert Chip Select
// Send register address (with MSB cleared for read)
SPI.transfer(READ_VERSION_CMD);
// Send dummy byte to clock out the response
byte chipVersion = SPI.transfer(0x00);
digitalWrite(CS_PIN, HIGH); // De-assert Chip Select
Serial.print('RC522 Chip Version: 0x');
Serial.println(chipVersion, HEX);
// Expected valid response: 0x91 or 0x92
delay(2000);
}
Sniffing, Debugging, and Fixing Classic SPI Failures
When your SPI bus returns garbage data or 0xFF, do not start swapping jumper wires blindly. SPI failures are almost always logical or timing-related, not physical breaks. According to Saleae's SPI protocol analysis guide, the vast majority of bench-level SPI bugs stem from clock phase misalignment.
The Debugging Decision Tree
- Symptom: MISO reads 0xFF or 0x00 consistently.
Cause: CS is never going LOW, or the MISO wire is broken.
Fix: Put your multimeter in continuity mode. Probe the master CS pin and the slave CS pin. Ensure your code explicitly drives the pin LOW before callingSPI.transfer(). If using an ESP32 hardware CS pin, ensure you aren't conflicting with the internal flash SPI bus (never use GPIO 6-11 for peripheral CS on the classic ESP32). - Symptom: Data is shifted by one bit, or returns random garbage.
Cause: Baud/Clock mismatch, specifically CPOL (Clock Polarity) and CPHA (Clock Phase).
Fix: SPI has four modes (0, 1, 2, 3). Mode 0 (Clock idles LOW, data sampled on rising edge) is the most common. If your peripheral requires Mode 3, you must configure it in software. In Arduino, useSPI.setDataMode(SPI_MODE3). In ESP-IDF, set thespics_io_numand clock flags correctly in the ESP32 SPI Master API. - Symptom: Bus works at 1 MHz but fails at 10 MHz.
Cause: Signal integrity degradation due to parasitic capacitance on long SPI wires.
Fix: You cannot fix this in software. Shorten the wires, move to a custom PCB, or add a 33-ohm series termination resistor on the SCK and MOSI lines near the master to dampen high-frequency ringing.
How to Sniff the Bus
A multimeter is useless for debugging SPI timing. You need a logic analyzer. A basic 8-channel USB logic analyzer (like the $15 Saleae clones based on the Cypress CY7C68013A chip) running PulseView / Sigrok is mandatory for embedded work. Connect the four SPI wires to channels 0-3, set the sample rate to at least 4x your SPI clock speed (e.g., 40 MS/s for a 10 MHz bus), and use the built-in SPI decoder. If the decoder spits out 'MISO data errors' or fails to frame the bytes, your CPOL/CPHA settings in the decoder don't match the hardware. Toggle the decoder settings until the hex output matches your expected register map.
Mastering SPI wires is less about memorizing pinouts and more about respecting the physical limits of high-speed push-pull digital signals. Keep your traces short, match your clock modes, and always verify your chip select logic with a scope or analyzer before blaming the library.






