SPI (Serial Peripheral Interface) is a synchronous, full-duplex serial protocol that uses four wires to move data between a master microcontroller and slave peripherals. If you need to push megabytes of data to an SD card, drive a TFT display, or read a high-speed IMU at 10+ MHz, SPI serial is the physical layer you want. Unlike asynchronous protocols, SPI relies on a shared clock line, eliminating baud-rate drift and allowing for massive throughput over short distances.

The SPI Serial Bus: Mechanics and Physical Layer

Before writing a single line of code, you must understand the physical layer. SPI is a push-pull CMOS architecture. This means the master and slave actively drive the data lines high and low. Unlike I2C, SPI does not require pull-up resistors on the MOSI, MISO, or SCK lines. Adding pull-ups to SPI data lines will only increase capacitive load and ruin your signal integrity at high clock speeds.

SPI Bus Mechanics and Specifications
ParameterSPI Serial SpecificationPractical Limit (Breadboard/Jumper)
Wires Required4 shared (SCK, MOSI, MISO) + 1 CS per deviceScales poorly past 3-4 slaves due to CS routing
Clock SpeedUp to 100+ MHz on custom PCBs1 MHz to 10 MHz max (parasitic capacitance limits higher)
AddressingNone (Hardware Chip Select / CS lines)Requires one dedicated GPIO per slave device
Distance< 1 meter (single-ended)< 30 cm recommended without differential line drivers
DuplexFull-Duplex (Simultaneous TX/RX)Master sends on MOSI while reading MISO on the same clock edge
The CS Pull-Up Exception: While data and clock lines don't need pull-ups, the Chip Select (CS) line on the slave device absolutely needs a 10kΩ pull-up resistor to VCC. If the master reboots and its GPIO floats, a slave without a CS pull-up will wake up, think it's selected, and drive the MISO line. This will cause a bus contention and potentially fry the output drivers if another slave is also trying to talk.

Protocol Selection: SPI vs I2C vs UART

Choosing the right protocol comes down to a strict evaluation of distance, speed, and device count. Use this decision path to terminate your architecture debate and pick a protocol.

  • IF you need >1 Mbps throughput, full-duplex simultaneous transfer, and the target is <30cm away → Pick SPI Serial. (e.g., SD cards, TFT screens, ADXL345 IMUs).
  • IF you need to connect 5+ low-bandwidth sensors, want to save GPIO pins, and distance is <1m → Pick I2C. (e.g., BME280 environmental sensors, OLED text displays).
  • IF you are communicating point-to-point over >5 meters, or talking to a PC/serial console → Pick UART (or RS-485 for noise immunity).
Communication Protocol Comparison Matrix
FeatureSPI SerialI2CUART
TopologyMaster-Slave (Ring or Star)Multi-Master BusPoint-to-Point
Wires (Min)4 (Shared) + CS2 (SDA, SCL)2 (TX, RX)
Max Speed (Typical)10 - 50 MHz100 kHz - 3.4 MHz115.2 kbps - 1 Mbps
Flow ControlHardware (CS line)Hardware (Clock stretching)Software (XON/XOFF) or Hardware (RTS/CTS)

Wiring an ESP32 to an SPI Sensor (ADXL345)

Let’s wire an Analog Devices ADXL345 accelerometer to an ESP32-WROOM-32 using the default VSPI hardware bus. The ADXL345 is a classic SPI device that strictly requires SPI Mode 3 (CPOL=1, CPHA=1).

ESP32 VSPI to ADXL345 Wiring Map
ESP32 Pin (VSPI)ADXL345 PinNotes
GPIO 18 (SCK)SCLClock signal. Keep wire short.
GPIO 23 (MOSI)SDA (SDI)Master Out, Slave In.
GPIO 19 (MISO)SDO (ALT ADDRESS)Master In, Slave Out. Tie to GND via 10k if not used as MISO.
GPIO 5 (CS)CSAdd 10k pull-up to 3.3V on this line.
3.3VVCCDo not use 5V; ADXL345 logic is 3.3V.
GNDGNDCommon ground required.

