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:
Add or uncomment:
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):
Save and reboot:
Confirm that the Linux character devices are exposed:
Expected output:
/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:
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:
- 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.
- 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.
- 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:
- UART Serial & Industrial RS-485: Connect factory automation PLCs and Modbus RTU devices over long-distance twisted pair.
- Dallas 1-Wire & Hardware PWM: Multidrop digital temperature networks and jitter-free hardware pulse generators.