When you need to move serious data between a microcontroller and a peripheral—like streaming pixels to an ILI9341 TFT display or logging high-speed ADC data to an SD card—an SPI channel is your default hardware peripheral. On modern MCUs like the ESP32 or RP2040, an SPI channel is a dedicated silicon block (such as the ESP32's HSPI/SPI2 or VSPI/SPI3) that handles synchronous serial clocking without CPU intervention.

The direct answer for most hobbyist and prosumer sensor dashboards: use the ESP32 VSPI channel (GPIO 5, 18, 19, 23) for your primary high-speed peripherals, and reserve HSPI for secondary devices. To run multiple devices on one SPI channel, you share the MOSI, MISO, and SCK lines, but you must route individual Chip Select (CS) pins for every target. Below is the complete physical, logical, and debugging framework to get your bus running without ghost-triggering or data corruption.

The Physical Layer: Wiring an SPI Channel

Unlike I2C, which relies on an open-drain architecture, SPI is a push-pull protocol. This means the master actively drives the clock (SCK) and data (MOSI) lines high and low. Because of this, the physical wiring rules are strict regarding capacitance and trace length.

Bench Rule: Keep your SPI jumper wires under 15 cm (6 inches) if you plan to run the bus above 20 MHz. If you need longer runs, drop your clock speed to 4 MHz or use a differential bus transceiver like the MAX3490.

Pull-Up Requirements: The SPI vs. I2C Distinction

A common mistake is applying I2C pull-up rules to an SPI channel. SPI does not require pull-up resistors on SCK, MOSI, or MISO for basic bus operation. The master and slave actively drive these lines. However, you absolutely need 10 kΩ pull-up resistors on every Chip Select (CS) line tied to VCC. During MCU boot or reset, GPIO pins float. If a CS line floats low, the slave device will attempt to drive the MISO line, causing bus contention and potentially bricking the boot sequence if it conflicts with the MCU's internal strapping pins.

Additionally, if you are sharing an SPI channel with a device that might be unpowered (like an SD card in a removable socket), place a 10 kΩ pull-up on the MISO line to prevent the unpowered slave from dragging the bus low through its internal ESD protection diodes.

Bus Mechanics: SPI vs. I2C vs. UART

Choosing the right protocol depends entirely on your distance, speed, and device count constraints. Here is how the physical layer mechanics break down across the big three embedded protocols.

Feature SPI Channel I2C Bus UART
Wires Required 4 (MOSI, MISO, SCK, CS) + 1 CS per device 2 (SDA, SCL) shared by all 2 (TX, RX) per pair
Max Speed (Typical) 10 MHz - 80 MHz (MCU dependent) 100 kHz to 3.4 MHz (Fast+) 115,200 bps to 3 Mbps
Addressing Hardware CS lines (no software addressing) 7-bit or 10-bit software addressing None (point-to-point only)
Reliable Distance < 30 cm (without transceivers) < 1 meter (highly capacitance dependent) < 15 meters (RS-485 differential)

The Classic Failures: Debugging Your SPI Channel

When a bus fails, the symptoms often mimic other protocols, leading to misdiagnosis. Here is how the classic embedded failures translate to an SPI channel, and how to fix them.

1. Address Clash vs. CS Contention

In I2C, an address clash (two devices with the same hardcoded address) halts the bus with an ACK error. SPI has no software addressing. The SPI equivalent is CS contention. If two devices attempt to drive MISO simultaneously because their CS lines are both pulled low, the bus will short, data will garble, and you may damage the slave's output drivers. Fix: Verify with a multimeter that only one CS line is at 0V (active low) during a transaction. Ensure your firmware initializes all CS pins to HIGH in the setup() block before calling SPI.begin().

2. Missing Pull-Up vs. Ghost Triggering

A missing pull-up on I2C SDA/SCL causes the bus to lock up completely because the lines never return to a logic HIGH. On an SPI channel, the clock and data lines don't need pull-ups, but a missing pull-up on the CS line causes ghost triggering. When the ESP32 resets, floating CS pins dip low, causing the SD card or sensor to wake up and drive MISO, which can prevent the ESP32 from booting if it conflicts with GPIO 12 (a strapping pin). Fix: Solder 10 kΩ resistors from every CS pin to 3.3V.

3. Baud Mismatch vs. CPOL/CPHA Mismatch

UART suffers from baud mismatch (e.g., transmitting at 115200 while the receiver expects 9600). SPI doesn't use baud rates; it uses clock phase and polarity, defined as SPI Modes (0, 1, 2, 3). If your logic analyzer shows the master clocking data on the falling edge, but the sensor expects the rising edge, you have a CPOL/CPHA mismatch. Mode 0 (CPOL=0, CPHA=0) is the default for 90% of sensors, but SD cards and certain shift registers require Mode 3. Fix: Check the sensor datasheet's timing diagram and set the mode explicitly in SPISettings.

How to Sniff and Debug the Bus

Do not guess with SPI. Use a logic analyzer. A $15 DSLogic Plus or a genuine Saleae Logic 8 is mandatory for bench work. Connect probes to SCK, MOSI, MISO, and the specific CS line. Set your trigger to the falling edge of the CS line. If you see SCK toggling but MISO is flatlined, your slave is unpowered, in reset, or in the wrong SPI mode. For deeper protocol decoding, use the Saleae Logic 2 software's built-in SPI analyzer to parse the hex bytes in real-time.

Minimal Working Exchange: ESP32 HSPI Configuration

Below is a robust, copy-pasteable example for the ESP32 using the secondary SPI channel (HSPI). This avoids conflicts with the default VSPI channel often used by the Arduino core for internal flash operations or default SPI objects.

Wiring Map (ESP32 DevKit V1 to BME280 Sensor):
GPIO 14 (HSPI SCK) → SCL
GPIO 13 (HSPI MOSI) → SDI
GPIO 12 (HSPI MISO) → SDO
GPIO 15 (HSPI CS) → CSB (with 10kΩ pull-up to 3.3V)
#include <SPI.h>
#include <Adafruit_BME280.h>

// Define the HSPI channel explicitly
SPIClass hspi(HSPI);
Adafruit_BME280 bme;

const int HSPI_CS = 15;
const int HSPI_SCK = 14;
const int HSPI_MISO = 12;
const int HSPI_MOSI = 13;

void setup() {
  Serial.begin(115200);
  
  // 1. Initialize CS pins HIGH before SPI.begin() to prevent ghost triggering
  pinMode(HSPI_CS, OUTPUT);
  digitalWrite(HSPI_CS, HIGH);

  // 2. Start the HSPI channel with explicit pin mapping
  hspi.begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
  
  // 3. Pass the custom SPIClass object to the sensor library
  if (!bme.begin(HSPI_CS, &hspi)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1) delay(10);
  }
  
  Serial.println("BME280 initialized on HSPI channel.");
}

