An SPI (Serial Peripheral Interface) connection on an Arduino is a synchronous, full-duplex serial bus that uses four shared wires to communicate with peripherals at speeds up to 10 MHz on standard 8-bit AVR boards (and higher on 32-bit ARM/ESP32 targets). Unlike I2C, SPI relies on individual Chip Select (CS) lines for each target rather than software addresses, making it the go-to protocol for high-speed sensors, TFT displays, SD cards, and LoRa radio modules. If you need raw throughput and don't mind running extra wires, SPI is your best option.

The Physical Layer: Wiring an SPI Connection

SPI is a master-slave (or controller-peripheral) architecture. The Arduino acts as the controller, generating the clock and initiating all data transfers. Because SPI uses push-pull drivers rather than the open-drain architecture of I2C, you do not need pull-up resistors on the MOSI, MISO, or SCK lines. However, the Chip Select (CS) line requires careful handling.

Bench Tip: The Floating CS Trap
During the Arduino boot sequence, GPIO pins float before your setup() function runs. If a peripheral's CS line is floating, the device might interpret noise as a selection signal and drive its MISO pin, colliding with other SPI devices. Always wire a 10kΩ external pull-up resistor on every CS line to VCC, or ensure your code sets the pin to INPUT_PULLUP before switching it to OUTPUT.
SPI Bus Mechanics & Specifications
ParameterSpecificationNotes for Arduino
Wires Required4 shared + 1 CS per deviceMOSI, MISO, SCK are shared; CS is individual.
Max Speed10 MHz - 20 MHz+AVR (Uno/Mega) tops out around 8-10 MHz reliably; ESP32 can hit 40-80 MHz.
AddressingHardware CS linesNo software addresses; requires one GPIO per target.
Max Distance~30 cm (1 ft) at high speedCapacitance kills high-frequency edges. Drop to 1 MHz for 1-meter runs.
Data FlowFull-DuplexMaster sends and receives a byte simultaneously via shift registers.

SPI vs I2C vs UART: Picking the Right Protocol

Choosing the right protocol depends entirely on your constraints regarding distance, speed, and device count. Here is how SPI stacks up against the alternatives on the bench.

CriteriaSPII2CUART
Best ForHigh-speed data (Displays, ADCs, SD cards)Many low-speed sensors on limited pinsPoint-to-point long-distance / PC comms
SpeedVery High (10+ MHz)Low to Med (100 kHz - 3.4 MHz)Medium (115.2k - 1 Mbps typical)
Wiring ComplexityHigh (4 + N wires)Low (2 shared wires)Low (2 wires per pair)
Device CountLimited by available CS GPIO pinsUp to 127 (limited by address space)1-to-1 (without multiplexers)
Distance LimitShort (< 1 meter)Short (< 1 meter)Long (RS-485 can go 1km+)

Minimal Working SPI Exchange

Let's wire up a classic SPI peripheral: the MCP3008 10-bit ADC. This chip reads analog voltages and sends the digital value back over SPI. We will read Channel 0.

Wiring Map (Arduino Uno to MCP3008)

  • VDD & VREF → 5V
  • AGND & DGND → GND
  • CLK → Arduino Pin 13 (SCK)
  • DOUT → Arduino Pin 12 (MISO)
  • DIN → Arduino Pin 11 (MOSI)
  • CS/SHDN → Arduino Pin 10 (CS) (Add 10kΩ pull-up to 5V)

Complete Arduino Code

#include <SPI.h>

const int CS_PIN = 10;

void setup() {
  Serial.begin(115200);
  
  // Prevent MISO collisions during boot
  pinMode(CS_PIN, INPUT_PULLUP); 
  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH); // Deselect MCP3008
  
  SPI.begin();
}

int readMCP3008(byte channel) {
  if (channel > 7) return -1;
  
  // MCP3008 max clock is ~3.6MHz at 5V
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
  digitalWrite(CS_PIN, LOW);
  
  SPI.transfer(0x01); // Start bit
  // Send Single-ended mode + Channel number, receive MSB
  byte msb = SPI.transfer(0x80 | (channel << 4)); 
  // Send dummy byte, receive LSB
  byte lsb = SPI.transfer(0x00); 
  
  digitalWrite(CS_PIN, HIGH);
  SPI.endTransaction();
  
  // Combine 10-bit result
  return ((msb & 0x03) << 8) | lsb;
}

