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.
| 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. |
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.
| 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 |
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:
- Shared Lines: Connect ESP32 SCK, MOSI, and MISO to the corresponding pins on all SPI devices on the bus.
- Individual CS: Run a dedicated wire from a unique ESP32 GPIO to the CS pin of each device.
- 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_MODE0toSPI_MODE3in yourSPISettingsfixes 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.






