SPI (Serial Peripheral Interface) relies on four primary SPI lines: SCK, MOSI, MISO, and CS. Unlike I2C, SPI uses a push-pull architecture. This means it does not strictly require pull-up resistors on the data and clock lines, but it demands strict attention to Chip Select (CS) routing, tri-state logic on the MISO line, and clock phase (CPOL/CPHA) configuration to prevent bus contention and silent data corruption.
The Physical Layer: Wiring SPI Lines and Signal Integrity
Before writing a single line of code, you must understand the electrical behavior of the four SPI lines. A common beginner mistake is treating SPI like I2C and littering the breadboard with pull-up resistors, or conversely, ignoring the floating GPIO states during microcontroller boot.
While SCK, MOSI, and MISO are push-pull and need no pull-ups, the Chip Select (CS) line is active-LOW. When an ESP32 or Arduino boots, its GPIO pins float before the bootloader initializes them. If your peripheral's CS line is floating, it may accidentally activate and drive the MISO line, causing a short circuit if another device is also active. Always place a 10kΩ pull-up resistor between the CS line and VCC to keep the peripheral deselected during MCU boot.
Physical Wiring Constraints:
- Distance: SPI is designed for on-board communication. Keep SPI lines under 20cm for clock speeds above 10MHz. If you must run lines up to 1 meter, drop the clock speed to 1MHz or lower and add 33Ω series termination resistors on the SCK and MOSI lines near the master to dampen high-frequency ringing.
- MISO Contention: The MISO line is shared across all devices on the bus. Every peripheral must tri-state (high-impedance) its MISO output when its CS line is HIGH. If you are using a cheap sensor module that lacks a tri-state buffer on MISO, it will back-feed voltage onto the bus when deselected, corrupting data from other active devices.
Bus Mechanics: SPI vs. I2C vs. UART
Choosing the right protocol depends entirely on your constraints regarding distance, speed, and device count. SPI trades pin count for raw bandwidth and full-duplex capability.
| Feature | SPI | I2C | UART (RS-485) |
|---|---|---|---|
| Wires Required | 3 shared + 1 CS per device | 2 shared (SDA, SCL) | 2 (TX, RX) per link |
| Max Speed (Typical) | 10MHz - 80MHz+ | 100kHz - 3.4MHz | 115.2kbps - 10Mbps |
| Addressing | Hardware CS routing (No software address) | 7-bit or 10-bit software address | None (point-to-point) or software |
| Max Distance | < 1 meter (highly dependent on capacitance) | < 1 meter (limited by bus capacitance) | Up to 1200 meters (via RS-485 PHY) |
| Duplex | Full-Duplex | Half-Duplex | Full-Duplex |
When to choose which: Use SPI when you need to move large blocks of data quickly (e.g., TFT displays, external flash, high-sample-rate ADCs) and have enough GPIO pins for individual CS lines. Use I2C when you have many low-bandwidth sensors (temperature, IMUs) and limited pins. Use UART/RS-485 for long-distance runs across a building or between separate enclosures.
Minimal Working Exchange: ESP32 to W25Q32 Flash
Let's look at a complete, minimal exchange. We will read the JEDEC Manufacturer ID from a W25Q32 SPI flash chip using an ESP32 DevKit v1. This verifies that the SPI lines are wired correctly and the clock polarity is matched.
Wiring Table:
| ESP32 DevKit v1 | W25Q32 Flash Pin | SPI Line Name |
|---|---|---|
| GPIO 18 | CLK (Pin 6) | SCK |
| GPIO 23 | DI (Pin 5) | MOSI |
| GPIO 19 | DO (Pin 2) | MISO |
| GPIO 5 | CS (Pin 1) | CS (Add 10k pull-up to 3.3V) |
| 3V3 | VCC (Pin 8) | Power |
| GND | GND (Pin 4) | Ground |
Arduino Code (ESP32 Core):
#include <SPI.h>
#define CS_PIN 5
void setup() {
Serial.begin(115200);
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect immediately
// Initialize SPI bus with explicit pin mapping
SPI.begin(18, 19, 23, CS_PIN);
delay(100);
}
void loop() {
// W25Q32 JEDEC ID command is 0x9F
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_PIN, LOW);
SPI.transfer(0x9F); // Send Read JEDEC ID command
byte manufacturer = SPI.transfer(0x00);
byte mem_type = SPI.transfer(0x00);
byte capacity = SPI.transfer(0x00);
digitalWrite(CS_PIN, HIGH);
SPI.endTransaction();
if (manufacturer == 0x00 || manufacturer == 0xFF) {
Serial.println("ERROR: MISO line floating or wrong CPOL/CPHA mode.");
} else {
Serial.printf("Success! Mfr: 0x%02X, Type: 0x%02X, Cap: 0x%02X\n",
manufacturer, mem_type, capacity);
}
delay(2000);
}
Sniffing and Debugging the SPI Bus
When your SPI device returns garbage data or 0xFF, the issue is almost always at the physical layer or the clock configuration. Here is how to systematically debug the SPI lines.
1. The Classic CPOL/CPHA Mismatch (Baud/Mode Mismatch)
SPI defines four modes based on Clock Polarity (CPOL) and Clock Phase (CPHA). Mode 0 (CPOL=0, CPHA=0) and Mode 3 (CPOL=1, CPHA=1) are the most common. If your master is set to Mode 0 but the peripheral expects Mode 3, the master will sample the MISO line exactly one half-clock-cycle too early, resulting in shifted, corrupted bytes. Always check the peripheral datasheet's timing diagram. If SCK idles LOW, it's Mode 0 or 1. If it idles HIGH, it's Mode 2 or 3.
2. Sniffing with a Logic Analyzer
Do not rely on an oscilloscope for protocol decoding unless you have a high-end model with SPI triggers. Instead, use a USB logic analyzer like a Saleae Logic 8 or a DSLogic Plus.
- Sample Rate Rule: Set your logic analyzer sample rate to at least 4x the SPI clock frequency. If your SCK is 10MHz, sample at 40MS/s minimum.
- Decoding: Connect the analyzer to SCK, MOSI, MISO, and CS. In the software, assign the SPI analyzer, set the correct CPOL/CPHA, and enable MISO/MOSI decoding. If the decoded hex matches your expected register values but your MCU code reads garbage, your software buffer handling is flawed, not the hardware.
3. Checking for Address Clashes (CS Routing)
If you have two devices on the same SPI bus and reading one alters the state of the other, you have a CS clash. Verify with a multimeter in continuity mode that the CS pins are not accidentally bridged on your breadboard, and ensure your code explicitly drives the unused CS pins HIGH before starting a transaction.
Frequently Asked Questions About SPI Lines
Can I share SPI lines between a 3.3V ESP32 and a 5V Arduino peripheral?
Not directly. The ESP32 is strictly 3.3V tolerant; feeding 5V into its MISO or SCK pins will permanently destroy the GPIO pad. You must use a bidirectional logic level shifter. The TXS0108E is a reliable choice for SPI because it handles the fast edge rates of SCK better than cheap MOSFET-based BSS138 shifters, which can round off the clock edges at speeds above 2MHz, causing phase errors.
Why is my SPI device returning all 0xFF or 0x00 on the MISO line?
This is the most common SPI failure. 0xFF usually means the MISO line is floating (disconnected wire, or the peripheral is unpowered and its internal pull-ups are dominating). 0x00 usually means MISO is shorted to ground, or the peripheral is held in a hardware reset state. First, verify the peripheral's VCC with a multimeter. Second, check your CPOL/CPHA settings. Finally, probe the CS line with a multimeter to ensure it is actually toggling LOW during the SPI.transfer() call.
Do I need to connect the MISO SPI line if I only write to a display?
No. If you are driving a write-only device like a TFT LCD or a DAC, you can leave the MISO line disconnected at the peripheral end. This frees up a microcontroller GPIO pin if you are routing the display to a dedicated hardware SPI bus that doesn't share MISO with other read-heavy sensors. However, you must still initialize the SPI bus correctly in your firmware; some ESP-IDF SPI master drivers require you to explicitly flag the bus as "MISO unused" to prevent the DMA controller from throwing a bus fault when it expects return data.
How long can I make my SPI lines before signal degradation occurs?
The limit is dictated by bus capacitance, not just distance. Every wire, breadboard contact, and peripheral input pin adds picofarads (pF) of capacitance. High capacitance rounds off the sharp square-wave edges of the SCK line, turning it into a triangle wave. Once the edge rises too slowly, the peripheral's clock sampler misses the beat. As a rule of thumb, keep total bus capacitance under 50pF for speeds above 10MHz. If you must run 50cm ribbon cables to an SPI ADC, drop the clock to 1MHz, use twisted-pair wiring (pairing SCK with GND), and add a 74HC125 tri-state buffer at the peripheral end to clean up the degraded signal.






