Skip to content

SPI Protocol Architecture & High-Speed Peripherals

When application requirements exceed the bandwidth limitations of the 2-wire I2C bus (typically 100 kHz to 400 kHz), the Serial Peripheral Interface (SPI) provides multi-megahertz, full-duplex synchronous communication. Originally designed by Motorola, SPI is widely adopted for high-frequency data streaming: Analog-to-Digital Converters (ADCs), Flash memory ICs, high-refresh TFT/e-paper displays, Ethernet controllers (ENC28J60), and CAN bus transceivers (MCP2515).

Because SPI employs dedicated push-pull lines and separate Chip Select (CE) signals, it avoids open-drain pull-up rise-time delays and eliminates software addressing overhead.


1. SPI Bus Topology & Signal Lines

Standard SPI utilizes a four-wire bus topology operating in a Controller/Target (formerly Master/Slave) configuration:

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph Controller["Raspberry Pi (Host Controller)"]
        SCLK["SCLK (Serial Clock)"]
        MOSI["MOSI / COPI (Controller Out, Peripheral In)"]
        MISO["MISO / CIPO (Controller In, Peripheral Out)"]
        CE0["CE0 / SS0 (Chip Enable 0)"]
        CE1["CE1 / SS1 (Chip Enable 1)"]
    end

    subgraph Dev0["Peripheral 0: MCP3008 ADC"]
        P0_CLK["CLK"]
        P0_DIN["DIN"]
        P0_DOUT["DOUT"]
        P0_CS["CS"]
    end

    subgraph Dev1["Peripheral 1: ST7789 TFT Display"]
        P1_CLK["SCK"]
        P1_DIN["SDA"]
        P1_CS["CS"]
    end

    SCLK ==> P0_CLK
    SCLK ==> P1_CLK

    MOSI ==> P0_DIN
    MOSI ==> P1_DIN

    MISO <== P0_DOUT

    CE0 --> P0_CS
    CE1 --> P1_CS

Signal Roles & Definitions

Signal Line BCM Pin Name Physical Pin Description
SCLK GPIO 11 Pin 23 Serial Clock generated exclusively by the Raspberry Pi
MOSI (COPI) GPIO 10 Pin 19 Controller Output, Peripheral Input (data sent to device)
MISO (CIPO) GPIO 9 Pin 21 Controller Input, Peripheral Output (data read from device)
CE0 (SS0) GPIO 8 Pin 24 Active-LOW Chip Select for primary peripheral
CE1 (SS1) GPIO 7 Pin 26 Active-LOW Chip Select for secondary peripheral

Because MISO is driven exclusively by the peripheral whose Chip Select line is pulled LOW (all inactive peripherals keep their MISO pins in high impedance), multiple devices share the same clock and data lines without bus contention.


2. The Four SPI Modes: CPOL and CPHA

SPI does not mandate a single universal clocking standard. Communication timing is governed by two parameters: - CPOL (Clock Polarity): Defines whether the clock line idles LOW (0) or HIGH (1). - CPHA (Clock Phase): Defines whether data bits are sampled on the leading (first) or trailing (second) clock edge.

Combining these two parameters yields the standard 4 SPI Modes:

SPI Mode CPOL (Idle Clock) CPHA (Sample Edge) Description
Mode 0 0 (Low) 0 (Leading Edge) Clock idles low; data sampled on rising edge (Most common: MCP3008, SD cards)
Mode 1 0 (Low) 1 (Trailing Edge) Clock idles low; data sampled on falling edge
Mode 2 1 (High) 0 (Leading Edge) Clock idles high; data sampled on falling edge
Mode 3 1 (High) 1 (Trailing Edge) Clock idles high; data sampled on rising edge (Common in industrial Flash/FRAM)

Mode Mismatch Pitfall

Configuring the wrong SPI mode will corrupt transmission by shifting every bit by half a clock cycle or inverting the bit stream entirely. Always verify the peripheral datasheet's timing diagram.


3. Kernel Enablement & Linux Spidev Devices

On Raspberry Pi OS Bookworm, enable SPI in /boot/firmware/config.txt:

sudo nano /boot/firmware/config.txt

Add or uncomment:

# Enable primary SPI0 bus (with CE0 and CE1)
dtparam=spi=on

If your project requires more than two SPI devices, you can enable the secondary SPI1 controller (which provides up to 3 additional chip selects on auxiliary GPIOs):

# Optional: Enable SPI1 with 3 chip selects (GPIO 18, 17, 16)
dtoverlay=spi1-3cs

Save and reboot:

sudo reboot

Confirm that the Linux character devices are exposed:

ls -l /dev/spidev*

Expected output:

crw-rw---- 1 root spi 153, 0 /dev/spidev0.0
crw-rw---- 1 root spi 153, 1 /dev/spidev0.1

/dev/spidev0.0 controls communication with the device on CE0 (GPIO 8), while /dev/spidev0.1 talks to the device on CE1 (GPIO 7).


4. Hardware Lab: Reading Analog Sensors via MCP3008 ADC

The Raspberry Pi has no native Analog-to-Digital Converter (ADC) pins on its header. To sample analog voltages (from potentiometers, analog temperature probes, light sensors, or 4–20 mA current loops), an external ADC like the Microchip MCP3008 (10-bit, 8-channel, SPI interface) is the industry standard.

Circuit Wiring Schematic

Connect the 16-pin DIP MCP3008 to the Raspberry Pi:

