An SPI controller (historically referred to as the "master") is the microcontroller node that generates the clock signal and initiates all data transfers on a Serial Peripheral Interface bus. Unlike I2C, which relies on a shared address space, an SPI controller manages individual Chip Select (CS) lines for every peripheral, enabling full-duplex, high-speed data transfers—often between 10 MHz and 80 MHz—over short distances. If you are interfacing high-throughput sensors, TFT displays, or flash memory chips to an ESP32 or Arduino, SPI is usually your only viable option.

The Physical Layer: Wiring an SPI Controller to Peripherals

Before writing a single line of code, you must understand the physical layer. SPI is not a standardized protocol in the same way I2C is; it is a de facto standard with variations in clock polarity and phase. However, the physical bus mechanics remain consistent across almost all silicon.

SPI Bus Mechanics & Specifications
Parameter SPI Specification
Wires Required 4 shared (SCK, MOSI, MISO, GND) + 1 dedicated CS per peripheral
Speed Typically 1 MHz to 80 MHz (dependent on peripheral and trace capacitance)
Addressing None. Hardware routing via individual Chip Select (CS/SS) lines.
Max Distance ~1 meter at low speeds (<1 MHz); <15 cm at high speeds (>10 MHz)
Duplex Full-duplex (simultaneous transmit and receive)

Physical Wiring and Pull-Up Requirements

A common mistake among hobbyists is applying I2C wiring rules to SPI. SPI data lines (MOSI, MISO, SCK) do not require pull-up resistors. They are push-pull driven by the controller and peripheral. Adding pull-ups to these lines will only increase rise times and limit your maximum clock speed.

Callout Tip: The Chip Select (CS) Pull-Up Rule
While data lines don't need pull-ups, every peripheral's Chip Select (CS) line must be pulled high to VCC (typically via a 10kΩ resistor). When an ESP32 resets or boots, its GPIO pins momentarily float. If a CS line floats low, the peripheral will wake up and attempt to drive the MISO line, potentially clashing with other peripherals or corrupting boot data.

SPI vs. I2C vs. UART: Choosing the Right Bus for Your Build

Deciding which protocol fits your distance, speed, and device count requirements is the first architectural decision in any embedded build. Here is how the SPI controller stacks up against the alternatives.

Protocol Comparison Matrix
Criterion SPI I2C UART
Best For High-speed, short-distance, high-bandwidth (displays, flash) Many low-speed sensors on minimal pins (temp, IMUs) Point-to-point long-distance, GPS, cellular modems
Max Speed 10 - 80+ MHz 100 kHz (Std), 400 kHz (Fast), 3.4 MHz (High) 115,200 bps to ~921,600 bps typical
Device Count Limited by available MCU GPIO pins for CS lines Up to 127 on a single 2-wire bus 1-to-1 (requires hardware UART ports)
Wiring Overhead High (4 shared + N chip selects) Low (2 shared wires for all devices) Low (2 wires per link: TX/RX)

The Verdict: Choose an SPI controller when you need to move large blocks of data quickly (like reading a 2MB WAV file from an SD card or pushing frame buffers to an ILI9341 TFT). Choose I2C when you are wiring up five different environmental sensors and want to save GPIO pins. Choose UART when talking to a PC, a GPS module, or an ESP8266 AT-command firmware.

Minimal Working Exchange: ESP32 SPI Controller Code & Wiring

Let's build a minimal, working exchange. We will configure an ESP32-WROOM-32 as the SPI controller to read the JEDEC Manufacturer ID from a W25Q32 SPI flash chip. This is the ultimate bench-test for verifying SPI wiring, as the JEDEC ID command (0x9F) is universally supported and returns a predictable 3-byte signature.

Hardware Wiring Table

ESP32 (VSPI Bus) W25Q32 Flash Chip Notes
GPIO 18 (SCK) CLK (Pin 6) Clock signal
GPIO 23 (MOSI) DI (Pin 5) Controller Out, Peripheral In
GPIO 19 (MISO) DO (Pin 2) Peripheral Out, Controller In
GPIO 5 (CS) CS (Pin 1) Add 10kΩ pull-up to 3.3V
3.3V VCC (Pin 8) & /HOLD (Pin 7) Tie /HOLD high to disable hold function
GND GND (Pin 4) & /WP (Pin 3) Tie /WP low for standard SPI mode

ESP32 Arduino IDE Code

This code uses the hardware VSPI bus. It explicitly sets the SPI mode and clock speed using SPISettings, which is critical for preventing bus contention.

#include <SPI.h>

// ESP32 VSPI default pins: SCK=18, MISO=19, MOSI=23, SS=5
const int CS_PIN = 5;
SPIClass vspi(VSPI);

void setup() {
  Serial.begin(115200);
  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH); // Deselect peripheral immediately

  // Initialize VSPI at 1MHz, MSB first, SPI Mode 0
  vspi.begin();
  Serial.println("SPI Controller initialized.");
}