void loop() {
  // The Adafruit library handles SPI.beginTransaction() internally,
  // ensuring correct CPOL/CPHA and preventing interrupt conflicts.
  Serial.print("Temperature = ");
  Serial.print(bme.readTemperature());
  Serial.println(" *C");
  
  delay(2000);
}

Decision Tree: Which Protocol and Channel Should You Pick?

Stop debating protocols in the abstract. Use this decision path to lock in your hardware architecture for your next build.

  • IF you need to connect more than 10 low-speed sensors (e.g., temperature, humidity) and want to minimize wiring.
    → PICK: I2C. Use a TCA9548A I2C multiplexer if you run out of addresses.
  • IF you need to push high-bandwidth data (e.g., ILI9341 TFT displays, 24-bit ADCs, SD cards) over short distances (<30 cm).
    → PICK: SPI.
  • IF you need to communicate over distances greater than 1 meter, or between separate enclosures.
    → PICK: UART via RS-485 transceivers (e.g., MAX485). SPI and I2C will fail due to capacitance and ground loops.
  • IF you are using an ESP32 and need to run an SD Card and a TFT Display simultaneously.
    → PICK: Put the SD Card on VSPI (default GPIO 5, 18, 19, 23) and the TFT on HSPI (GPIO 15, 14, 12, 13). Do not share the bus between high-speed SD writes and display refreshes; the CS switching overhead will cause display tearing.
The Default Concrete Pick: For a standard 2026 IoT sensor node (e.g., ESP32-S3 + BME688 + MicroSD), wire the MicroSD to the primary SPI channel (SPI2/VSPI) using the native pins, and put the BME688 on I2C. This reserves your secondary SPI channel (HSPI) for future expansion, like adding an RFM95 LoRa module, without requiring a PCB respin.

For deeper technical specifications on ESP32 SPI peripheral routing and DMA capabilities, refer to the official Espressif SPI Master API documentation. For foundational timing diagrams and mode definitions, the Analog Devices SPI Interface Guide remains the industry-standard reference.