If you need to move data fast over a short distance on a workbench, Arduino hardware SPI is your default choice. On an ATmega328P (Arduino Uno/Nano), the hardware SPI bus maxes out at 8 MHz (1 MB/s). On an ESP32, it pushes up to 80 MHz (10 MB/s). Unlike software-bit-banged SPI, which chokes your CPU and introduces jitter, hardware SPI offloads the clock generation to the MCU's dedicated peripheral, freeing your main loop to handle logic.
This guide strips away the abstract theory and gives you the exact pinouts, physical layer rules, and debugging steps to get your SPI peripherals talking on the first try.
Bus Mechanics: SPI vs I2C vs UART at the Physical Layer
Before wiring anything, you need to know where SPI fits in the embedded ecosystem. SPI is a synchronous, full-duplex, master-slave protocol. It trades wire count for raw speed.
| Feature | Hardware SPI | I2C | UART |
|---|---|---|---|
| Wires Required | 4 (MOSI, MISO, SCK, CS) | 2 (SDA, SCL) | 2 (TX, RX) + GND |
| Max Speed (Typical MCU) | 8 MHz (Uno) / 80 MHz (ESP32) | 400 kHz (Fast) / 1 MHz (Fast+) | 115,200 baud (11.5 KB/s) |
| Addressing | Hardware CS lines (1 per device) | Software 7-bit/10-bit addresses | None (Point-to-point) |
| Max Distance | < 1 meter (highly capacitance-sensitive) | ~1 meter (with proper pull-ups) | ~15 meters (at 9600 baud) |
| Pull-up Resistors | None required (Push-pull) | Required (2.2k - 4.7k) | None (Idle high) |
Physical Wiring and the "No Pull-Up" Rule
The most common mistake makers migrating from I2C to SPI make is looking for the pull-up resistors. SPI does not use pull-up resistors. The SCK, MOSI, and MISO lines are driven push-pull by the master and slave. Adding pull-ups will only increase rise times and cause signal ringing at high clock speeds.
Standard Pinouts by Board
Always use the dedicated hardware SPI pins. Bit-banging these on random GPIOs will drop your throughput by 90%.
- Arduino Uno / Nano (ATmega328P): MOSI = Pin 11, MISO = Pin 12, SCK = Pin 13, CS = Pin 10 (or any digital pin).
- Arduino Mega 2560: MOSI = Pin 51, MISO = Pin 50, SCK = Pin 52, CS = Pin 53.
- ESP32 DevKit V1: MOSI = GPIO 23, MISO = GPIO 19, SCK = GPIO 18, CS = GPIO 5. (Note: ESP32 allows SPI pin remapping via the GPIO matrix, but sticking to defaults avoids conflicts with internal flash).
On Arduino Uno/Nano, pins 11, 12, and 13 are also broken out on the 2x3 ICSP header. If you are designing a custom shield, route your SPI traces to the ICSP header. This guarantees your shield will also work on the Arduino Mega, which uses different digital pins for SPI but shares the exact same ICSP pinout.
Logic Level Shifting (3.3V vs 5V)
If you are connecting a 5V Arduino Uno to a 3.3V SPI peripheral (like a W25Q32 flash chip or an SD card module), you must level-shift the MOSI, SCK, and CS lines. Feeding 5V into a 3.3V MISO pin usually won't fry the master, but 5V into a 3.3V slave's SCK or MOSI pin will destroy it. Use a CD4050B non-inverting buffer or a TXS0108E bidirectional translator. Do not use simple resistor dividers for SPI; the parasitic capacitance of the resistors will round off your 8 MHz square waves into unusable sine waves.
Minimal Working Exchange: W25Q32 Flash Memory
Let's wire up a Winbond W25Q32 (4MB SPI Flash) and read its JEDEC ID. This requires sending a 1-byte command and reading 3 bytes back.
| W25Q32 Pin | Arduino Uno Pin | Notes |
|---|---|---|
| VCC | 3.3V | Do not use 5V |
| GND | GND | Common ground required |
| CS (Chip Select) | Pin 10 | Active LOW |
| CLK (SCK) | Pin 13 | Serial Clock |
| DO (MISO) | Pin 12 | Data Out from Flash |
| DI (MOSI) | Pin 11 | Data In to Flash |
Here is the robust C++ code using the native Arduino SPI library. Notice the use of SPI.beginTransaction(). This is mandatory in real-world projects to prevent interrupt service routines (like encoder readers or timer callbacks) from hijacking the SPI bus mid-transfer and corrupting your data.
#include <SPI.h>
const int CS_PIN = 10;
void setup() {
Serial.begin(115200);
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect chip immediately
// Initialize hardware SPI
SPI.begin();
}
void loop() {
// Define bus settings: 10MHz, MSB first, SPI Mode 0
SPISettings settings(10000000, MSBFIRST, SPI_MODE0);
SPI.beginTransaction(settings);
digitalWrite(CS_PIN, LOW); // Assert Chip Select
// Send JEDEC ID command (0x9F)
SPI.transfer(0x9F);
// Read 3 bytes: Manufacturer ID, Memory Type, Capacity
byte manufacturer = SPI.transfer(0x00);
byte memType = SPI.transfer(0x00);
byte capacity = SPI.transfer(0x00);
digitalWrite(CS_PIN, HIGH); // Deassert Chip Select
SPI.endTransaction();
Serial.print("Manufacturer: 0x"); Serial.println(manufacturer, HEX);
Serial.print("Memory Type: 0x"); Serial.println(memType, HEX);
Serial.print("Capacity: 0x"); Serial.println(capacity, HEX);
delay(2000);
}
The Classic Failures: Debugging and Sniffing the Bus
Unlike I2C, where a missing 4.7k pull-up or an address clash halts the bus, SPI fails silently or returns garbage. Here are the three classic SPI failures and how to fix them.
1. CPOL and CPHA Mode Mismatch
SPI has four modes (0, 1, 2, 3) defined by Clock Polarity (CPOL) and Clock Phase (CPHA). If your master is in Mode 0 (clock idles LOW, sample on rising edge) and your slave expects Mode 3 (clock idles HIGH, sample on falling edge), you will read 0xFF or 0x00 for everything.
The Fix: Check the slave datasheet. If you don't have a scope, use a multimeter to measure the DC voltage on the SCK pin while the bus is idle. If it reads 0V, it's Mode 0 or 1. If it reads VCC, it's Mode 2 or 3.
2. Baud Rate Ringing and Reflections
If you push an ESP32 SPI bus to 40 MHz over 20cm of cheap breadboard jumper wires, the square wave will ring, causing the slave to see multiple clock edges and shifting your data bits.
The Fix: Drop the clock speed. Run SPISettings(4000000, MSBFIRST, SPI_MODE0) to test. If the data reads correctly at 4 MHz but fails at 20 MHz, your physical wiring has too much parasitic capacitance. Keep SPI traces under 15cm for speeds above 10 MHz.
3. Floating or Shared CS Lines
If you have multiple SPI devices on the same bus (e.g., an SD card and a TFT display), they must have individual CS wires routed back to the master. If you leave a CS pin floating, the slave will randomly wake up and drive the MISO line, colliding with the active device and shorting the bus.
The Fix: Enable internal pull-ups on all unused CS pins in setup() using pinMode(CS_PIN, INPUT_PULLUP) to keep them safely deselected until you explicitly configure them as outputs.
How to Sniff the Bus
When serial prints aren't enough, you need to see the physical layer. Use a Sigrok/PulseView compatible logic analyzer (the $12 8-channel Saleae clones on Amazon work perfectly). Hook up GND, SCK, MOSI, MISO, and CS. Set the sample rate to at least 4x your SPI clock speed (e.g., 100 MS/s for a 20 MHz bus). PulseView's built-in SPI decoder will automatically translate the hex bytes, letting you verify exactly what the master is sending versus what the datasheet demands.
Decision Tree: Which Protocol Wins Your Design?
Don't default to SPI just because it's fast. Use this decision matrix to lock in your architecture.
| Design Constraint | If True... | Choose This Protocol |
|---|---|---|
| Distance is > 2 meters | Signal integrity over long wire runs is required. | RS-485 or CAN bus |
| You have > 5 low-speed sensors (temp, humidity) | Running individual CS wires is a routing nightmare. | I2C (Use a multiplexer like TCA9548A if addresses clash) |
| Throughput must exceed 500 KB/s (TFTs, Audio, SD) | I2C and UART will bottleneck your frame rate or buffer. | Hardware SPI |
| Connecting to a PC or GPS module | Need asynchronous, point-to-point text/NMEA streams. | UART |
If your project requires high-speed local data transfer (SD cards, SPI Flash, ILI9341 TFT displays) and the devices are within 30cm of the microcontroller, the concrete pick is Hardware SPI on the native MCU pins. For the ESP32, utilize the ESP-IDF SPI Master driver if you need DMA-backed background transfers, but for standard Arduino framework builds, the native
SPI.h library with transaction blocks is your definitive starting point.






