The Serial Peripheral Interface (SPI) is the heavy lifter of embedded communication. When you need to move megabytes of sensor data, drive a TFT display, or write to an SD card, I2C chokes and UART lacks the synchronous clocking. SPI solves this with a dedicated, push-pull, full-duplex architecture. But because it relies on strict hardware timing rather than software addressing, misinterpreting SPI interface signals is the fastest way to brick a sensor or read endless streams of 0xFF. This primer strips away the abstract theory and focuses on the physical layer, the classic failure modes, and exactly how to wire and debug the bus.
The Four Core SPI Interface Signals
SPI is fundamentally a shift-register mechanism. The master shifts bits out on one wire while simultaneously shifting bits in on another. Here is the exact mechanical breakdown of the bus.
| Signal Name | Direction (Master Perspective) | Function & Physical Layer | Bus Mechanics |
|---|---|---|---|
| SCK (Serial Clock) | Output | Provides the timing edge for data shifting. Push-pull driven. | Speed: 1MHz to 50MHz+ (limited by trace capacitance and peripheral specs). |
| MOSI (Master Out Slave In) | Output | Data sent from the master to the peripheral. Push-pull driven. | Addressing: None. Routing is handled purely by hardware Chip Select lines. |
| MISO (Master In Slave Out) | Input | Data sent from the peripheral to the master. Push-pull driven. | Distance: <1 meter. High-speed edges degrade over long unshielded ribbon cables. |
| CS / SS (Chip Select) | Output | Active-LOW enable for a specific peripheral. Push-pull driven. | Topology: Point-to-point from Master to each Slave (star topology). |
Unlike I2C, SPI does not use software addressing. If you have three sensors on the bus, you must route three separate CS wires from your microcontroller. This is the primary trade-off: you gain massive bandwidth at the cost of GPIO pin real estate.
Physical Wiring, Pull-Ups, and the Classic Failures
Because SPI uses push-pull drivers (the pins actively drive both HIGH and LOW), you do not need pull-up resistors on SCK, MOSI, or MISO. Adding them will only increase rise/fall times and ruin high-frequency signal integrity. However, the physical layer has three classic failure modes that catch almost every hobbyist on their first build.
The Big Three Failure Modes
- The MISO/MOSI Swap (Address/Perspective Clash): Many breakout boards label their pins from the peripheral's perspective (e.g., labeling the data-in pin as MOSI, even though it should connect to the master's MOSI). If your logic analyzer shows SCK toggling but MISO reads flatline 0x00, swap your MOSI and MISO wires.
- Baud and Mode Mismatch (CPOL/CPHA): SPI has four "modes" based on Clock Polarity (CPOL) and Clock Phase (CPHA). If the master samples the MISO line on the falling edge, but the peripheral shifts data on the falling edge, you will read garbage. Always check the peripheral datasheet for "SPI Mode" (0, 1, 2, or 3). Mode 0 and Mode 3 cover 95% of modern sensors.
- The Ground Loop: SPI is single-ended. The receiver compares the signal voltage against its local ground. If your microcontroller and peripheral have a ground potential difference of more than a few hundred millivolts (common when powering a peripheral from a separate buck converter), the logic thresholds will fail. Always run a dedicated ground wire alongside your SPI ribbon.
Protocol Showdown: When to Pick SPI Over I2C or UART
Choosing a bus shouldn't be a guessing game. Use this decision matrix to terminate your protocol selection based on physical constraints.
| Condition / Constraint | Protocol Pick | Why It Wins Here |
|---|---|---|
| Distance > 5 meters | RS-485 / UART | Differential signaling rejects common-mode noise over long twisted pairs. |
| Device count > 10, Speed < 400kHz | I2C | Only requires 2 wires regardless of how many nodes are on the bus. |
| Speed > 1MHz, Distance < 1 meter | SPI | Push-pull drivers and dedicated clock allow clean edges at 20MHz+. |
| Need simultaneous Send/Receive | SPI | Full-duplex hardware shift registers process TX and RX on the same clock cycle. |
The Concrete Pick: If your project involves high-speed data acquisition (like an IMU, ADC, or SD card) on a single PCB or short breadboard jumpers, choose SPI. Specifically, default to the ESP32's Hardware SPI2 host pins (GPIO 18 for SCK, GPIO 19 for MISO, GPIO 23 for MOSI). Using these specific pins engages the ESP32's internal DMA (Direct Memory Access) matrix, offloading the byte-shifting from the CPU to the hardware peripheral and freeing up cycles for your main application loop.
Minimal Working Exchange: ESP32 to ADXL345 Accelerometer
Let's wire an ESP32 to an Analog Devices ADXL345 digital accelerometer. The ADXL345 requires SPI Mode 3 and a maximum clock speed of 5MHz for reliable reads.
Wiring Map
| ESP32 Pin (Hardware SPI2) | ADXL345 Breakout Pin | Notes |
|---|---|---|
| 3V3 | VCC | Do not use 5V; the ADXL345 I/O is strictly 3.3V tolerant. |
| GND | GND | Keep this wire short and direct. |
| GPIO 18 (SCK) | SCL | Sometimes labeled SCLK on breakouts. |
| GPIO 23 (MOSI) | SDA / SDI | Data INTO the accelerometer. |
| GPIO 19 (MISO) | SDO / ALT ADDRESS | Data OUT of the accelerometer. Tie to GND via 10k if using I2C, but here it's MISO. |
| GPIO 5 (CS) | CS | Add a 10kΩ pull-up to 3V3 on this line. |
Arduino / ESP32 C++ Code
This code initializes the bus, verifies the peripheral's WHO_AM_I register (a mandatory sanity check), and configures the device. It includes explicit error handling if the wiring is swapped.
#include <SPI.h>
// Pin definitions for ESP32 Hardware SPI2
#define ADXL_CS_PIN 5
#define ADXL_SCK_PIN 18
#define ADXL_MISO_PIN 19
#define ADXL_MOSI_PIN 23
// ADXL345 Registers
#define REG_DEVID 0x00
#define REG_POWER_CTL 0x2D
#define REG_DATAX0 0x32
// SPI Settings: 1MHz, MSB First, Mode 3 (CPOL=1, CPHA=1)
SPISettings adxlSettings(1000000, MSBFIRST, SPI_MODE3);
uint8_t writeRegister(uint8_t reg, uint8_t value) {
SPI.beginTransaction(adxlSettings);
digitalWrite(ADXL_CS_PIN, LOW);
SPI.transfer(reg & 0x7F); // Clear bit 7 for WRITE
SPI.transfer(value);
digitalWrite(ADXL_CS_PIN, HIGH);
SPI.endTransaction();
return 0;
}
uint8_t readRegister(uint8_t reg) {
uint8_t val;
SPI.beginTransaction(adxlSettings);
digitalWrite(ADXL_CS_PIN, LOW);
SPI.transfer(reg | 0x80); // Set bit 7 for READ
val = SPI.transfer(0x00); // Clock out the data
digitalWrite(ADXL_CS_PIN, HIGH);
SPI.endTransaction();
return val;
}
void setup() {
Serial.begin(115200);
pinMode(ADXL_CS_PIN, OUTPUT);
digitalWrite(ADXL_CS_PIN, HIGH); // Deselect immediately
// Initialize ESP32 hardware SPI with explicit pins
SPI.begin(ADXL_SCK_PIN, ADXL_MISO_PIN, ADXL_MOSI_PIN, ADXL_CS_PIN);
delay(100);
// Sanity check: Read the DEVID register. It MUST return 0xE5.
uint8_t devId = readRegister(REG_DEVID);
if (devId != 0xE5) {
Serial.printf("FATAL: SPI Wiring Error. Expected 0xE5, got 0x%02X. Check MOSI/MISO swap and CS pull-up.\n", devId);
while (1) { delay(1000); } // Halt execution
}
Serial.println("ADXL345 found on SPI bus. Enabling measurement mode.");
writeRegister(REG_POWER_CTL, 0x08); // Set Measure bit
}
void loop() {
// Read X-axis data (burst read would be faster, kept simple for primer)
uint8_t x_l = readRegister(REG_DATAX0);
uint8_t x_h = readRegister(REG_DATAX0 + 1);
int16_t x_raw = (x_h << 8) | x_l;
Serial.printf("X-Axis Raw: %d\n", x_raw);
delay(250);
}
Sniffing and Debugging the Bus When Things Go Silent
When your code compiles, the wiring matches the table, but the sensor still returns 0x00, a multimeter is useless. SPI operates at frequencies where a DMM's 2Hz sampling rate will only show an average voltage. You need a logic analyzer.
For bench debugging, a Saleae Logic 8 or a budget-friendly DSLogic U3Pro16 is mandatory. Here is the exact procedure to sniff the bus and isolate the fault:
- Probe all 5 lines: Connect your logic analyzer pods to SCK, MOSI, MISO, CS, and GND. Do not skip the GND pod; floating ground clips cause phantom clock edges.
- Set the Sampling Rate: Apply the Nyquist-plus-margin rule. Your sampling rate must be at least 4x your SPI clock speed. If your code sets SPI to 1MHz, set the logic analyzer to sample at 8MHz or higher to cleanly capture the rise/fall times.
- Trigger on CS: Set a complex trigger to capture on the falling edge of the CS channel. This ensures you only record the exact microseconds the bus is active, saving buffer memory.
- Decode and Verify: Enable the SPI protocol decoder in your software (Saleae Logic 2 or PulseView). Map the channels. If the decoder shows MOSI transmitting the correct register address, but MISO reads
0xFF, your peripheral is either unpowered, held in reset, or you are using the wrong SPI Mode (CPOL/CPHA). If MISO mirrors MOSI exactly, you likely have a short between the two traces on your breadboard.
Mastering SPI interface signals means respecting the physical layer. By enforcing pull-ups on chip selects, verifying SPI modes against the datasheet, and using a logic analyzer to validate the clock edges, you eliminate the guesswork and build embedded systems that survive outside the prototyping phase.






