The SPI (Serial Peripheral Interface) bus on an Arduino uses four shared wires (MOSI, MISO, SCK) plus individual Chip Select (CS) lines to achieve full-duplex communication at speeds up to 8 MHz on 8-bit AVR boards, and significantly higher on 32-bit architectures. It is the undisputed default choice for high-throughput peripherals like TFT displays, SD cards, and external flash memory. If you need to move bulk data quickly over a short distance on your workbench, SPI is the protocol you want.

The SPI Bus on Arduino: Physical Layer and Mechanics

Unlike I2C, which relies on a shared address space and open-drain lines, SPI is a push-pull, master-slave (or controller-peripheral) architecture. The master generates the clock and dictates the data flow, while peripherals only speak when their specific CS line is pulled low. This physical layer design eliminates the software overhead of address polling, allowing for raw speed.

Table 1: SPI Bus Mechanics and Specifications
Parameter SPI Specification Arduino AVR (Uno/Nano) Reality
Wires Required 3 shared (MOSI, MISO, SCK) + 1 CS per device Pins 11, 12, 13 + any digital pin for CS
Max Speed Up to 100+ MHz (silicon dependent) 8 MHz (half of 16MHz system clock)
Addressing None (Hardware CS routing) Manual GPIO management for each CS pin
Max Distance ~1 meter at low speeds < 30cm recommended for >1MHz signals
Duplex Mode Full-duplex (simultaneous TX/RX) Shift register exchanges byte simultaneously

Wiring the SPI Bus: Pins, Level Shifting, and Pull-Up Confusion

A common trap for beginners is assuming the digital pins 11, 12, and 13 are the only way to access the hardware SPI bus on an ATmega328P. In reality, the hardware SPI bus is physically tied to the ICSP header (the 2x3 pin block near the microcontroller). On the Uno, pins 11-13 are simply routed to the ICSP header. If you are using an Arduino Mega or a 32-bit board like the ESP32, the pin mappings change entirely, but the ICSP/SPI peripheral logic remains the same.

Bench Tip: The 3.3V vs 5V Logic Trap
Most modern SPI sensors (BME280, ADXL345, W25Q128 Flash) are strictly 3.3V logic. Feeding 5V from an Arduino Uno's MOSI or SCK pins directly into a 3.3V sensor will degrade the silicon over time or instantly brick it. Always use a bidirectional logic level shifter (like the BSS138 MOSFET-based modules or a CD4050 buffer) when mixing 5V AVRs with 3.3V SPI peripherals.

The 'Missing Pull-Up' Confusion

If you are coming from I2C, you are used to soldering 4.7kΩ pull-up resistors to SDA and SCL. Standard SPI does not use pull-up resistors on MOSI, MISO, or SCK. SPI uses push-pull outputs that actively drive the lines high and low. Adding pull-ups to SPI data lines will cause signal ringing, slow down edge transitions, and lead to corrupted data at high baud rates. The only exception is the CS (Chip Select) line, which some designers pull high via a 10kΩ resistor to prevent accidental peripheral activation during microcontroller boot-up, though managing this in software via pinMode(CS, OUTPUT) and digitalWrite(CS, HIGH) in setup() is the standard practice.

Minimal Working Exchange: Reading an SPI Sensor

Below is a minimal, robust exchange to read the WHO_AM_I register (0x00) from an ADXL345 accelerometer over SPI. This code uses the modern SPI.beginTransaction() method, which safely handles SPI settings without breaking other libraries sharing the bus.

Table 2: ADXL345 to Arduino Uno SPI Wiring
ADXL345 PinArduino Uno PinNotes
VCC3.3VDo not use 5V
GNDGNDCommon ground required
CSDigital 10Active LOW
SCKDigital 13 (or ICSP SCK)Clock
SDO (MISO)Digital 12 (or ICSP MISO)Data Out from sensor
SDA (MOSI)Digital 11 (or ICSP MOSI)Data In to sensor
#include <SPI.h>

