Serial Peripheral Interface (SPI) is the high-speed, full-duplex workhorse of the Arduino ecosystem. When you need to push color data to an ILI9341 TFT display, read from a high-speed ADC, or interface with an SD card, I2C and UART simply lack the bandwidth. SPI solves this by using separate lines for sending and receiving data simultaneously, routinely hitting 8 MHz to 20 MHz on standard 5V AVR boards, and up to 80 MHz on specialized ESP32 peripherals.
Unlike I2C, SPI does not use software addressing. It relies on individual hardware Chip Select (CS) lines for every target device, which means wiring complexity scales linearly with device count. This guide breaks down the physical layer, exact pinouts, and the specific failure modes that cause 90% of SPI headaches on the bench.
SPI Bus Mechanics and Protocol Limits
To decide if SPI is the right tool for your build, you have to look at the physical constraints. SPI trades wiring simplicity for raw speed. Below is a data-dense comparison of the serial protocols you will encounter in embedded design.
| Feature | SPI | I2C | UART | CAN Bus |
|---|---|---|---|---|
| Wires Required | 4 (MOSI, MISO, SCK, CS) | 2 (SDA, SCL) | 2 (TX, RX) | 2 (CANH, CANL) |
| Max Speed (Typical) | 10 - 20 MHz (AVR) 80 MHz (ESP32) |
100 kHz / 400 kHz (Fast-mode+ 1 MHz) |
115,200 bps (up to 2-3 Mbps) |
1 Mbps (standard) 5 Mbps (CAN-FD) |
| Addressing | None (Hardware CS lines) | 7-bit or 10-bit I2C address | None (Point-to-point) | 11-bit or 29-bit ID |
| Topology | Master-Slave (Bus/Daisy-chain) | Multi-Master Bus | Point-to-Point | Multi-Master Bus |
| Max Practical Distance | ~30 cm (1 ft) without differential drivers | ~30 cm (1 ft) at 400kHz ~1m at 100kHz |
~15 m (50 ft) at 9600 bps | ~40 m at 1 Mbps ~1 km at 50 kbps |
| Duplex | Full-Duplex | Half-Duplex | Full-Duplex | Half-Duplex |
The Verdict on Protocol Fit: Choose SPI when you need to move large blocks of data (like screen framebuffers or audio samples) over short distances on a single PCB or tightly coupled breadboard. Choose I2C when you have many low-speed sensors (temperature, IMUs) and want to save GPIO pins. Choose UART for GPS modules or PC serial consoles, and CAN for automotive or long-distance noisy environments.
Hardware Pinouts and Physical Wiring Rules
While you can 'bit-bang' SPI on any GPIO pins using software libraries, you should always use the hardware SPI peripheral built into the microcontroller. It handles the clock shifting in hardware, freeing the CPU to do actual work. However, hardware SPI pins are fixed on most AVR boards.
| Board | MOSI (COPI) | MISO (CIPO) | SCK (SCLK) | SS (Default CS) |
|---|---|---|---|---|
| Arduino Uno / Nano (ATmega328P) | Pin 11 | Pin 12 | Pin 13 | Pin 10 |
| Arduino Mega 2560 | Pin 51 | Pin 50 | Pin 52 | Pin 53 |
| ESP32 DevKit (VSPI) | GPIO 23 | GPIO 19 | GPIO 18 | GPIO 5 |
| ESP32 DevKit (HSPI) | GPIO 13 | GPIO 12 | GPIO 14 | GPIO 15 |
| Raspberry Pi Pico (RP2040 SPI0) | GPIO 19 (TX) | GPIO 16 (RX) | GPIO 18 (SCK) | Any GPIO (Software) |
A common point of confusion is pull-up resistors. SPI data and clock lines (MOSI, MISO, SCK) do not require pull-up resistors. They are actively driven push-pull lines. However, the Chip Select (CS) line absolutely requires a 10kΩ pull-up resistor to VCC on the slave device. When your Arduino reboots or uploads code, its GPIO pins float. If the CS line floats low during this boot sequence, the SPI slave (like an SD card or display) will wake up, see garbage data on the MOSI line, and enter an undefined or locked state. A 10kΩ pull-up keeps the slave deaf until your code explicitly initializes the CS pin as an OUTPUT and drives it HIGH.
Minimal Working Exchange: Wiring and Code
Let's wire an Arduino Uno to a standard SPI peripheral (like an MCP3008 ADC or a 74HC595 shift register) and execute a minimal, safe transaction. The modern Arduino SPI library uses the SPISettings object to configure the bus safely, preventing conflicts if multiple libraries (like an SD card and a Display) share the same hardware SPI bus.
Wiring Checklist:
- Uno Pin 13 (SCK) → Slave SCK
- Uno Pin 11 (MOSI) → Slave MOSI / DIN
- Uno Pin 12 (MISO) → Slave MISO / DOUT
- Uno Pin 10 (CS) → Slave CS / SS (Add 10kΩ pull-up to 5V on this line)
- Uno 5V → Slave VCC
- Uno GND → Slave GND
#include <SPI.h>
// Define the Chip Select pin.
// On AVR boards, pin 10 MUST be set as OUTPUT even if you use another pin for CS,
// otherwise the hardware SPI controller drops into Slave mode.
const int CS_PIN = 10;
void setup() {
Serial.begin(115200);
// Critical: Set hardware SS pin as output to force Master mode on ATmega328P
pinMode(10, OUTPUT);
digitalWrite(10, HIGH); // Deselect slave immediately
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH);
// Initialize the SPI bus
SPI.begin();
}
void loop() {
// Configure bus: 4MHz clock, Most Significant Bit first, SPI Mode 0
SPISettings mySettings(4000000, MSBFIRST, SPI_MODE0);
// 1. Claim the bus and apply settings
SPI.beginTransaction(mySettings);
// 2. Select the slave (Active LOW)
digitalWrite(CS_PIN, LOW);
// 3. Perform the exchange.
// SPI is full duplex: sending a byte simultaneously receives a byte.
uint8_t command = 0x01; // Example start bit for MCP3008
uint8_t response1 = SPI.transfer(command);
uint8_t response2 = SPI.transfer(0x80); // Send dummy byte to clock in data
uint8_t response3 = SPI.transfer(0x00); // Clock in final bits
// 4. Deselect the slave
digitalWrite(CS_PIN, HIGH);
// 5. Release the bus so other libraries can use it
SPI.endTransaction();
// Process the 10-bit ADC result (example logic)
int adcValue = ((response2 & 0x03) << 8) | response3;
Serial.print("ADC Reading: ");
Serial.println(adcValue);
delay(500);
}
Classic SPI Failures and How to Debug the Bus
Because SPI lacks the built-in acknowledgments of I2C, failures often result in silent garbage data rather than a hard crash. Here is how to diagnose the classic failure modes.
1. Baud Rate and Clock Mode Mismatch
Unlike UART, where a 'baud mismatch' means the bits are sampled at the wrong time, in SPI this manifests as a Clock Polarity (CPOL) and Clock Phase (CPHA) mismatch. SPI defines four modes (0, 1, 2, 3). Mode 0 (Clock idle LOW, sample on rising edge) and Mode 3 (Clock idle HIGH, sample on falling edge) account for 95% of devices. If your display shows a white screen or your SD card fails to initialize, check the datasheet. If the device expects Mode 3 and you send Mode 0, the slave will read the data bits exactly one half-clock-cycle off, resulting in completely corrupted bytes.
2. The Chip Select (CS) Clash
Since SPI has no addresses, you cannot get an 'address clash'. Instead, you get a CS clash. If you have an SD card on Pin 4 and a Display on Pin 10, and you forget to drive the SD card's CS pin HIGH before talking to the display, both chips will try to drive the MISO line simultaneously. This causes a short circuit on the data line, resulting in corrupted reads and potentially overheating the output drivers on the silicon. Always ensure all non-active CS pins are driven HIGH.
3. Sniffing and Debugging the Physical Layer
When Serial.print() isn't enough, you need to look at the physical wires. A standard multimeter is useless here because the clock transitions happen in microseconds.
- The Logic Analyzer: A $15 clone Saleae logic analyzer or a DSLogic Plus running the free PulseView / Sigrok software is mandatory for serious SPI debugging. Connect the probes to SCK, MOSI, MISO, and CS. Set the sample rate to at least 4x your SPI clock speed (e.g., 40 MS/s for a 10 MHz clock). PulseView has a built-in SPI decoder that will translate the hex bytes on the fly.
- The Oscilloscope: Use a scope to check for signal integrity issues. If your SPI wires are longer than 10 cm, you will see 'ringing' (oscillations) on the SCK square waves due to parasitic inductance and capacitance. If the ringing crosses the logic threshold voltage, the slave will register multiple clock ticks for a single pulse. Fix this by adding a small series resistor (33Ω to 100Ω) on the MOSI and SCK lines near the master to dampen the reflections.
- Using a Second Arduino as a Sniffer: In a pinch, you can wire a second Arduino's MISO, MOSI, and SCK to the main bus, and wire the CS line to an interrupt pin. Write a sketch that triggers an SPI read on the falling edge of the CS pin. It won't capture high-speed (8MHz+) traffic reliably due to interrupt latency, but it works perfectly for debugging slow 100kHz sensor initialization sequences.
Mastering SPI on the Arduino platform comes down to respecting the physical layer. Keep your wires short, manage your Chip Select lines rigorously, and always verify your CPOL/CPHA mode against the silicon datasheet. When things go wrong, bypass the software abstractions and hook up a logic analyzer—the waveform never lies.