MCP3008 Pin Name Connects To Notes
Pin 1 CH0 Analog Input Voltage signal to measure (0.0 V to 3.3 V)
Pins 2–8 CH1–CH7 Analog Inputs Remaining channels
Pin 9 DGND Pin 6 (GND) Digital Ground
Pin 10 CS Pin 24 (GPIO 8 / CE0) Chip Select
Pin 11 DIN Pin 19 (GPIO 10 / MOSI) Data In
Pin 12 DOUT Pin 21 (GPIO 9 / MISO) Data Out
Pin 13 CLK Pin 23 (GPIO 11 / SCLK) Clock
Pin 14 AGND Pin 14 (GND) Analog Ground (shielded)
Pin 15 VREF Pin 1 (3.3V) Reference Voltage (3.30 V)
Pin 16 VDD Pin 1 (3.3V) Power supply (3.3V)

Noise Suppression Best Practice

Place a 100 nF ceramic capacitor directly across Pin 16 (VDD) and Pin 9 (DGND) within 5 mm of the chip to decouple power supply ripple. For high accuracy, place an additional 100 nF cap between VREF and AGND.


5. Full-Duplex Python Engineering with spidev

The MCP3008 requires a 3-byte command packet to request a channel reading. Because SPI is strictly full-duplex, the Raspberry Pi simultaneously receives 3 bytes while transmitting 3 bytes:

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart LR
    subgraph TX["Transmitted by Raspberry Pi (MOSI)"]
        T1["Byte 1: Start Bit (0x01)"]
        T2["Byte 2: Single-Ended Config (0x80 | Ch<<4)"]
        T3["Byte 3: Don't Care (0x00)"]
    end

    subgraph RX["Received from MCP3008 (MISO)"]
        R1["Byte 1: High-Z Garbage"]
        R2["Byte 2: Null Bit + B9..B8"]
        R3["Byte 3: B7..B0 (Low 8 bits)"]
    end

The 10-bit numerical result is assembled by masking the lowest 2 bits of Byte 2 and shifting them by 8 positions, then combining them with Byte 3:

10-Bit ADC Assembly Formula:
Result = ((Byte[1] & 0x03) · 256) + Byte[2]
Voltage = (Result ÷ 1023) · VREF

Production Python Implementation

Install the spidev module:

sudo apt update
sudo apt install -y python3-spidev

Create mcp3008_monitor.py:

#!/usr/bin/env python3
"""
High-Performance SPI ADC Driver for Microchip MCP3008
Reads analog telemetry channels at up to 1.35 MHz clock speeds with voltage scaling.
"""

import time
import sys
import spidev

class MCP3008ADC:
    def __init__(self, bus: int = 0, device: int = 0, speed_hz: int = 1350000, vref: float = 3.3):
        """
        Initializes the MCP3008 SPI connection.
        Maximum recommended SPI clock for MCP3008 at 3.3V is 1.35 MHz.
        """
        self.vref = vref
        self.spi = spidev.SpiDev()
        self.spi.open(bus, device)
        self.spi.max_speed_hz = speed_hz
        self.spi.mode = 0b00  # SPI Mode 0 (CPOL=0, CPHA=0)
        self.spi.bits_per_word = 8

    def read_channel(self, channel: int) -> tuple[int, float]:
        """
        Reads raw 10-bit integer (0..1023) and calculates scaled voltage.
        Channel must be an integer between 0 and 7.
        """
        if not 0 <= channel <= 7:
            raise ValueError(f"Invalid channel {channel}: must be between 0 and 7")

        # Command packet: Start Bit (1), Single-ended (1), Channel ID (D2, D1, D0)
        # Byte 1: 00000001 (Start Bit)
        # Byte 2: 1xxx0000 (Single-ended bit + 3-bit channel)
        # Byte 3: 00000000 (Don't care, generates clock for 8 lower data bits)
        tx_buffer = [1, (8 + channel) << 4, 0]

        # Perform full-duplex transfer
        rx_buffer = self.spi.xfer2(tx_buffer)

        # Assemble 10-bit reading
        raw_val = ((rx_buffer[1] & 3) << 8) | rx_buffer[2]
        voltage = (raw_val / 1023.0) * self.vref

        return raw_val, voltage

    def close(self) -> None:
        self.spi.close()

def main() -> None:
    print("Initializing MCP3008 ADC interface...")
    adc = MCP3008ADC(bus=0, device=0, speed_hz=1350000, vref=3.3)

    try:
        print("Sampling Channel 0 (Potentiometer / Analog Sensor)...")
        print("--------------------------------------------------")
        while True:
            raw, volts = adc.read_channel(0)
            percentage = (raw / 1023.0) * 100.0

            # Print formatted telemetry output
            sys.stdout.write(f"\rCH0 Raw: {raw:4d} | Voltage: {volts:5.3f} V | Range: {percentage:5.1f}%")
            sys.stdout.flush()
            time.sleep(0.1)

    except KeyboardInterrupt:
        print("\n\nHalting ADC sampler.")
    finally:
        adc.close()

if __name__ == "__main__":
    main()

6. High-Frequency Noise & Signal Integrity

When pushing SPI speeds toward 10 MHz to 32 MHz over ribbon cables or external breadboards, transmission line effects degrade signal integrity:

  1. Ringing and Ground Bounce: Fast rise times cause impedance mismatches. If oscilloscope probes reveal overshoot or ringing on SCLK, place a 33 Ω to 47 Ω damping resistor in series on the SCLK line close to the Raspberry Pi.
  2. Clock Cross-Talk: Never run ribbon cables where SCLK and MISO are placed on adjacent ungrounded conductors. Insert a Ground conductor between SCLK and data lines.
  3. Cable Length Limits: Standard SPI is designed for board-level routing (< 20 cm). If communication must travel over 1 meter, drop the clock speed below 500 kHz or use differential transceivers like RS-485 / LVDS.

Next Steps

Now that you have mastered high-speed synchronous streaming, proceed to asynchronous and differential industrial communications: