SPI (Serial Peripheral Interface) is the high-speed workhorse of embedded systems. If you need to move bulk data—like reading an SD card, driving a TFT display, or streaming from an ADC—SPI for Arduino is your default choice. It pushes clock speeds up to 8 MHz on 8-bit boards (and 24 MHz+ on 32-bit architectures) over short distances, trading wire count for raw throughput. Unlike I2C, SPI lacks a formal software addressing scheme, relying instead on individual chip select lines for every target device.
This guide skips the abstract theory and goes straight to the bench: physical layer requirements, exact pin mappings, modern transaction-safe code, and how to debug the bus when your logic analyzer shows garbage.
SPI Bus Mechanics and Physical Layer Requirements
SPI operates on a master-slave (or controller-peripheral) architecture using a synchronous, full-duplex four-wire bus. The master generates the clock and initiates all transfers, while data shifts simultaneously in both directions via a ring-buffer shift register mechanism.
| Signal | Direction (Master) | Function | Max Speed (Uno/Nano) | Physical Limits |
|---|---|---|---|---|
| SCK (Clock) | Output | Serial clock generated by master | 8 MHz (System/2) | Keep traces < 30cm at >4MHz |
| MOSI / COPI | Output | Master Out, Slave In (Controller Out) | 8 MHz | Series 33Ω resistor recommended |
| MISO / CIPO | Input | Master In, Slave Out (Controller In) | 8 MHz | Requires pull-up if slave tri-states |
| CS / SS | Output | Chip Select (Active LOW) | N/A (GPIO speed) | Must have 10kΩ pull-up to VCC |
setup() runs. Without a pull-up, a floating CS pin can accidentally enable the slave, causing it to drive the MISO line and create a bus collision with other peripherals. Additionally, if your slave device tri-states its MISO pin when deselected, add a 10kΩ pull-up on the MISO line to prevent the master's input from floating and triggering spurious interrupts.
Voltage Translation: 5V to 3.3V
Connecting a 5V Arduino Uno directly to a 3.3V SPI sensor (like the BMP280 or W25Q128 flash) will fry the slave's silicon. You must use a logic level shifter. Avoid the TXB0108E for SPI; its internal edge-rate acceleration circuitry often misinterprets the bidirectional MISO line and causes oscillation. Instead, use a CD4050B non-inverting buffer for the unidirectional lines (MOSI, SCK, CS) and a dedicated bidirectional MOSFET shifter (like the BSS138) for MISO, or simply use a 3.3V Arduino (Nano 33 IoT, ESP32) to bypass the problem entirely.
Protocol Selection: SPI vs I2C vs UART
Choosing the right protocol depends entirely on your constraints regarding distance, speed, and device count. Here is the decision matrix for embedded bus selection.
| Criteria | SPI | I2C | UART |
|---|---|---|---|
| Wire Count | 4 + 1 per extra device | 2 (shared bus) | 2 (point-to-point) |
| Max Speed | 10 MHz - 50+ MHz | 100 kHz / 400 kHz / 3.4 MHz | 115,200 baud (typical) |
| Addressing | Hardware CS lines (no software address) | 7-bit or 10-bit software address | None (point-to-point) |
| Max Devices | Limited by GPIO pins for CS | Up to 127 (theoretical) | 1 to 1 |
| Distance | < 1 meter (degrades > 10MHz) | < 1 meter (capacitance limited) | 15+ meters (RS-485 adapted) |
| Best Use Case | High-speed bulk data (Displays, SD, ADC) | Low-speed config/sensors (Temp, IMU) | GPS, PC comms, long-distance |
Choose SPI when: You need to push pixels to a TFT screen, log high-frequency ADC data, or read/write to an SD card where I2C's 400 kHz ceiling would bottleneck your application.
Choose I2C when: You are wiring multiple low-bandwidth environmental sensors on a cramped PCB and want to save GPIO pins.
Choose UART when: You are communicating with a GPS module, a cellular modem, or bridging to a PC.
Minimal Working Exchange: Hardware Map and Code
Modern Arduino SPI code must use the SPI.beginTransaction() and SPI.endTransaction() methods. Older tutorials relying on SPI.setClockDivider() are deprecated and will cause catastrophic bus collisions if an interrupt service routine (ISR) uses SPI in the background.
Pin Mapping (Arduino Uno / Nano ATmega328P)
- SCK: Pin 13
- MOSI (COPI): Pin 11
- MISO (CIPO): Pin 12
- CS (SS): Pin 10 (Must be set as OUTPUT to keep the ATmega in Master mode, even if you use a different pin for the actual CS signal).
Fail-Safe Master Code
This example demonstrates a robust byte exchange with a generic SPI peripheral (like a digital potentiometer or DAC). It includes the critical CS initialization sequence to prevent boot-time ghosting.
#include <SPI.h>
const int CS_PIN = 10;
// Define SPI settings: 1MHz clock, MSB first, SPI Mode 0
SPISettings mySettings(1000000, MSBFIRST, SPI_MODE0);
void setup() {
Serial.begin(115200);
// CRITICAL: Set CS HIGH before SPI.begin() to prevent ghost-selecting
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH);
// Pin 10 must be OUTPUT on Uno/Nano to force Master hardware mode
pinMode(10, OUTPUT);
SPI.begin();
Serial.println("SPI Master initialized.");
}
void loop() {
uint8_t commandByte = 0x11; // Example command
uint8_t returnByte = 0x00;
// Acquire the bus with specific settings (prevents ISR conflicts)
SPI.beginTransaction(mySettings);
digitalWrite(CS_PIN, LOW); // Select slave
// Full duplex exchange: send command, read response simultaneously
SPI.transfer(commandByte);
returnByte = SPI.transfer(0x00); // Send dummy byte to clock in response
digitalWrite(CS_PIN, HIGH); // Deselect slave
SPI.endTransaction(); // Release the bus
Serial.print("Received: 0x");
Serial.println(returnByte, HEX);
delay(1000);
}
Debugging the Bus: Classic Failures and Sniffing
When your SPI device returns 0xFF, 0x00, or pure garbage, the issue is almost always at the physical layer or the clock phase. Here is how to diagnose the classic failures.
1. Clock Polarity and Phase Mismatch (CPOL / CPHA)
SPI does not have a single standard for clock idle state and data sampling edges. This is defined by SPI Modes 0 through 3. If your MISO data looks shifted by one bit, you are likely using the wrong mode.
- Mode 0 (SPI_MODE0): Clock idles LOW, data sampled on rising edge. (Most common).
- Mode 3 (SPI_MODE3): Clock idles HIGH, data sampled on rising edge. (Common in Microchip ADCs and SD cards).
SPISettings to SPI_MODE3.
2. The Baud Rate Mismatch
Unlike UART, SPI is synchronous, so the slave doesn't have an internal baud rate generator. However, slaves have a maximum clock frequency. If you initialize SPI.beginTransaction(SPISettings(8000000, ...)) but your sensor (e.g., a MAX31855 thermocouple amp) maxes out at 4 MHz, the slave's internal shift register will fail to keep up, returning corrupted bits. Always start debugging at 100 kHz, verify the data, and then step up to the datasheet's maximum.
3. Sniffing the Bus with a Logic Analyzer
A multimeter is useless for SPI debugging; you need to see the timing. Use a 24 MHz Saleae-compatible logic analyzer (available for ~$12 on Amazon) and the free, open-source PulseView (sigrok) software.
- Connect CH0 to CS, CH1 to SCK, CH2 to MOSI, and CH3 to MISO. Do not forget the ground wire.
- Set the sample rate to at least 4x your SPI clock speed (e.g., 10 MHz sample rate for a 2 MHz SPI clock).
- Set the trigger to the falling edge of the CS line.
- Add the SPI decoder in PulseView, map the pins, and set the correct CPOL/CPHA.
If the decoded MOSI hex matches your code but MISO decodes as 0xFF, your MISO wire is broken, the slave is unpowered, or the slave requires a specific wake-up command before it will drive the MISO line.






