The hardware is ready, the protocol is understood. Now it's time to flex the ESP32's silicon muscles.
Using SPI on ESP32 is not like using an Arduino Uno. Factors like IO Matrix (Pin Remapping), DMA (Direct Memory Access), and Dual Core come into play.
There are 4 hardware SPI units inside the ESP32:
- SPI0: Used for Internal Flash memory. (Do not touch!)
- SPI1: Used for Internal Flash memory. (Do not touch!)
- SPI2 (HSPI): Open to the user.
- SPI3 (VSPI): Open to the user.
Confusion: In the Arduino world, the default SPI object usually uses the VSPI (SPI3) block. If you want to open a second SPI line, you use HSPI (SPI2). The letters "V" or "H" at the beginning have no effect on speed; they are just naming conventions.
| Pin Name | VSPI (Default) | HSPI (Second Port) |
|---|---|---|
| SCK | GPIO 18 | GPIO 14 |
| MISO | GPIO 19 | GPIO 12 |
| MOSI | GPIO 23 | GPIO 13 |
| CS | GPIO 5 | GPIO 15 |
ESP32's greatest power is its GPIO Matrix structure. The table above is just the "default". You can route SPI signals (SCK, MOSI, MISO) to almost any GPIO pin.
#include <SPI.h>
// Let's define our own pins
#define MY_SCK 25
#define MY_MISO 26
#define MY_MOSI 27
#define MY_CS 32
SPIClass mySPI(VSPI); // Use VSPI hardware block
void setup() {
// begin(SCK, MISO, MOSI, CS);
mySPI.begin(MY_SCK, MY_MISO, MY_MOSI, MY_CS);
pinMode(MY_CS, OUTPUT);
}
⚠️ Field Note: Do not use "Input only" pins (GPIO 34, 35, 36, 39) for anything other than MISO!
What happens if there are two sensors on the same bus, one running at 1 MHz Mode 0 and the other at 10 MHz Mode 3?
In the old way, you would change settings globally every time. This is dangerous. ESP32 uses the Transaction structure. This means: "Load settings right before sending data, remove protection when done."
This function does 3 things:
- Sets the SPI bus speed, Bit order (MSB/LSB), and Mode (CPOL/CPHA).
- Prevents other tasks (RTOS tasks) from interrupting and messing up settings.
- Creates a critical "Atomic" operation area.
graph TD
A[Start] --> B{Lock Bus<br>beginTransaction}
B --> C[Set CS Pin LOW]
C --> D[Data Transfer<br>transfer/transfer16]
D --> E[Set CS Pin HIGH]
E --> F{Release Bus<br>endTransaction}
F --> G[Finish]
style B fill:#fff9c4,stroke:#fbc02d
style F fill:#fff9c4,stroke:#fbc02d
style D fill:#e3f2fd,stroke:#1565c0
Does the processor (CPU) have to wait for every byte when driving a TFT screen or writing large data to an SD card? No.
DMA bypasses the CPU, takes data from RAM, and pumps it directly to the SPI block.
- CPU: "DMA, push this image sequence to the screen, I'll read sensors in the meantime."
- Result: CPU load decreases, SPI speed hits maximum.
Library Recommendation: The standard
SPI.hlibrary has partial DMA support. For high-performance displays, use DMA-optimized libraries like TFT_eSPI or LovyanGFX.
The following code is a secure and standard-compliant SPI read/write function.
#include <SPI.h>
const int PIN_CS = 5;
// Define settings as an object (1 MHz, MSB First, MODE 0)
SPISettings sensorSettings(1000000, MSBFIRST, SPI_MODE0);
void setup() {
Serial.begin(115200);
// Start SPI (Default Pins)
SPI.begin();
// CS pin is managed manually
pinMode(PIN_CS, OUTPUT);
digitalWrite(PIN_CS, HIGH); // Passive at start
}
void loop() {
readSensor();
delay(100);
}
void readSensor() {
// STEP 1: Apply settings and protect the bus
SPI.beginTransaction(sensorSettings);
// STEP 2: Select Slave
digitalWrite(PIN_CS, LOW);
// STEP 3: Data Exchange (Send and Receive simultaneously)
// Send "0x01" command to sensor, receive the response
byte response = SPI.transfer(0x01);
// STEP 4: Deselect Slave
digitalWrite(PIN_CS, HIGH);
// STEP 5: Release the bus (For other devices)
SPI.endTransaction();
Serial.print("Received Data: ");
Serial.println(response, HEX);
}- Pin Freedom: You can use any pin with
SPI.begin(sck, miso, mosi, cs). - Transaction Essential: Never drive SPI without
beginTransactionandendTransaction. The system will crash, especially with multiple slaves. - Speed Limit: Lower the speed if the cable is long (via
SPISettings). 10MHz is good for 10cm cable, drop to 1MHz for 50cm cable.