SPI Protocol Reference

ESP32 SPI reference — VSPI/HSPI buses, pin assignments, SPI modes, clock speeds, and troubleshooting

SPI (Serial Peripheral Interface) is a high-speed, full-duplex serial protocol used when you need to move data quickly — driving TFT displays, reading SD cards, or communicating with fast sensors. It trades simplicity for speed: SPI needs more wires than I2C but can clock data at up to 80 MHz on the ESP32.

🔗How SPI Works

SPI is a master-slave protocol. The ESP32 is the master, and each peripheral is a slave. Unlike I2C (which uses addresses), SPI selects devices with a dedicated CS (Chip Select) line per device. Pull a device's CS line LOW to talk to it; all other devices ignore the bus.

SPI uses four signals:

SignalOther NamesDirectionPurpose
MOSISDI, DIN, DIMaster -> SlaveData from ESP32 to device
MISOSDO, DOUT, DOSlave -> MasterData from device to ESP32
SCKSCLK, CLKMaster -> SlaveClock signal
CSSS, CE, CSNMaster -> SlaveChip Select — one per device

SPI is full duplex: the master sends a byte on MOSI and simultaneously receives a byte on MISO during the same clock cycles. Even if you only want to read, you still send something (usually 0x00), and vice versa.

graph TB
    ESP32["ESP32<br/>(Master)"]

    subgraph Shared Lines
        MOSI["MOSI"]
        MISO["MISO"]
        SCK["SCK"]
    end

    DEV1["Device A<br/>(e.g., TFT Display)"]
    DEV2["Device B<br/>(e.g., SD Card)"]
    DEV3["Device C<br/>(e.g., MAX7219)"]

    ESP32 --- MOSI
    ESP32 --- MISO
    ESP32 --- SCK

    MOSI --- DEV1
    MOSI --- DEV2
    MOSI --- DEV3

    MISO --- DEV1
    MISO --- DEV2
    MISO --- DEV3

    SCK --- DEV1
    SCK --- DEV2
    SCK --- DEV3

    ESP32 -- "CS_A" --- DEV1
    ESP32 -- "CS_B" --- DEV2
    ESP32 -- "CS_C" --- DEV3

MOSI, MISO, and SCK are shared. Each device gets its own CS line. When $\text{CS} = \text{LOW}$, that device is selected.

🔗ESP32 SPI Buses

The ESP32 has four SPI controllers, but only two are available for general use:

BusMOSIMISOSCKDefault CSNotes
VSPIGPIO 23GPIO 19GPIO 18GPIO 5Default bus used by SPI.begin()
HSPIGPIO 13GPIO 12GPIO 14GPIO 15Second bus, must be configured manually
SPI0Reserved for internal flash (do not use)
SPI1Reserved for internal flash (do not use)

Important: SPI0 and SPI1 are used by the ESP32's flash memory. Never configure your peripherals on these buses.

🔗Using HSPI

If you need a second SPI bus (or VSPI pins are occupied), configure HSPI manually:

#include <SPI.h>

SPIClass hspi(HSPI);

void setup() {
  hspi.begin(14, 12, 13, 15);  // SCK, MISO, MOSI, CS
}

Like I2C, the GPIO matrix lets you remap SPI pins to almost any available GPIO. Just avoid GPIOs 6--11 (flash) and 34--39 (input-only).

🔗SPI Modes

SPI has four modes defined by two parameters:

  • CPOL (Clock Polarity) — the idle state of the clock line (LOW or HIGH)
  • CPHA (Clock Phase) — whether data is sampled on the rising or falling clock edge
ModeCPOLCPHAClock Idle StateData Sampled On
SPI_MODE000LOWRising edge
SPI_MODE101LOWFalling edge
SPI_MODE210HIGHFalling edge
SPI_MODE311HIGHRising edge

SPI_MODE0 is the most common. Most sensors and displays default to mode 0. Always check the device datasheet if communication fails — using the wrong mode will produce garbage data or no response at all.

🔗Clock Speed

The ESP32 supports SPI clock speeds up to 80 MHz, but practical limits depend on the device and wiring:

SpeedTypical Use
1 MHzSafe default for testing
10 MHzMost sensors and modules
20--40 MHzTFT displays, SD cards
80 MHzMaximum, only for short traces on a PCB

Longer wires and breadboard connections add capacitance, which limits the maximum reliable clock speed. On a breadboard, staying at or below 10 MHz is a good rule of thumb.

The clock frequency you request is rounded down to the nearest divisor of 80 MHz. For example, requesting 15 MHz gives you $80 / 6 \approx 13.3\,\text{MHz}$.

🔗Code Example

The standard Arduino SPI transaction pattern — this is what libraries like TFT_eSPI and SD.h do internally:

#include <SPI.h>

#define CS_PIN 5

void setup() {
  Serial.begin(115200);
  SPI.begin();              // Init VSPI: SCK=18, MISO=19, MOSI=23
  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH);  // Deselect device
}

byte spiTransfer(byte reg) {
  byte result;

  digitalWrite(CS_PIN, LOW);                             // 1. Select device
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0)); // 2. Configure bus
  SPI.transfer(reg);                                     // 3. Send register address
  result = SPI.transfer(0x00);                           // 4. Read response
  SPI.endTransaction();                                  // 5. Release bus settings
  digitalWrite(CS_PIN, HIGH);                            // 6. Deselect device

  return result;
}

void loop() {
  byte value = spiTransfer(0x0F);  // Example: read WHO_AM_I register
  Serial.printf("Register 0x0F = 0x%02X\n", value);
  delay(1000);
}

In practice, you rarely write raw SPI code. Device libraries handle the protocol details — you just need to know which pins to pass to the library's constructor.

🔗SPI vs I2C — When to Choose SPI

FactorSPII2C
SpeedUp to 80 MHzUp to 400 kHz
Wires3 + 1 per device2 (shared)
Pin usageHigher (extra CS per device)Lower
ComplexityMediumLow
Best forDisplays, SD cards, fast ADCsSensors, small OLEDs

If your device supports both protocols, choose I2C for simplicity or SPI for speed.

🔗Common Issues

ProblemCauseFix
No response from deviceCS pin not configured as OUTPUT or not pulled LOWAdd pinMode(CS_PIN, OUTPUT) and digitalWrite(CS_PIN, LOW) before transfers
Garbage dataWrong SPI modeCheck the datasheet for CPOL/CPHA and set the correct SPI_MODE
HSPI not workingUsing VSPI pins with HSPI objectMake sure you pass the correct HSPI pin numbers to begin()
Intermittent failuresClock too fast for breadboard wiringReduce clock speed to 1--4 MHz for testing
Conflicts with flashUsing GPIOs 6--11These pins are reserved for the ESP32's internal flash — choose other pins
Two devices interferingShared CS or floating CS lineEvery device needs its own CS pin; pull unused CS lines HIGH

🔗Used In

These articles on this site use SPI: