The standard ESP32-WROOM-32 features two fully usable hardware SPI buses: VSPI and HSPI. The default VSPI pins are GPIO 18 (SCK), 19 (MISO), 23 (MOSI), and 5 (CS). The default HSPI pins are GPIO 14 (SCK), 12 (MISO), 13 (MOSI), and 15 (CS). While the ESP32’s GPIO matrix allows you to remap these signals to almost any available pin, sticking to the hardware defaults prevents routing conflicts and avoids triggering the chip's strapping pin boot failures.

The Physical Layer: SPI Bus Mechanics & Protocol Fit

Before wiring up your ESP32, you need to know if SPI is actually the right tool for your sensor or display. Serial Peripheral Interface (SPI) is a synchronous, full-duplex protocol designed for high throughput over short distances. For a deeper look at the protocol's origins and timing diagrams, refer to the SparkFun SPI Tutorial.

SPI Bus Mechanics & Constraints
Parameter SPI Specification ESP32 Implementation Notes
Wires Required 4 shared (SCK, MOSI, MISO, GND) + 1 CS per device CS lines are active-LOW. You need one free GPIO for every target.
Speed (Clock) Typically 1MHz - 20MHz (up to 80MHz) ESP32 can drive up to 80MHz, but most sensors max out at 10-20MHz.
Addressing None (Hardware Chip Select routing) No software addressing overhead; pure data throughput.
Distance Under 1 meter (typically < 30cm) High clock speeds suffer from signal reflection and capacitance on long wires.
Which protocol fits your project?
Use SPI when you need high speed (TFT displays, SD cards) over short distances (< 30cm) with a moderate device count (1-4 devices). Use I2C for low-speed sensors (BME280, MPU6050) where you want to daisy-chain dozens of devices on just two wires. Use RS-485 or CAN if your devices are spread across several meters or in electrically noisy industrial environments.

Default ESP32 SPI Pins: VSPI vs HSPI

On the ubiquitous 30-pin ESP32 DevKitV1, the silicon exposes two SPI peripherals. The Arduino core maps these to SPI (which defaults to VSPI) and HSPI.

Standard ESP32-WROOM-32 SPI Pinout
Signal VSPI (Default SPI) HSPI Direction (Master POV)
SCK (Clock) GPIO 18 GPIO 14 Output
MISO (Master In) GPIO 19 GPIO 12 Input
MOSI (Master Out) GPIO 23 GPIO 13 Output
CS / SS (Chip Select) GPIO 5 GPIO 15 Output
⚠️ The GPIO 12 Boot Trap: GPIO 12 is the default MISO pin for HSPI, but it is also a critical strapping pin. If GPIO 12 is pulled HIGH during power-on, the ESP32 will attempt to boot using 1.8V flash voltage instead of 3.3V, resulting in a continuous boot loop. If your SPI slave device has internal pull-ups on MISO, you must either use VSPI instead, or physically rewire HSPI MISO to a different GPIO using the GPIO matrix.

Physical Wiring and Minimal Working Exchange

Physical layer integrity dictates SPI success. Keep your SCK, MOSI, and MISO jumper wires under 15cm if you plan to run the clock above 10MHz. More importantly, you must manage the Chip Select (CS) lines.

Physical Wiring Requirements:

  1. Shared Lines: Connect ESP32 SCK, MOSI, and MISO to the corresponding pins on all SPI devices on the bus.
  2. Individual CS: Run a dedicated wire from a unique ESP32 GPIO to the CS pin of each device.
  3. The Pull-Up Rule: Solder or breadboard a 10kΩ pull-up resistor between 3.3V and every CS line. During ESP32 reset, GPIOs float. If a CS line floats LOW, the slave device will wake up and interpret MOSI noise as commands, corrupting its internal state before your code even starts.

Below is a minimal, robust Arduino IDE example initializing the HSPI bus with custom pins to avoid the GPIO 12 strapping conflict, then reading a dummy register.

#include <SPI.h>

// Define HSPI pins (Remapped to avoid GPIO 12 strapping pin)
#define HSPI_SCK  14
#define HSPI_MISO 27  // Remapped from 12
#define HSPI_MOSI 13
#define HSPI_CS   15

// Instantiate the HSPI class
SPIClass hspi(HSPI);

void setup() {
  Serial.begin(115200);
  
  // Initialize HSPI with custom pin mapping
  hspi.begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
  
  // Configure CS pin
  pinMode(HSPI_CS, OUTPUT);
  digitalWrite(HSPI_CS, HIGH); // Deselect slave immediately
  
  Serial.println("HSPI Initialized on custom MISO pin 27.");
}

