Serial Peripheral Interface (SPI) is the high-speed workhorse of embedded systems. When you need to move data faster than I2C can handle, or when you are interfacing with high-throughput peripherals like TFT displays, SD cards, or multi-channel ADCs, Arduino SPI is the protocol of choice. Unlike asynchronous protocols, SPI is synchronous and full-duplex, meaning the master and slave exchange data simultaneously on every clock pulse.
This guide skips the abstract theory and goes straight to the bench: physical layer realities, bus mechanics, exact wiring for a classic ADC, and how to use a logic analyzer to catch the clock-phase mismatches that plague most SPI builds.
The Physical Layer: Bus Mechanics and Wiring Realities
Before writing a single line of code, you must understand the physical constraints of the SPI bus. Unlike I2C, which uses open-drain lines requiring external pull-up resistors, SPI uses push-pull drivers. The master actively drives the clock and data lines high and low. This allows for much higher speeds but strictly limits bus length due to parasitic capacitance and signal ringing.
Do not put pull-up resistors on SCK, MOSI, or MISO. They are actively driven push-pull lines. However, you must place a 10kΩ pull-up resistor on the slave's Chip Select (CS) line to VCC. This prevents the slave from misinterpreting floating GPIO states as a "selected" signal while the Arduino master is booting up and its pins are high-impedance.
| Parameter | SPI Standard | Arduino Uno (ATmega328P) Reality | Practical Notes |
|---|---|---|---|
| Topology | Master-Slave (Multi-slave via CS) | Single Master, Multiple Slaves | Each slave requires a dedicated CS pin. |
| Clock Speed | Up to 50+ MHz (device dependent) | Max 8 MHz (F_CPU / 2) | Default SPI.beginTransaction often sets 4 MHz. |
| Addressing | Hardware lines (No software addressing) | Hardware CS pins | Saves bus overhead, but consumes MCU GPIOs. |
| Max Distance | ~1 meter at low speeds | < 30 cm at 8 MHz | Longer wires increase capacitance, rounding clock edges. |
| Data Lines | 4 (SCK, MOSI, MISO, CS) | Pins 13, 11, 12, plus any GPIO for CS | On ESP32, default SPI pins vary by board variant. |
Protocol Fit: When to Choose SPI Over I2C or UART
Choosing the right protocol comes down to balancing speed, wiring complexity, and distance. Here is the decision framework for your next build:
- Choose SPI when: You need high throughput (e.g., streaming audio, writing to an SD card, driving a 320x240 TFT display) and you have enough GPIO pins for individual Chip Select lines. Distance is strictly confined to the same PCB or a short ribbon cable.
- Choose I2C when: You are connecting multiple low-speed sensors (temperature, humidity, IMUs) and want to save GPIO pins. I2C only requires two shared wires (SDA, SCL) regardless of how many devices are on the bus, provided their addresses don't clash.
- Choose UART/RS-485 when: You need to communicate over long distances (meters to kilometers) or between entirely separate microcontroller boards where a shared ground and clock line are impractical.
For a deeper dive into the electrical differences between these buses, SparkFun's SPI tutorial provides excellent oscilloscope captures comparing the signal integrity of SPI versus I2C.
The Minimal Working Exchange: MCP3008 ADC Wiring and Code
Let's build a functional circuit. The MCP3008 is a classic 10-bit, 8-channel SPI ADC. It is frequently used to add analog inputs to digital-only boards like the ESP8266, or to expand the analog channels on an Arduino.
Physical Wiring Table
Wire the MCP3008 to an Arduino Uno (ATmega328P) as follows. Keep the jumper wires under 15 cm to prevent clock ringing.
| MCP3008 Pin | Function | Arduino Uno Pin | Notes |
|---|---|---|---|
| 16 (VDD) | Power | 5V | Add 100nF decoupling cap to GND |
| 15 (VREF) | Reference Voltage | 5V | Sets max analog read value |
| 14 (AGND) | Analog Ground | GND | Tie to DGND at chip |
| 9 (DGND) | Digital Ground | GND | - |
| 13 (CLK) | Clock (SCK) | Pin 13 | Hardware SPI Clock |
| 12 (DOUT) | Data Out (MISO) | Pin 12 | Master In, Slave Out |
| 11 (DIN) | Data In (MOSI) | Pin 11 | Master Out, Slave In |
| 10 (CS) | Chip Select | Pin 10 | Add 10k pull-up to 5V |
Complete Arduino SPI Code
This code uses the hardware SPI bus via <SPI.h>. It configures the bus for Mode 0 (the MCP3008's required mode) and reads Channel 0.
#include <SPI.h>
// Pin definitions
const int CS_PIN = 10;
void setup() {
Serial.begin(115200);
while (!Serial); // Wait for serial monitor (Leonardo/Micro)
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect slave immediately
// Initialize hardware SPI
SPI.begin();
}
void loop() {
int adcValue = readMCP3008(0); // Read Channel 0
// Convert 10-bit ADC value to voltage (assuming 5V VREF)
float voltage = adcValue * (5.0 / 1023.0);
Serial.print("CH0 Raw: ");
Serial.print(adcValue);
Serial.print(" | Voltage: ");
Serial.println(voltage, 3);
delay(250);
}
// Function to read a specific channel (0-7) from MCP3008
int readMCP3008(byte channel) {
// Configure SPI settings for MCP3008: 1MHz, MSBFIRST, Mode 0
SPISettings settings(1000000, MSBFIRST, SPI_MODE0);
// MCP3008 requires a 3-byte transaction
// Byte 1: Start bit (1), Single-ended (1), Channel (3 bits), Don't care (3 bits)
byte commandBits = B00000001;
byte configBits = (channel | 0x08) << 4; // Set single-ended and channel
SPI.beginTransaction(settings);
digitalWrite(CS_PIN, LOW); // Select slave
SPI.transfer(commandBits);
byte highByte = SPI.transfer(configBits);
byte lowByte = SPI.transfer(0x00); // Dummy byte to clock out remaining data
digitalWrite(CS_PIN, HIGH); // Deselect slave
SPI.endTransaction();
// Combine the 10-bit result
int result = ((highByte & 0x03) << 8) | lowByte;
return result;
}
For the official Arduino SPI library reference, including details on SPISettings and interrupt-safe transfers, consult the core documentation.
Debugging the Bus: Classic Failures and How to Sniff
When your SPI device returns garbage data, zeros, or 0xFF, the issue is almost always at the physical layer or in the clock configuration. Here is how to diagnose the classic failures.
1. The CPOL/CPHA Mismatch (SPI Modes)
SPI does not have a single standard for clock polarity and phase. It uses four modes. If your master is in Mode 0 and your slave expects Mode 3, the slave will sample the data line at the exact wrong microsecond, resulting in shifted bits.
- Mode 0 (Most Common): Clock idle LOW. Data sampled on the rising (leading) edge.
- Mode 1: Clock idle LOW. Data sampled on the falling (trailing) edge.
- Mode 2: Clock idle HIGH. Data sampled on the falling (leading) edge.
- Mode 3: Clock idle HIGH. Data sampled on the rising (trailing) edge.
Fix: Check the slave device datasheet. If it specifies CPOL=1, CPHA=1, you must use SPI_MODE3 in your SPISettings.
2. Baud Rate Too High for the Wiring
If you push the SPI clock to 8 MHz over 30 cm of breadboard jumper wires, the parasitic capacitance between the wires will round off the sharp square-wave edges of the clock signal. The slave's Schmitt trigger inputs may fail to register the clock transitions.
Fix: Drop the clock speed in SPISettings from 8000000 to 1000000 (1 MHz). If the data suddenly becomes reliable, your wiring is too long or poorly routed. Use twisted pairs for SCK/MOSI/MISO in high-speed runs.
3. MISO Line Floating
If you read exactly 0xFF or 0x00 consistently, the MISO line is likely floating. This happens if the slave is unpowered, the CS line is stuck HIGH (deselecting the slave), or the slave's MISO pin is not entering a high-impedance state when deselected, causing bus contention with another slave.
How to Sniff and Debug the Bus
You cannot debug SPI timing issues with a standard multimeter. You need a Logic Analyzer. Tools like the Saleae Logic Pro 8 or cheaper 24MHz 8-channel clones running PulseView (Sigrok) are mandatory for serious embedded work.
To accurately capture SPI traffic without aliasing, your logic analyzer's sampling rate must be at least 4 times higher than the SPI clock frequency. If your SPI bus is running at 4 MHz, set your logic analyzer to sample at a minimum of 16 MHz (24 MHz is safer). Connect the ground clip to the MCU ground, and probe SCK, MOSI, MISO, and CS simultaneously.
In the logic analyzer software, use the SPI protocol decoder. Set the decoder to match your expected Mode (e.g., Mode 0) and bit order (MSB first). The decoder will translate the raw hex bytes into human-readable MOSI commands and MISO responses, allowing you to instantly verify if the master is sending the correct register address and if the slave is acknowledging it.






