The Serial Peripheral Interface (SPI) is the heavy-lifter of embedded communication. When you need to push megabytes of data to a TFT display or pull high-speed samples from an analog-to-digital converter (ADC), I2C chokes and UART lacks the synchronization. The SPI bus on Raspberry Pi hardware uses four shared wires to achieve full-duplex, synchronous communication at speeds that can theoretically hit 125 MHz, though practical breadboard wiring limits you to about 10–20 MHz before signal integrity degrades.

Unlike I2C, which uses software addressing to talk to dozens of devices on two wires, SPI routes data using individual Chip Select (CS) lines for every target. Below is the exact physical layer breakdown, wiring rules, and Python implementation you need to get your first SPI exchange working without frying your Pi's GPIO bank.

Bus Mechanics: SPI at a Glance

Before writing a single line of Python, you need to understand the physical constraints of the bus. SPI is a master-slave (or controller-peripheral) architecture. The Raspberry Pi acts as the master, generating the clock and initiating all transfers.

Parameter SPI Specification Raspberry Pi 4/5 Implementation
Wires Required 3 shared (MOSI, MISO, SCLK) + 1 CS per device SPI0 uses GPIO 9, 10, 11 + GPIO 8 (CE0) / 7 (CE1)
Max Speed Up to 100+ MHz (silicon dependent) 125 MHz theoretical; 10–20 MHz practical for jumper wires
Addressing Hardware routing via individual CS pins No software addressing; requires one GPIO per peripheral
Duplex Full-duplex (simultaneous send/receive) Shift registers exchange bytes simultaneously on clock edges
Distance Limit < 1 meter (without differential drivers) Keep traces/wires under 30cm to avoid capacitive loading on MISO

Physical Wiring and Pull-Up Requirements

The most common mistake makers make when migrating from Arduino to Raspberry Pi is ignoring logic levels. The Raspberry Pi GPIO operates strictly at 3.3V. If you connect a 5V SPI device (like an older Adafruit TFT shield or an Arduino Uno acting as a peripheral) directly to the Pi's MISO pin, you will back-feed 5V into the Pi's SoC and permanently destroy the GPIO bank. Always use a bidirectional logic level shifter (like the TXB0108 or CD4050) when mixing 5V and 3.3V SPI devices.

SPI0 Pinout (BCM Numbering)

  • MOSI (Master Out Slave In): GPIO 10 (Pin 19)
  • MISO (Master In Slave Out): GPIO 9 (Pin 21)
  • SCLK (Serial Clock): GPIO 11 (Pin 23)
  • CE0 (Chip Enable 0): GPIO 8 (Pin 24)
  • CE1 (Chip Enable 1): GPIO 7 (Pin 26)
The Pull-Up Misconception: Unlike I2C, the SPI data and clock lines (MOSI, MISO, SCLK) do not require pull-up resistors. They are actively driven push-pull outputs. However, the Chip Select (CS) lines absolutely need 10kΩ pull-up resistors to 3.3V. During the Raspberry Pi boot sequence, GPIO pins float in a high-impedance state before the device tree configures them. Without a pull-up, a floating CS line can ghost-trigger your peripheral, causing it to drive the MISO line and collide with other boot traffic, potentially stalling the boot process or corrupting sensor registers.

Minimal Working Exchange: Python and spidev

To interact with the SPI bus in user-space, Linux uses the spidev kernel module. First, enable the SPI interface via the terminal: sudo raspi-config → Interface Options → SPI → Enable, then reboot.

Below is a complete, minimal exchange using the MCP3008 (a classic 10-bit, 8-channel ADC). We will read Channel 0. The MCP3008 requires a specific 3-byte sequence to initiate a read: a start bit, a configuration byte (selecting single-ended CH0), and a dummy byte to clock out the result.

MCP3008 to Raspberry Pi Wiring

  • VDD & DGTL (Pins 16, 15) → Pi 3.3V
  • AGND & DGND (Pins 14, 9) → Pi GND
  • Din (Pin 11) → Pi MOSI (GPIO 10)
  • Dout (Pin 12) → Pi MISO (GPIO 9)
  • CLK (Pin 13) → Pi SCLK (GPIO 11)
  • CS (Pin 10) → Pi CE0 (GPIO 8) with 10kΩ pull-up to 3.3V

Python Implementation

import spidev
import time
import sys

# Initialize SPI bus
spi = spidev.SpiDev()

