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:
| Signal | Direction | Description |
|---|---|---|
| MOSI | Master → Slave | Master Out, Slave In — data from master to slave |
| MISO | Slave → Master | Master In, Slave Out — data from slave to master |
| SCK | Master → Slave | Clock signal generated by master |
| CS/NSS | Master → Slave | Chip 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.
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:
| Parameter | Description |
|---|---|
| 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 |
| Mode | CPOL | CPHA | Description |
|---|---|---|---|
| Mode 0 | 0 | 0 | Clock idle LOW, sample on rising edge |
| Mode 1 | 0 | 1 | Clock idle LOW, sample on falling edge |
| Mode 2 | 1 | 0 | Clock idle HIGH, sample on falling edge |
| Mode 3 | 1 | 1 | Clock 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:
| Bus | Name | Default MOSI | Default MISO | Default CLK | Default CS |
|---|---|---|---|---|---|
| SPI2 | HSPI | GPIO13 | GPIO12 | GPIO14 | GPIO15 |
| SPI3 | VSPI | GPIO23 | GPIO19 | GPIO18 | GPIO5 |
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
#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
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
SPISettings(speed, bitOrder, dataMode)
| Parameter | Description | Example |
|---|---|---|
speed | Clock frequency in Hz | 1000000 = 1 MHz |
bitOrder | Bit transmission order | MSBFIRST or LSBFIRST |
dataMode | SPI mode (CPOL/CPHA) | SPI_MODE0, SPI_MODE1, SPI_MODE2, SPI_MODE3 |
Multiple Byte Transfer (Buffer)
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():
// 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:
#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
| Feature | SPI | I2C |
|---|---|---|
| Speed | Up to 80 MHz on ESP32 | 100 kHz / 400 kHz / 3.4 MHz |
| Wires | 4 (MOSI, MISO, SCK, CS per device) | 2 (SDA, SCL) |
| Duplex | Full-duplex | Half-duplex |
| Multi-device | One CS per device | Up to 127 devices (7-bit address) |
| Distance | Short (PCB-level) | Short (PCB-level, up to ~1 m) |
| Best for | Displays, flash, high-speed ADC | Sensors, 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
SPISettingswithbeginTransaction/endTransactionfor 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.