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.
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.
| Parameter | Specification | Notes for Arduino |
|---|---|---|
| Wires Required | 4 shared + 1 CS per device | MOSI, MISO, SCK are shared; CS is individual. |
| Max Speed | 10 MHz - 20 MHz+ | AVR (Uno/Mega) tops out around 8-10 MHz reliably; ESP32 can hit 40-80 MHz. |
| Addressing | Hardware CS lines | No software addresses; requires one GPIO per target. |
| Max Distance | ~30 cm (1 ft) at high speed | Capacitance kills high-frequency edges. Drop to 1 MHz for 1-meter runs. |
| Data Flow | Full-Duplex | Master 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.
| Criteria | SPI | I2C | UART |
|---|---|---|---|
| Best For | High-speed data (Displays, ADCs, SD cards) | Many low-speed sensors on limited pins | Point-to-point long-distance / PC comms |
| Speed | Very High (10+ MHz) | Low to Med (100 kHz - 3.4 MHz) | Medium (115.2k - 1 Mbps typical) |
| Wiring Complexity | High (4 + N wires) | Low (2 shared wires) | Low (2 wires per pair) |
| Device Count | Limited by available CS GPIO pins | Up to 127 (limited by address space) | 1-to-1 (without multiplexers) |
| Distance Limit | Short (< 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.
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.