Below is the minimal working exchange to initialize the sensor and read the WHO_AM_I register (0x00). If the bus is wired correctly and Mode 3 is set, it will return 0xE5.

#include <SPI.h>

// ESP32 VSPI default pins
#define SCK_PIN  18
#define MISO_PIN 19
#define MOSI_PIN 23
#define CS_PIN   5

// ADXL345 Registers
#define REG_DEVID 0x00
#define DEVID_EXPECTED 0xE5

SPIClass vspi(VSPI);

void setup() {
  Serial.begin(115200);
  while(!Serial) { delay(10); }

  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH); // Deselect slave

  // Initialize VSPI at 1MHz, MSB first, SPI Mode 3
  vspi.begin(SCK_PIN, MISO_PIN, MOSI_PIN, CS_PIN);
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE3));

  // Read WHO_AM_I register
  digitalWrite(CS_PIN, LOW);
  vspi.transfer(REG_DEVID | 0x80); // 0x80 sets the read bit
  uint8_t devID = vspi.transfer(0x00); // Clock out the data
  digitalWrite(CS_PIN, HIGH);

  if (devID == DEVID_EXPECTED) {
    Serial.println("ADXL345 SPI serial link verified. ID: 0xE5");
  } else {
    Serial.print("SPI Failure. Expected 0xE5, got: 0x");
    Serial.println(devID, HEX);
    Serial.println("Check wiring, CS pull-up, and SPI_MODE3 setting.");
  }
}

void loop() {
  // Main sensor reading loop goes here
  delay(1000);
}

Classic SPI Failures and How to Sniff the Bus

When your SPI serial bus returns garbage data or hangs the microcontroller, the issue is almost always physical layer or timing. Here are the classic failure modes and how to debug them.

  1. CPOL/CPHA Mismatch (The Mode Error): SPI has four modes (0, 1, 2, 3) defining clock polarity and phase. If your sensor expects Mode 3 but your library defaults to Mode 0, the master will sample the MISO line on the wrong clock edge, resulting in shifted or inverted data. Fix: Check the sensor datasheet and explicitly define the SPISettings mode.
  2. Baud Rate vs. Parasitic Capacitance: You set the clock to 20 MHz, but you are using 10cm Dupont jumper wires on a solderless breadboard. The parasitic capacitance of the breadboard rounds off the square clock waves into sine waves. The slave fails to register clock edges. Fix: Drop the SPI clock to 1 MHz or 4 MHz for breadboard prototyping. Only use 20+ MHz on custom PCBs with controlled impedance routing.
  3. Missing CS Pull-Up (Bus Contention): You have two SPI devices sharing the MISO line. Device A is selected, but Device B’s CS line is floating low due to a missing pull-up resistor. Device B drives the MISO line simultaneously, causing a short circuit between the output drivers. Fix: Add 10kΩ pull-ups to VCC on every slave CS pin.
How to Sniff the Bus: Do not guess with a multimeter. SPI moves too fast. Use a logic analyzer (a $15 24MHz 8-channel clone works fine for < 5MHz buses). Connect the ground clip, then clip probes to SCK, MOSI, MISO, and CS. Use open-source PulseView / Sigrok software. Set the trigger to the falling edge of the CS line. Decode the SPI protocol in the software and verify that the MOSI command byte matches what your C++ code intended to send.

The Verdict: Default Recommendations

Stop debating the protocol for every new sensor. Apply this hard default rule for your embedded architecture:

Default to SPI serial for any peripheral that requires high throughput, rapid polling, or large memory transfers. This includes TFT LCDs, SD/MMC cards, external SPI Flash (like the W25Q128), and high-speed 6-axis IMUs. The 4-wire overhead is a small price to pay for the 10 MHz+ full-duplex bandwidth and the elimination of I2C address-clash headaches.

Default to I2C only for low-bandwidth, “set-and-forget” environmental sensors (temperature, humidity, ambient light) where you need to daisy-chain three or four devices on the same two bus wires and pin count is your primary constraint. If you need speed, wire it to the SPI bus.