If you are wiring up high-speed sensors, SD cards, or TFT displays, you will inevitably run into the Serial Peripheral Interface (SPI). At its core, SPI relies on four primary SPI pins: MOSI (Master Out Slave In), MISO (Master In Slave Out), SCK (Clock), and CS/SS (Chip Select). Unlike I2C, which shares a bidirectional data line, SPI uses separate lines for sending and receiving, allowing for full-duplex communication and significantly higher clock speeds.
But knowing the acronyms is only 10% of the battle. The other 90% is understanding bus capacitance, chip select contention, and clock polarity. Below is a bench-tested guide to mapping, wiring, and debugging SPI pins on modern microcontrollers like the ESP32 and Arduino.
SPI vs I2C vs UART: Bus Mechanics at a Glance
Before routing traces or plugging in jumper wires, you need to know which protocol actually fits your physical constraints. SPI is the undisputed king of short-distance, high-throughput chip-to-chip communication, but it scales poorly when you add dozens of devices. Here is how the big three serial protocols stack up on the bench.
| Feature | SPI | I2C | UART |
|---|---|---|---|
| Wires Required | 4 (SCK, MOSI, MISO, CS) + extra CS per device | 2 (SDA, SCL) shared by all | 2 (TX, RX) per pair |
| Practical Top Speed | 10 MHz - 50 MHz (depends on capacitance) | 100 kHz (Std), 400 kHz (Fast), 3.4 MHz (HS) | 115,200 baud (typ), up to 3 Mbps |
| Addressing Method | Hardware Chip Select (CS) lines (no software address) | 7-bit or 10-bit software I2C address | None (Point-to-Point) |
| Max Reliable Distance | ~30 cm (1 ft) on breadboards; longer with RS-422 buffers | ~1 meter (with proper pull-ups and capacitance management) | ~15 meters (RS-232) or 1.2 km (RS-485) |
| Duplex | Full-Duplex (simultaneous TX/RX) | Half-Duplex | Full-Duplex |
The Verdict: Choose SPI when you need raw speed (e.g., streaming raw ADC data or driving a 320x240 TFT display) and only have 1 to 4 peripherals. Choose I2C when you have 10+ low-speed sensors (like BME280s) and want to save GPIO pins. Choose UART for long-distance comms, GPS modules, or talking to a PC.
Physical Wiring and Pin Mapping (ESP32 & Arduino)
A common trap for beginners is assuming SPI pins are fixed. On an ATmega328P (Arduino Uno/Nano), they are hardcoded to specific ports. On the ESP32, the GPIO matrix allows you to route SPI to almost any pin, but using the default hardware SPI pins yields vastly superior performance and stability.
Default SPI Pinouts
| SPI Pin Function | Arduino Uno / Nano (ATmega328P) | ESP32 (VSPI - Default) | ESP32 (HSPI - Secondary) |
|---|---|---|---|
| MOSI | GPIO 11 | GPIO 23 | GPIO 13 |
| MISO | GPIO 12 | GPIO 19 | GPIO 12 * |
| SCK | GPIO 13 | GPIO 18 | GPIO 14 |
| CS / SS | GPIO 10 | GPIO 5 | GPIO 15 |
Pull-Up and Pull-Down Requirements
Unlike I2C, SPI does not strictly require bus-wide pull-up resistors on the data and clock lines. However, the Chip Select (CS) line is a different story. CS is active-LOW. If the microcontroller resets or boots, its GPIOs float. During this floating state, a peripheral might think it has been selected and drive the MISO line, causing bus contention or a short circuit if another device is also transmitting. Always place a 10kΩ pull-up resistor between the CS pin and VCC (3.3V or 5V) for every SPI slave device.
Minimal Working Exchange: ADXL345 Accelerometer
Let us look at a concrete, minimal working exchange. We will wire an ADXL345 SPI accelerometer to an ESP32 and read its WHO_AM_I register to verify communication. The ADXL345 expects SPI Mode 3 (CPOL=1, CPHA=1) and MSB-first bit ordering.
Wiring Diagram
| ADXL345 Pin | ESP32 VSPI Pin | Notes |
|---|---|---|
| VCC | 3V3 | Do not use 5V; the ADXL345 logic is 3.3V. |
| GND | GND | Common ground is mandatory. |
| CS | GPIO 5 | Add 10kΩ pull-up to 3V3. |
| SDO (MISO) | GPIO 19 | Serial Data Out from sensor. |
| SDA (MOSI) | GPIO 23 | Serial Data In to sensor. |
| SCL (SCK) | GPIO 18 | Clock signal. |
Arduino IDE Code
This code initializes the bus, asserts the CS pin, and reads the DEVID register (address 0x00). A successful read returns 0xE5.
#include <SPI.h>
// ESP32 VSPI Chip Select Pin
const int CS_PIN = 5;
// ADXL345 Register Addresses
#define REG_DEVID 0x00
#define EXPECTED_DEVID 0xE5
void setup() {
Serial.begin(115200);
delay(1000); // Allow serial monitor to connect
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect device immediately
// Initialize SPI at 1MHz, MSB First, Mode 3
// ADXL345 supports up to 5MHz, but 1MHz is safer for breadboards
SPI.begin();
uint8_t devID = readRegister(REG_DEVID);
if (devID == EXPECTED_DEVID) {
Serial.println("SPI Exchange Successful: ADXL345 Found!");
} else {
Serial.print("SPI Failure. Expected 0xE5, got: 0x");
Serial.println(devID, HEX);
Serial.println("Check wiring, breadboard capacitance, and SPI Mode.");
}
}
void loop() {
// Main loop left empty for this primer
}
uint8_t readRegister(uint8_t reg) {
// For ADXL345, reading requires setting the MSB of the register byte to 1 (0x80)
uint8_t readCommand = reg | 0x80;
uint8_t result = 0;
// Configure bus settings for this specific transaction
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE3));
digitalWrite(CS_PIN, LOW); // Assert Chip Select
SPI.transfer(readCommand); // Send register address
result = SPI.transfer(0x00); // Clock out the data
digitalWrite(CS_PIN, HIGH); // Deassert Chip Select
SPI.endTransaction();
return result;
}
Debugging the Bus: Sniffing and Classic Failures
When your SPI bus returns 0x00 or 0xFF, do not just start changing random wires. SPI failures are highly predictable. Here is how to diagnose the classic failure modes and how to sniff the bus when the code fails you.
The Classic Failures
- Baud and Mode Mismatch (CPOL/CPHA): SPI has four clock modes based on Clock Polarity (CPOL) and Clock Phase (CPHA). If your sensor expects Mode 3 (clock idles HIGH, data sampled on trailing edge) and your MCU defaults to Mode 0 (clock idles LOW), you will read garbage. Always check the peripheral datasheet's timing diagram. The Espressif SPI Master API docs explicitly detail how the ESP32 handles these clock edges.
- Missing CS Pull-Up (Bus Contention): If you have two SPI devices on the same MISO line and one lacks a pull-up on its CS pin, it may drive MISO low during MCU boot. This causes a direct short when the other device tries to transmit, often resulting in a brownout reset or a fried GPIO.
- Breadboard Capacitance Killing the Clock: SPI speed is limited by parasitic capacitance. At 20 MHz, the square wave clock signal on a standard solderless breadboard will degrade into a triangular wave due to the ~5pF capacitance per contact. If your code works at 1 MHz but fails at 10 MHz, you have hit the physical limit of your wiring. Solder the connections or drop the baud rate.
- The 'Address Clash' Equivalent: SPI does not use software addresses, so you cannot have an I2C-style address clash. However, if you wire two CS lines to the same GPIO pin by mistake, both devices will try to drive MISO simultaneously, corrupting the data.
How to Sniff and Debug the Bus
When serial prints are not enough, you need to look at the physical layer. The standard tool for this is a USB logic analyzer. You do not need a $400 Saleae Logic Pro 8; a $15 clone based on the Cypress CY7C68013A chip running at 24 MHz is more than enough for SPI debugging.
Download PulseView (Sigrok), an open-source logic analyzer GUI. Wire the analyzer's ground to your circuit ground, and clip probes onto SCK, MOSI, MISO, and CS. Set the sample rate to at least 4 times your SPI clock speed (e.g., if SCK is 1 MHz, sample at 4 MHz or higher) to satisfy the Nyquist theorem and capture clean edges.
Add the SPI protocol decoder in PulseView. It will parse the raw high/low transitions into hex bytes. If you see the MCU sending the correct register command on MOSI, but MISO stays flatlined at HIGH, your peripheral is either unpowered, wired to the wrong MISO pin, or the CS line is not pulling low enough to trigger the slave's internal logic.
Mastering SPI pins is less about memorizing acronyms and more about respecting the physical layer. Manage your CS pull-ups, verify your clock modes, and keep your high-speed traces short, and SPI will reliably move megabytes of data across your workbench without dropping a single bit.






