ES: ESP32 SPI

SPI (Serial Peripheral Interface) is a high-speed synchronous communication protocol used to interface the ESP32 with external flash memory, displays, sensors, and other peripherals. This guide covers SPI fundamentals, the ESP32 SPI buses, and how to use the Arduino SPI library.

SPI is the protocol of choice when speed matters. Unlike I2C, which operates at typical speeds of 100–400 kHz, SPI can run at tens of megahertz — making it the right choice for displays, SD card interfaces, high-speed ADCs, and external flash memory.


What is SPI?

SPI (Serial Peripheral Interface) is a synchronous, full-duplex serial communication protocol that uses four signals:

SignalDirectionDescription
MOSIMaster → SlaveMaster Out, Slave In — data from master to slave
MISOSlave → MasterMaster In, Slave Out — data from slave to master
SCKMaster → SlaveClock signal generated by master
CS/NSSMaster → SlaveChip Select — LOW selects the target device

The master (ESP32) generates the clock and initiates all transfers. The slave (sensor, display, etc.) responds when selected via its CS pin.

md
SPI Bus Topology (Multiple Slaves)
-----------------------------------
ESP32 (Master)
  MOSI ----+----------+----------+
  MISO ----+----------+----------+
  SCK  ----+----------+----------+
  CS0  ----|Slave 0   |          |
  CS1  ---------+    |Slave 1   |
  CS2  -------------------|Slave 2|

Each slave shares the MOSI, MISO, and SCK lines, but has a dedicated CS line. Only one slave is active at a time.


SPI Modes

SPI has four operating modes defined by two parameters:

ParameterDescription
CPOL (Clock Polarity)Idle state of clock: 0 = LOW idle, 1 = HIGH idle
CPHA (Clock Phase)Data sampled on: 0 = first edge, 1 = second edge
ModeCPOLCPHADescription
Mode 000Clock idle LOW, sample on rising edge
Mode 101Clock idle LOW, sample on falling edge
Mode 210Clock idle HIGH, sample on falling edge
Mode 311Clock idle HIGH, sample on rising edge

Most devices use Mode 0 or Mode 3. Always check the device datasheet.


ESP32 SPI Buses

The ESP32 provides four SPI controllers, two of which are available for general use:

BusNameDefault MOSIDefault MISODefault CLKDefault CS
SPI2HSPIGPIO13GPIO12GPIO14GPIO15
SPI3VSPIGPIO23GPIO19GPIO18GPIO5

SPI0 and SPI1 are reserved for the internal flash memory connection and must not be used.

VSPI (SPI3) is recommended for general use because its default pins (GPIO18, 19, 23, 5) have no boot-strapping constraints.


Using SPI with the Arduino Framework

The Arduino ESP32 core provides the SPI.h library for SPI communication.

Include and Initialize

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

#define CS_PIN 5    // Chip select — VSPI default

void setup() {
    Serial.begin(115200);

    // Initialize SPI with default VSPI pins
    SPI.begin();

    // Configure CS pin as output and deselect device
    pinMode(CS_PIN, OUTPUT);
    digitalWrite(CS_PIN, HIGH);
}

Single Byte Transfer

cpp
uint8_t send_data(uint8_t data) {
    uint8_t received;

    SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
    digitalWrite(CS_PIN, LOW);          // Select the slave device
    received = SPI.transfer(data);      // Send byte, receive byte simultaneously
    digitalWrite(CS_PIN, HIGH);         // Deselect the slave device
    SPI.endTransaction();

    return received;
}

SPI.transfer() is full-duplex — it sends one byte while simultaneously receiving one byte from the slave.

SPISettings Parameters

cpp
SPISettings(speed, bitOrder, dataMode)

ParameterDescriptionExample
speedClock frequency in Hz1000000 = 1 MHz
bitOrderBit transmission orderMSBFIRST or LSBFIRST
dataModeSPI mode (CPOL/CPHA)SPI_MODE0, SPI_MODE1, SPI_MODE2, SPI_MODE3

Multiple Byte Transfer (Buffer)

cpp
void spi_transfer_buffer(uint8_t *tx_buf, uint8_t *rx_buf, size_t len) {
    SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
    digitalWrite(CS_PIN, LOW);

    for (size_t i = 0; i < len; i++) {
        rx_buf[i] = SPI.transfer(tx_buf[i]);
    }

    digitalWrite(CS_PIN, HIGH);
    SPI.endTransaction();
}


Custom SPI Pins

To use non-default SPI pins, specify them in SPI.begin():

cpp
// SPI.begin(SCK, MISO, MOSI, SS)
SPI.begin(14, 12, 13, 15);   // Use HSPI pins explicitly

This is useful when VSPI default pins conflict with other peripherals in your design.


Complete SPI Example: Reading a Register from a Sensor

This pattern applies to most SPI sensors and ADCs:

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

#define CS_PIN    5
#define READ_CMD  0x80   // Read command bit (sensor-specific)

void setup() {
    Serial.begin(115200);
    SPI.begin();
    pinMode(CS_PIN, OUTPUT);
    digitalWrite(CS_PIN, HIGH);
    delay(100);
}

uint8_t read_register(uint8_t reg_address) {
    uint8_t value;

    SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
    digitalWrite(CS_PIN, LOW);
    SPI.transfer(READ_CMD | reg_address);  // Send read command + register address
    value = SPI.transfer(0x00);            // Send dummy byte, receive register value
    digitalWrite(CS_PIN, HIGH);
    SPI.endTransaction();

    return value;
}

void loop() {
    uint8_t device_id = read_register(0x0F);  // Read WHO_AM_I register
    Serial.printf("Device ID: 0x%02X\n", device_id);
    delay(1000);
}


SPI vs I2C Comparison

FeatureSPII2C
SpeedUp to 80 MHz on ESP32100 kHz / 400 kHz / 3.4 MHz
Wires4 (MOSI, MISO, SCK, CS per device)2 (SDA, SCL)
DuplexFull-duplexHalf-duplex
Multi-deviceOne CS per deviceUp to 127 devices (7-bit address)
DistanceShort (PCB-level)Short (PCB-level, up to ~1 m)
Best forDisplays, flash, high-speed ADCSensors, EEPROMs, RTC

Final Thoughts

SPI is the protocol for applications where speed and full-duplex operation matter. The ESP32's VSPI and HSPI buses, combined with the Arduino SPI.h library, make it straightforward to interface with the wide range of SPI-compatible peripherals.

Always:

  • Check the device datasheet for the correct SPI mode (CPOL/CPHA)
  • Use SPISettings with beginTransaction/endTransaction for safe multi-device operation
  • Keep CS lines managed carefully when multiple SPI devices share the same bus

SPI is how the ESP32 talks to the fastest peripherals in the system.