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.
| 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.
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.
| ADXL345 Pin | Arduino Uno Pin | Notes |
|---|---|---|
| VCC | 3.3V | Do not use 5V |
| GND | GND | Common ground required |
| CS | Digital 10 | Active LOW |
| SCK | Digital 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:
- 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_MODE3explicitly inSPISettings. - 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
SPISettingsspeed to 1MHz or 500kHz and use shorter, twisted-pair wiring. - 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. - 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.
| 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. |
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.






