The Complete Guide to ESP32 SPI Communication with Arduino IDE

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:

  1. HSPI (SPI2): The “High-Speed” SPI peripheral.

  2. 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:

SPI Bus MOSI MISO SCK CS (Example)
VSPI GPIO 23 GPIO 19 GPIO 18 GPIO 5 (Any GPIO)
HSPI GPIO 13 GPIO 12 GPIO 14 GPIO 15 (Any GPIO)

⚠️ 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.

cpp
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.

cpp
#include <Adafruit_BME280.h>
#include <SPI.h>

// Define your CUSTOM SPI pins (not the defaults)
#define MY_MISO 32
#define MY_MOSI 26
#define MY_SCK  25
#define MY_CS   33

// Pass custom pins to the library constructor
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);
  }
}
// ... loop() to read sensor

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().

cpp
#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);
  // ... further device-specific setup
}

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.

cpp
#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); // Deselect both initially
  digitalWrite(CS_SENSOR_2, HIGH);
  SPI.begin(); // Initialize the bus
}

void readSensor1() {
  digitalWrite(CS_SENSOR_1, LOW); // Select Sensor 1
  // ... Perform SPI.transfer() commands here
  digitalWrite(CS_SENSOR_1, HIGH); // Deselect
}

void readSensor2() {
  digitalWrite(CS_SENSOR_2, LOW); // Select Sensor 2
  // ... Perform SPI.transfer() commands here
  digitalWrite(CS_SENSOR_2, HIGH); // Deselect
}

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.

cpp
#include <SPI.h>

// Define pins for VSPI (can be default or custom)
#define VSPI_MISO 19
#define VSPI_MOSI 23
#define VSPI_SCK  18
#define VSPI_SS    5

// Define pins for HSPI (can be default or custom)
#define HSPI_MISO 12
#define HSPI_MOSI 13
#define HSPI_SCK  14
#define HSPI_SS   15

// Create two distinct SPI objects
SPIClass * vspi = new SPIClass(VSPI);
SPIClass * hspi = new SPIClass(HSPI);

void setup() {
  // Initialize both buses with their respective pins
  vspi->begin(VSPI_SCK, VSPI_MISO, VSPI_MOSI, VSPI_SS);
  hspi->begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, HSPI_SS);

  pinMode(vspi->pinSS(), OUTPUT); // Set SS pins as outputs
  pinMode(hspi->pinSS(), OUTPUT);
}

void loop() {
  // Communicate with a device on the VSPI bus
  vspi->beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
  digitalWrite(vspi->pinSS(), LOW);
  vspi->transfer(0xAA); // Example command
  digitalWrite(vspi->pinSS(), HIGH);
  vspi->endTransaction();

  // Independently communicate with a device on the HSPI bus
  hspi->beginTransaction(SPISettings(500000, MSBFIRST, SPI_MODE0));
  digitalWrite(hspi->pinSS(), LOW);
  hspi->transfer(0xBB); // Example command
  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.

cpp
/*
 * ESP32 SPI Communication with BME280 Sensor
 * Uses custom SPI pins and demonstrates library integration.
 */

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <SPI.h>

// 1. DEFINE CUSTOM SPI PINS
#define BME280_SCK   25
#define BME280_MISO  32
#define BME280_MOSI  26
#define BME280_CS    33

// 2. CREATE SENSOR OBJECT WITH CUSTOM PINS
Adafruit_BME280 bme(BME280_CS, BME280_MOSI, BME280_MISO, BME280_SCK);

void setup() {
  Serial.begin(115200);
  Serial.println(F("BME280 SPI Test"));

  // 3. INITIALIZE SENSOR
  if (!bme.begin()) {
    Serial.println("Failed to find BME280. Check wiring/pins!");
    while (1);
  }
}

void loop() {
  // 4. READ AND PRINT VALUES
  Serial.print("Temperature = ");
  Serial.print(bme.readTemperature());
  Serial.println(" °C");

  Serial.print("Pressure = ");
  Serial.print(bme.readPressure() / 100.0F); // Convert to hPa
  Serial.println(" hPa");

  Serial.print("Humidity = ");
  Serial.print(bme.readHumidity());
  Serial.println(" %");

  Serial.println("----------------");
  delay(5000); // Wait 5 seconds
}

Troubleshooting Common SPI Issues

Problem Likely Cause Solution
No response from slave Incorrect wiring, wrong CS pin, or slave not powered. Double-check MISO/MOSI/SCK/CS and GND connections. Verify the slave is powered (3.3V). Manually toggle the CS pin LOW/HIGH.
Garbage/incorrect data SPI mode mismatch or excessive clock speed. Ensure the master and slave use the same SPI Mode (MODE0, MODE1, MODE2, MODE3). Reduce the SPI clock speed (SPISettings).
SPI bus conflicts Multiple slaves interfering on the same CS line. Ensure only one CS pin is LOW at any time. Add a small delay between selecting different slaves.
begin() fails with custom pins Pins are not suitable for output or are already in use. Avoid using strapping pins (GPIO 0, 2, 12, 15 on boot). Check your board’s pinout for restricted pins.

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.

======================================

About ESP32S.com

Since 2016, ESP32S.com has grown to become a complete ecosystem partner for your IoT journey. Based in Shenzhen, a global hub for electronics innovation, we have helped hundreds of developers and businesses bring their ESP32-based ideas to life. Our team is dedicated to providing exceptional support and innovative solutions to help you achieve your IoT goals.
At ESP32S.com, we master the intricacies of developing an ESP32-based product, which involves multiple stages, from concept to market launch. That’s why we now offer comprehensive solutions covering the entire product lifecycle for ESP32-based devices. Whether you need help with PCB design, prototyping, production, or even marketing and fulfillment, we have you covered.

Contact Us

Ready to take your IoT project to the next level? Contact ESP32S.com today to learn more about our comprehensive solutions for ESP32-based devices. Let us be your trusted partner in bringing your innovative ideas to life. Contact us now to get started.

Table of Contents

Related Posts
Start typing to see products you are looking for.
Shopping cart
Sign in

No account yet?

Shop
Wishlist
0 items Cart
My account