void loop() {
  // Read JEDEC ID Command: 0x9F
  vspi.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
  digitalWrite(CS_PIN, LOW);
  
  vspi.transfer(0x9F); // Send Read JEDEC ID command
  
  uint8_t manufacturer = vspi.transfer(0x00);
  uint8_t mem_type = vspi.transfer(0x00);
  uint8_t capacity = vspi.transfer(0x00);
  
  digitalWrite(CS_PIN, HIGH);
  vspi.endTransaction();

  Serial.printf("JEDEC ID -> Mfg: 0x%02X, Type: 0x%02X, Capacity: 0x%02X\n", 
                manufacturer, mem_type, capacity);
  
  // Winbond W25Q32 should return: Mfg: 0xEF, Type: 0x40, Capacity: 0x16
  delay(2000);
}

Debugging the Bus: Sniffing SPI and Fixing Classic Failures

When an SPI bus fails, it rarely fails silently. You will usually read all 0xFF (MISO floating high) or all 0x00 (MISO shorted to ground). According to SparkFun's SPI guide, the vast majority of bus failures stem from physical layer mismatches rather than software logic errors.

The Classic Failures

  1. CPOL/CPHA Mismatch (Baud & Mode): SPI defines four modes based on Clock Polarity (CPOL) and Clock Phase (CPHA). If your peripheral datasheet specifies SPI Mode 3 (clock idles HIGH, data sampled on falling edge) and your controller defaults to Mode 0, every bit will be shifted incorrectly. Always verify the idle state of the SCK line on an oscilloscope or logic analyzer.
  2. CS Contention (Missing Pull-Ups): Unlike I2C address clashes, SPI suffers from CS contention. If you wire two peripherals to the same CS line, or if a CS line lacks a pull-up resistor and glitches low during MCU boot, both peripherals will attempt to drive the MISO line simultaneously. This creates a short circuit that corrupts data and can overheat the silicon.
  3. Capacitive Loading at High Speeds: Pushing an SPI bus to 40 MHz over 20cm of dupont jumper wires will fail. The parasitic capacitance of the wires rounds off the square clock edges, causing the peripheral to miss clock triggers. Keep high-speed SPI traces under 10cm, or drop the clock speed to 1-4 MHz for breadboard prototyping.

How to Sniff and Debug the Bus

Do not guess SPI timing; measure it. The standard tool for debugging SPI is a logic analyzer. A basic $15 FX2LP clone running Sigrok/PulseView is sufficient for speeds under 10 MHz. For faster buses, a Saleae Logic Pro 8 or similar 100MS/s+ analyzer is required.

Debugging Workflow:
1. Connect probes to SCK, MOSI, MISO, and CS.
2. Set the trigger to the falling edge of the CS line.
3. Capture the transaction and decode the hex bytes.
4. Verify the controller's MOSI matches the peripheral's expected command set, and check if the peripheral is actually pulling MISO low to respond. If MISO stays flat high, your peripheral is unpowered, in reset, or wired to the wrong pin.

SPI Controller FAQ: Long-Tail Troubleshooting

Why is my SPI controller reading all zeros or 0xFF?

Reading all 0xFF means the MISO line is floating high; the peripheral is not responding. This usually indicates the peripheral is unpowered, the CS line is not actually going low (check your GPIO assignment), or you are using the wrong SPI Mode (CPOL/CPHA). Reading all 0x00 means the MISO line is being pulled low, which often points to a wiring short to ground or a dead peripheral IC.

Can I connect multiple SPI peripherals to one ESP32 controller without a multiplexer?

Yes, but with strict rules. You can share the SCK, MOSI, and MISO lines among dozens of peripherals, provided every single peripheral has its own dedicated Chip Select (CS) GPIO pin. You must also ensure that every CS line has a 10kΩ pull-up resistor to VCC. Never share a CS line between two active peripherals unless you are using a hardware demultiplexer like a 74HC138.

How do I change the SPI clock polarity and phase (CPOL/CPHA) in Arduino?

You change the SPI mode inside the SPISettings object. The Arduino SPI library defines four constants: SPI_MODE0, SPI_MODE1, SPI_MODE2, and SPI_MODE3. If your datasheet specifies "CPOL=1, CPHA=1", you must use SPI_MODE3. Pass this into your transaction: SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE3));.

What is the maximum reliable cable length for an SPI bus?

SPI is designed for on-board communication, not long-distance cabling. At 1 MHz, you can reliably push SPI over 50-100 cm of twisted-pair or ribbon cable if you interleave ground wires between signal wires to reduce crosstalk. At 20 MHz+, reliable communication is generally limited to 10-15 cm on a PCB or very short, high-quality jumper wires. If you need SPI over meters of cable, look into differential SPI transceivers like the MAX3030E/MAX3040E, or switch to RS-485.