const int CS_PIN = 10;

void setup() {
  Serial.begin(115200);
  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH); // Deselect sensor
  
  SPI.begin();
  
  // Read WHO_AM_I register (0x00) to verify connection
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE3));
  digitalWrite(CS_PIN, LOW);
  
  // Send register address with read bit (0x80) set
  SPI.transfer(0x80 | 0x00); 
  byte whoami = SPI.transfer(0x00); // Clock out the data
  
  digitalWrite(CS_PIN, HIGH);
  SPI.endTransaction();
  
  Serial.print("WHO_AM_I Register: 0x");
  Serial.println(whoami, HEX); // Should print 0xE5 for ADXL345
}

void loop() {
  // Main application logic here
}

Debugging the SPI Bus: Sniffing and Classic Failures

When your SPI bus returns 0xFF or 0x00 for every byte, guessing is a waste of time. You need to look at the physical layer. The best tool for this is a USB Logic Analyzer. A $15 24MHz 8-channel clone (based on the Cypress CY7C68013A chip) running the open-source PulseView / Sigrok software is all you need to decode SPI frames on your bench.

Connect the logic analyzer probes to SCK, MOSI, MISO, and CS, set the ground clip to the Arduino GND, and trigger on the falling edge of the CS pin. Here are the classic failures you will spot in the trace:

  1. SPI Mode Mismatch (CPOL/CPHA): This is the #1 SPI killer. SPI has four modes (0, 1, 2, 3) defining clock polarity and phase. If the sensor datasheet specifies Mode 3 (Clock idle HIGH, sample on falling edge) and your Arduino code defaults to Mode 0, the data will be shifted by one bit, resulting in garbage. Always check the datasheet and set SPI_MODE3 explicitly in SPISettings.
  2. Baud Rate Too High: Long Dupont jumper wires act as antennas and add parasitic capacitance. An 8MHz clock signal will ring and degrade into a sawtooth wave over 20cm of cheap wire. If your logic analyzer shows rounded, messy clock edges, drop your SPISettings speed to 1MHz or 500kHz and use shorter, twisted-pair wiring.
  3. Floating or Unmanaged CS Line: If you forget pinMode(CS, OUTPUT), the Arduino leaves the pin in a high-impedance state. The peripheral might randomly wake up and drive the MISO line, colliding with other SPI devices and locking up the bus.
  4. The I2C Pull-Up Hangover: As mentioned earlier, if you wired 4.7kΩ pull-ups to MISO/MOSI because you confused the protocol with I2C, the logic analyzer will show slow rise times and failure to reach the VCC threshold at high speeds. Remove them.

Protocol Decision Tree: When to Use SPI vs I2C vs UART

Choosing the right communication protocol prevents architectural dead-ends later in your project. Use this decision matrix to lock in your physical layer.

Table 3: Embedded Protocol Decision Tree
Requirement / Constraint Protocol Pick Why It Wins Here
Need to drive a TFT display, camera, or SD card (>1 Mbps) SPI Full-duplex, high clock speeds, no addressing overhead.
Need to connect 10+ low-speed sensors (temp, humidity) on 2 wires I2C Shared bus, built-in addressing, only requires SDA/SCL.
Need to communicate over meters of wire or to a PC/Cellular modem UART (RS485) Asynchronous, long-distance capable with differential drivers.
Need multi-master arbitration (two microcontrollers sharing sensors) I2C Hardware collision detection and arbitration built into the spec.
The Default Recommendation
If your project involves a microcontroller talking to a high-bandwidth peripheral (screen, flash, SD) over a distance of less than 30cm, pick SPI. If you are wiring up a cluster of environmental sensors (BME680, SHT40) on a custom PCB where trace routing space is tight, pick I2C. Never use SPI for long-distance inter-board communication; the clock skew and ground bounce will corrupt your data. For further reading on bus specifications, refer to the official Arduino SPI Reference and the SparkFun SPI Tutorial.