SPI (Serial Peripheral Interface) is a high-speed, full-duplex synchronous serial protocol essential for connecting the ESP32 to a wide range of peripherals like displays, sensors, SD cards, and wireless modules. This comprehensive guide goes beyond basic theory to deliver practical, actionable knowledge on mastering the ESP32‘s SPI capabilities using the Arduino IDE. We’ll cover everything from the fundamentals of the four-wire bus to advanced techniques for using custom pins and managing multiple devices on independent SPI buses, all based on hands-on experience and the official ESP32 framework.

Understanding SPI: The Master-Slave Communication Protocol
SPI operates on a master-slave architecture, where a single controller (the master, in this case, your ESP32) manages one or more peripheral devices (the slaves). It’s a synchronous protocol, meaning data transfer is coordinated by a clock signal (SCK) generated by the master. One of its key advantages is full-duplex communication; data can be sent from the master to the slave (via MOSI) and from the slave to the master (via MISO) simultaneously, making it faster than protocols like I2C.
To operate an SPI bus, you need four essential lines:
-
MOSI (Master Out Slave In): The line for data sent from the master to the slave.
-
MISO (Master In Slave Out): The line for data sent from the slave to the master.
-
SCK (Serial Clock): The clock signal generated by the master to synchronize data bits.
-
CS/SS (Chip Select / Slave Select): An active-low signal used by the master to select which specific slave device is active on the shared bus.
Pro-Tip: On slave devices like sensor modules, you might see the terms SDO (Serial Data Out) instead of MISO and SDI (Serial Data In) instead of MOSI. They refer to the same pins from the peripheral’s perspective.
ESP32 SPI Hardware: Unlocking HSPI and VSPI
The ESP32 chip is equipped with four SPI peripherals (SPI0, SPI1, HSPI, VSPI). However, SPI0 and SPI1 are dedicated to internal communication with the device’s flash memory and are not available for general use.
For your projects, you have two fully independent, user-configurable SPI buses:
-
HSPI (SPI2): The “High-Speed” SPI peripheral.
-
VSPI (SPI3): The “Very High-Speed” SPI peripheral (often used as the default).
Each of these buses (HSPI and VSPI) can control up to three slave devices by using separate Chip Select (CS) pins. This means you can natively connect up to six SPI devices to a single ESP32. For scenarios requiring more devices, an SPI multiplexer (mux) would be necessary.
Default SPI Pins and How to Find Yours
Most common ESP32 development boards (like the ESP32 DevKit) come with a default pin mapping for the HSPI and VSPI buses. The following table shows the typical arrangement:
⚠️ Critical Check: These are common defaults, but they can vary between board models and manufacturers (especially for ESP32-S3, which has a different mapping). Always verify your specific board’s pinout diagram.
You can also use this simple Arduino sketch to print your board’s configured default SPI pins to the Serial Monitor. Ensure you’ve selected the correct board under Tools > Board before uploading.
void setup() {
Serial.begin(115200);
Serial.print("Default MOSI: "); Serial.println(MOSI);
Serial.print("Default MISO: "); Serial.println(MISO);
Serial.print("Default SCK: "); Serial.println(SCK);
Serial.print("Default SS: "); Serial.println(SS);
}
void loop() {}
Using Custom SPI Pins for Maximum Flexibility
A significant strength of the ESP32 is its GPIO matrix, which allows most peripheral functions to be mapped to almost any GPIO pin. You are not locked into the default SPI pins. There are two primary methods to use custom pins:
1. Via Library Constructor (Recommended)
Most well-written peripheral libraries (e.g., for the BME280 sensor) allow you to specify the SPI pins when creating the device object. This is the cleanest and easiest method.
#include <Adafruit_BME280.h>
#include <SPI.h>
#define MY_MISO 32
#define MY_MOSI 26
#define MY_SCK 25
#define MY_CS 33
Adafruit_BME280 bme(MY_CS, MY_MOSI, MY_MISO, MY_SCK);
void setup() {
Serial.begin(9600);
if (!bme.begin()) {
Serial.println("Could not find BME280 sensor!");
while (1);
}
}
2. By Initializing the SPI Bus Directly
If a library doesn’t offer a direct pin configuration, or you are writing low-level SPI code, you can initialize the bus with SPI.begin().
#include <SPI.h>
#define CUSTOM_SCK 18
#define CUSTOM_MISO 19
#define CUSTOM_MOSI 23
#define CUSTOM_SS 5
void setup() {
SPI.begin(CUSTOM_SCK, CUSTOM_MISO, CUSTOM_MOSI, CUSTOM_SS);
}
Advanced Configuration: Multiple SPI Devices and Dual Buses
Connecting Multiple Slaves on One Bus
You can connect several devices to the same SPI bus (e.g., VSPI) as long as each has a unique Chip Select (CS) pin. The master (ESP32) controls communication by pulling the CS pin of the target slave LOW and keeping all others HIGH.
#define CS_SENSOR_1 5
#define CS_SENSOR_2 17
void setup() {
pinMode(CS_SENSOR_1, OUTPUT);
pinMode(CS_SENSOR_2, OUTPUT);
digitalWrite(CS_SENSOR_1, HIGH);
digitalWrite(CS_SENSOR_2, HIGH);
SPI.begin();
}
void readSensor1() {
digitalWrite(CS_SENSOR_1, LOW);
digitalWrite(CS_SENSOR_1, HIGH);
}
void readSensor2() {
digitalWrite(CS_SENSOR_2, LOW);
digitalWrite(CS_SENSOR_2, HIGH);
}
Leveraging Both HSPI and VSPI Simultaneously
For higher performance or to isolate critical devices, you can use both HSPI and VSPI buses at the same time. This requires creating separate SPIClass objects.
#include <SPI.h>
#define VSPI_MISO 19
#define VSPI_MOSI 23
#define VSPI_SCK 18
#define VSPI_SS 5
#define HSPI_MISO 12
#define HSPI_MOSI 13
#define HSPI_SCK 14
#define HSPI_SS 15
SPIClass * vspi = new SPIClass(VSPI);
SPIClass * hspi = new SPIClass(HSPI);
void setup() {
vspi->begin(VSPI_SCK, VSPI_MISO, VSPI_MOSI, VSPI_SS);
hspi->begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, HSPI_SS);
pinMode(vspi->pinSS(), OUTPUT);
pinMode(hspi->pinSS(), OUTPUT);
}
void loop() {
vspi->beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
digitalWrite(vspi->pinSS(), LOW);
vspi->transfer(0xAA);
digitalWrite(vspi->pinSS(), HIGH);
vspi->endTransaction();
hspi->beginTransaction(SPISettings(500000, MSBFIRST, SPI_MODE0));
digitalWrite(hspi->pinSS(), LOW);
hspi->transfer(0xBB);
digitalWrite(hspi->pinSS(), HIGH);
hspi->endTransaction();
delay(1000);
}
Practical Code Walkthrough: Integrating an SPI Sensor
Let’s solidify the concepts with a practical example of reading from a BME280 temperature, pressure, and humidity sensor via SPI.
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <SPI.h>
#define BME280_SCK 25
#define BME280_MISO 32
#define BME280_MOSI 26
#define BME280_CS 33
Adafruit_BME280 bme(BME280_CS, BME280_MOSI, BME280_MISO, BME280_SCK);
void setup() {
Serial.begin(115200);
Serial.println(F("BME280 SPI Test"));
if (!bme.begin()) {
Serial.println("Failed to find BME280. Check wiring/pins!");
while (1);
}
}
void loop() {
Serial.print("Temperature = ");
Serial.print(bme.readTemperature());
Serial.println(" °C");
Serial.print("Pressure = ");
Serial.print(bme.readPressure() / 100.0F);
Serial.println(" hPa");
Serial.print("Humidity = ");
Serial.print(bme.readHumidity());
Serial.println(" %");
Serial.println("----------------");
delay(5000);
}
Troubleshooting Common SPI Issues
This guide provides the foundational and advanced knowledge to confidently implement SPI communication in your ESP32 projects. By understanding the dual-bus architecture, mastering custom pin configuration, and following best practices for multi-slave management, you can interface with a vast ecosystem of high-speed peripherals.