void loop() {
  int sensorValue = readMCP3008(0);
  Serial.print("CH0 Raw: ");
  Serial.println(sensorValue);
  delay(250);
}

Sniffing the Bus and Classic Failures

When an SPI connection fails, it rarely fails silently. It usually returns garbage data (like 0xFF or 0x00) or hangs the microcontroller. Here are the classic failures and how to debug them.

1. The CPOL/CPHA Mismatch (SPI Modes)

SPI defines four clock modes (Mode 0 through 3) based on Clock Polarity (CPOL) and Clock Phase (CPHA). If your Arduino is set to SPI_MODE0 but the sensor expects SPI_MODE3, the data will be sampled on the wrong clock edge, resulting in shifted or completely corrupted bytes. Fix: Check the peripheral's datasheet for the required SPI mode and update SPISettings accordingly.

2. MISO Bus Contention (CS Clash)

If you have multiple SPI devices on the same bus and two CS lines accidentally go LOW at the same time, both devices will try to drive the MISO line. This causes a short circuit between their internal push-pull drivers, corrupting data and potentially overheating the chips. Fix: Verify your CS pin logic and ensure no CS pins are left floating.

3. Logic Level Mismatch

Connecting a 5V Arduino Uno directly to a 3.3V SPI sensor (like an RFM95 LoRa module or an ADXL345) without a logic level shifter will fry the sensor's MISO/CS inputs over time. Fix: Use a bidirectional logic level converter (like the Texas Instruments TXB0104 or a simple BSS138 MOSFET-based module) between the 5V and 3.3V domains.

How to Sniff the Bus
Stop guessing and use a logic analyzer. A basic $15 8-channel 24MHz USB logic analyzer running PulseView (Sigrok) will decode SPI packets instantly. Connect CH0 to SCK, CH1 to MOSI, CH2 to MISO, and CH3 to CS. Set the decoder to SPI, and you will see the exact hex bytes flying across the wire, making baud and mode mismatches obvious.

Frequently Asked Questions

Can I daisy-chain multiple devices on an Arduino SPI connection?

Generally, no. Unlike I2C where devices share two wires, standard SPI requires a dedicated Chip Select (CS) wire from the Arduino to every single peripheral. However, some specific chips (like WS2812 LEDs or certain shift registers like the 74HC595) support a 'daisy-chain' topology where the MISO of one chip feeds the MOSI of the next, allowing you to control dozens of chips with a single CS line. For standard sensors, use a multiplexer or a GPIO expander if you run out of CS pins.

Why does my Arduino SPI connection return all 0xFF or 0x00?

If you read 0xFF, your MISO line is likely floating (pulled high by internal leakage or a pull-up resistor) because the peripheral is not responding or is not selected. If you read 0x00, the MISO line is being pulled to ground, or the peripheral is actively driving zeros because it is in a reset state. Check your CS wiring, verify the peripheral has power, and confirm you are using the correct SPI Mode (CPOL/CPHA).

What is the maximum wire length for an Arduino SPI connection?

SPI is designed for on-board communication, not long-distance runs. At 10 MHz, signal degradation and parasitic capacitance will corrupt data after about 30 cm (12 inches) of standard jumper wire. If you must run SPI over a longer distance (e.g., 1 to 2 meters), you must drastically reduce the clock speed to 1 MHz or lower, use twisted-pair cables, and terminate the lines properly. For distances beyond 2 meters, switch to RS-485 or CAN bus.

Do I need pull-up resistors for an Arduino SPI connection?

This is a common point of confusion with I2C. No, you do not need pull-up resistors on the MOSI, MISO, or SCK lines. SPI uses push-pull drivers that actively drive the lines HIGH and LOW. However, you do need a pull-up resistor (typically 10kΩ) on every Chip Select (CS) line to keep the peripheral deselected while the Arduino boots up and configures its GPIO pins.

For deeper dives into protocol timing, refer to the official Arduino SPI Reference or the comprehensive SparkFun SPI Tutorial.