void loop() {
  // Begin transaction: 10MHz clock, MSB first, SPI Mode 0
  hspi.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));
  
  digitalWrite(HSPI_CS, LOW); // Assert Chip Select
  
  // Send command byte (e.g., 0x80 for read register 0x00)
  hspi.transfer(0x80);
  
  // Clock in the response byte
  uint8_t response = hspi.transfer(0x00);
  
  digitalWrite(HSPI_CS, HIGH); // Deassert Chip Select
  hspi.endTransaction();
  
  Serial.printf("Received: 0x%02X\n", response);
  delay(1000);
}

Debugging the Bus: Sniffing and Classic Failures

When your SPI display stays blank or your SD card fails to mount, guessing is a waste of time. You need to look at the physical signals. The ESP32 Technical Reference Manual details the internal GPIO matrix, but external hardware debugging requires a logic analyzer.

How to Sniff the Bus:
You do not need a $400 Saleae Logic Pro. A standard $15 24MHz 8-channel clone logic analyzer running PulseView / Sigrok is perfectly adequate for SPI debugging. Connect the ground clip to your circuit GND, and probe SCK, MOSI, MISO, and CS. Crucial rule: Set your logic analyzer sample rate to at least 4x your SPI clock speed. If your ESP32 is pushing a 10MHz SPI clock, set the analyzer to sample at 50MHz or higher to avoid aliasing and missed clock edges.

The Classic SPI Failures:

  • The "Address Clash" (CS Collision): Unlike I2C, SPI has no software addressing. The equivalent of an I2C address clash is a CS collision. If you accidentally assign the same GPIO for two different CS lines, or forget to pull a CS line HIGH in your code before initializing a second device, both slaves will drive the MISO line simultaneously. This causes a short circuit, resulting in corrupted data and potentially damaging the slave's output buffers.
  • Missing Pull-Up on CS: As mentioned, if you omit the 10kΩ pull-up on the CS line, the ESP32's boot sequence (which outputs debug logs on GPIO 1/3 and toggles various pins) will accidentally clock garbage into your SPI device. The device will lock up before setup() even runs.
  • Baud Mismatch & Clock Polarity: SPI defines four modes based on Clock Polarity (CPOL) and Clock Phase (CPHA). Mode 0 (CPOL=0, CPHA=0) and Mode 3 (CPOL=1, CPHA=1) are the most common. If your logic analyzer shows data shifting on the wrong clock edge, or the MISO line is returning all 0xFF / 0x00, check the slave's datasheet. Changing SPI_MODE0 to SPI_MODE3 in your SPISettings fixes 90% of "dead on arrival" SPI sensor issues.

FAQ: ESP32 SPI Pins

Can I use any GPIO for ESP32 SPI pins?

Almost, but not all. The ESP32’s GPIO matrix allows you to route the SPI signals to most pins, but you must avoid GPIOs 34, 35, 36, and 39. These are input-only pins and cannot drive the SCK, MOSI, or CS outputs. Additionally, avoid GPIOs 6-11, as they are connected to the integrated SPI flash memory on the WROOM module and using them for external peripherals will crash the system.

Why is my ESP32 HSPI MISO (GPIO 12) causing a boot loop?

GPIO 12 is a strapping pin used by the ESP32 bootloader to determine the flash voltage (3.3V vs 1.8V). If your connected SPI device has an internal pull-up resistor on its MISO line, it will pull GPIO 12 HIGH during power-on. The ESP32 will misread this, switch to 1.8V flash mode, fail to read its own firmware, and reboot endlessly. The fix is to either move the HSPI MISO pin to a safe GPIO (like 27) via the hspi.begin() remapping shown above, or physically remove the pull-up on the slave device.

How do I connect multiple SPI devices to the same ESP32 SPI bus?

SPI is designed to be shared. Wire the SCK, MOSI, and MISO pins of all devices together in parallel. Then, assign a unique ESP32 GPIO to the Chip Select (CS) pin of each device. In your code, create separate SPISettings if the devices require different clock speeds or SPI modes, and ensure you only pull one CS line LOW at a time while keeping all others HIGH. Never daisy-chain MISO to MOSI between devices unless the hardware specifically supports SPI daisy-chaining (like certain LED drivers); for standard sensors and displays, use the parallel bus with individual CS lines.