try:
    # Open SPI bus 0, device 0 (CE0)
    spi.open(0, 0)
    
    # Set speed to 1.35 MHz (MCP3008 max is ~3.6MHz at 5V, ~1.35MHz at 3.3V)
    spi.max_speed_hz = 1350000
    
    # Set SPI Mode 0 (CPOL=0, CPHA=0) - Default for MCP3008
    spi.mode = 0
    
    def read_adc_channel(channel):
        if channel < 0 or channel > 7:
            raise ValueError("Channel must be 0-7")
        
        # Construct the 3-byte request
        # Byte 1: Start bit (00000001)
        # Byte 2: Single-ended config + channel number (10000000 | channel) shifted left by 4
        # Byte 3: Don't care (00000000)
        request = [
            0x01, 
            (0x08 | channel) << 4, 
            0x00
        ]
        
        # Perform full-duplex transfer
        response = spi.xfer2(request)
        
        # Parse the 10-bit result from the response bytes
        # Byte 1 contains the null bit, Byte 2 contains the top 2 bits, Byte 3 has the lower 8
        adc_value = ((response[1] & 0x03) << 8) | response[2]
        return adc_value

    while True:
        val = read_adc_channel(0)
        voltage = (val * 3.3) / 1023.0
        print(f"CH0 Raw: {val:04d} | Voltage: {voltage:.3f} V")
        time.sleep(0.5)

except OSError as e:
    print(f"SPI Error: {e}. Is SPI enabled in raspi-config?", file=sys.stderr)
    sys.exit(1)
finally:
    spi.close()

For deeper kernel-level details on how spidev handles buffer limits and chip select behavior, refer to the Linux Kernel spidev documentation.

Debugging the SPI Bus: Sniffing and Classic Failures

When your SPI bus returns all zeros, all 255s, or garbage data, the issue is almost always at the physical layer or clock configuration. Here is how to diagnose the classic failures.

The Classic Failures

  1. Clock Polarity and Phase Mismatch (CPOL/CPHA): SPI has four modes (0, 1, 2, 3) defining whether the clock idles high or low, and whether data is sampled on the leading or trailing edge. The Pi defaults to Mode 0. If your sensor requires Mode 3, your data will be shifted by one bit, resulting in garbage. Fix: Set spi.mode = 0b11 in Python.
  2. Baud Rate Too High: Pushing 20 MHz over 20cm breadboard wires introduces capacitive coupling. The square wave clock turns into a sawtooth, and the slave misses edges. Fix: Drop spi.max_speed_hz to 1,000,000 (1 MHz) to verify connectivity, then step up.
  3. Kernel Driver Clashes: If the Pi's device tree has loaded a driver for an SPI screen or CAN bus, user-space spidev will be blocked. Fix: Run dmesg | grep spi to check for driver bindings, and disable conflicting overlays in /boot/config.txt.

How to Sniff the Bus

Software debugging only goes so far. To truly see the bus, you need a hardware sniff. A $12 USB logic analyzer (like the 24MHz 8-channel Saleae clones) running PulseView / sigrok is mandatory for serious SPI work.

Connect the logic analyzer probes to MOSI, MISO, SCLK, and CS. Set the decoder to SPI. What to look for: Verify that the CS line drops low before the first clock edge, and rises high after the last clock edge. If CS is jittering or bouncing, your pull-up resistor is missing or too weak. If MISO stays flat while MOSI toggles, your slave is either unpowered, held in reset, or you have a MISO/MOSI crossover wiring error.

Protocol Decision Tree: Which Bus Wins?

Choosing between SPI, I2C, and UART depends entirely on your physical constraints and throughput requirements. Use this decision matrix to lock in your architecture.

Constraint / Requirement Winning Protocol Why?
Need > 5 devices on just 2 wires? I2C Uses 7-bit/10-bit software addressing; only requires SDA/SCL.
Need > 1 Mbps over 10+ meters? RS-485 (UART) Differential signaling rejects noise over long cable runs.
Need MHz-speed full-duplex streaming < 1m? SPI No addressing overhead; separate MISO/MISO allows simultaneous TX/RX.
Need to connect a Pi to an ESP32 wirelessly? MQTT / WiFi Physical buses fail beyond a few meters; use network stacks instead.

The Concrete Pick

If you are wiring a high-throughput sensor array, an RF module (like the nRF24L01), or a TFT display on a single Raspberry Pi board under 1 meter, choose hardware SPI0 (GPIO 8/9/10/11). It offers the lowest CPU overhead and highest bandwidth.

The primary limitation of SPI is pin starvation: the Pi only has two native hardware CE pins (CE0 and CE1). If you need to connect five SPI devices, do not switch to software bit-banged SPI (which destroys CPU performance), and do not downgrade to I2C if your sensors require high bandwidth. Instead, wire an I2C GPIO expander like the MCP23017 to the Pi, and use its 16 output pins as individual, software-controlled active-low Chip Select lines for your SPI peripherals. This gives you the bandwidth of SPI with the device density of I2C.