SPI is the synchronous, fast serial bus that links the ESP32 to demanding peripherals — SD card, TFT display, high-rate sensors — over four wires: SCK (clock), MOSI (outgoing data), MISO (incoming data) and one CS per peripheral. On the ESP32, two buses are usable (VSPI and HSPI). The default VSPI pins are SCK = GPIO18, MISO = GPIO19, MOSI = GPIO23, CS = GPIO5, and they are remappable, at the price of a capped speed once you leave the native pins.

What is it for?

Where the I2C tops out, the SPI transfers fast and in full duplex (sending and receiving at the same time):

  • Storing. A microSD card records measurements and logs. That is the most common use of the SPI in a project.
  • Displaying richly. Colour TFT screens refresh whole images — out of reach over I2C.
  • Reading fast. Some ADCs, motion sensors and radio modules exist only in SPI, for the throughput.

How the protocol works (clock, full duplex, selection by CS) is described in the cross-cutting concept page SPI. Here you see how to wire it and code it on the ESP32.

Step by step

Four moves: wire the harness, initialise, use a library, add peripherals.

1. Wire it: a shared harness, one CS per module

SCK, MOSI and MISO are common to every peripheral on the bus. Only the CS (Chip Select) belongs to each one: it is what designates the active module. Unlike the UART, the SPI does not cross over: MOSI goes to MOSI, MISO to MISO.

SPI wiring between an ESP32 and a module (SD card): SCK on GPIO18, MOSI on GPIO23, MISO on GPIO19, CS on GPIO5, 3.3 V supply, common ground|640

Warning

A 3.3 V peripheral. The ESP32 works at 3.3 V: a 5 V SPI module (some SD readers with a regulator) must either carry a built-in level shifter, or go through an adapter (see logic levels).

2. Initialise the bus

SPI.begin() with no argument uses VSPI (default pins). The libraries (SD, TFT) often call begin() themselves. What you mainly pass them is the CS:

#include <SPI.h>
 
const int CS = 5;   // VSPI: SCK=18, MOSI=23, MISO=19 by default

3. Use a library

You rarely handle SPI.transfer() by hand: a library (SD, Adafruit GFX for a TFT) takes care of the protocol. You give it the CS and, if needed, a clock speed.

4. Add peripherals

For a second module, you share SCK/MOSI/MISO and give it another CS (a free GPIO). The code activates a single CS at a time.

Example — Logging to a microSD card

A concrete case: write a line of measurement into a file on the SD card, then read it back. This is the entry point of the embedded datalogger.

#include <SPI.h>
#include <SD.h>
 
const int CS = 5;   // CS of the SD reader on GPIO5 (VSPI)
 
void setup() {
  Serial.begin(115200);
 
  if (!SD.begin(CS)) {                 // VSPI by default: SCK18 / MISO19 / MOSI23
    Serial.println("SD card missing or miswired");
    return;
  }
 
  File f = SD.open("/mesures.csv", FILE_WRITE);   // opens (creates if needed)
  if (f) {
    f.println("time;value");           // header
    f.println("0;512");                // one measurement
    f.close();                         // close() required: flushes the buffer to the card
    Serial.println("Written.");
  }
 
  // reading back
  f = SD.open("/mesures.csv");
  while (f && f.available()) {
    Serial.write(f.read());            // sends the content back to the monitor
  }
  f.close();
}
 
void loop() {}

SD.begin(CS) initialises the VSPI bus and mounts the card. SD.open(..., FILE_WRITE) opens the file for writing. The close() is essential: it flushes the buffer and closes the file, without which the last write can be lost. Reading back reopens the file and sends its content to the monitor.

In the monitor:

Written.
time;value
0;512

The last two lines do not come from a println of the sketch: they are the content of the file, read back byte by byte and sent by Serial.write(). The proof that the write did reach the card.

Pitfalls

MOSI and MISO crossed over. A reflex inherited from the UART, but the SPI does not cross over: MOSI on MOSI, MISO on MISO. Crossing them gives a silent module.

CS forgotten or shared. Two modules on the same CS answer together and jam each other. Every peripheral has its own CS on a distinct GPIO.

SD card not detected. Often a wrong CS, a swapped MISO/MOSI wire, or a badly formatted card (FAT32). Check the wiring pin by pin against the code first.

A 5 V peripheral on the ESP32. Lines at 5 V attack the 3.3 V pins. Pick a 3.3 V module or shift the level.

Remapped pins, throughput down. Going through GPIOs other than the native pins routes the signal through the internal matrix: the speed drops (~40 MHz instead of 80). For maximum performance, keep the default VSPI pins.

Ground not tied. Like every bus, the SPI demands a common ground between the ESP32 and the peripheral.

Exercises

Exercise 1 — A clocked datalogger

Take the SD card again and record a measurement every second, without freezing the program. Hint: clock on millis(), open/write/close on every reading.

Exercise 2 — Two peripherals on the bus

Add a second SPI module (a display, say) beside the SD card. What do they share, and what tells them apart?

Special case — The second bus and the speed

The ESP32 offers a second SPI bus, HSPI, to separate two groups of peripherals or to avoid saturation. You instantiate it through SPIClass:

SPIClass hspi(HSPI);
hspi.begin(14, 12, 13, 15);   // SCK, MISO, MOSI, CS

The clock speed is set through SPISettings (often carried by the library). The native pins (VSPI: 18/19/23/5; HSPI: 14/12/13/15) allow up to 80 MHz. Remapped pins top out lower.

On the C3 / S3 variants, the organisation of the SPI buses and the default pins differ. Refer to the pinout through configuring the GPIO.

Where it fits in the project

  • Proof of concept — as soon as measurements have to be stored (datalogger) or richly displayed, the SPI is the right bus. The SD card example validates the recording in a few lines.
  • Integration and testing — logging to SD during the trials gives a trace you can exploit afterwards. The TFT display shows the state of the system on its own.

See also