What Is SPI (Serial Peripheral Interface) in Embedded Systems?

What is an SPI (Serial Peripheral Interface) in embedded systems?

Whenever I need speed — driving an SD card, a fast SPI flash chip, or a TFT display that needs to push a lot of pixel data quickly — SPI is what I reach for. It’s less economical on pins than I2C, but it more than makes up for that in raw throughput and simplicity of implementation. In this article I’ll go through how SPI actually works, how the four wires interact, how to configure it in code, and where it fits best in real embedded designs.

What Is SPI?

SPI (Serial Peripheral Interface) is a synchronous, full-duplex, master-slave serial communication protocol developed by Motorola in the 1980s. Like I2C, it uses a shared clock line generated by the master. Unlike I2C, SPI has no built-in addressing scheme — instead, each slave device gets its own dedicated Chip Select (CS) line, and only the slave whose CS line is active participates in the current transaction.

graph TD
    MCU[Microcontroller - Master] -->|MOSI| S1[SPI Flash]
    S1 -->|MISO| MCU
    MCU -->|SCK| S1
    MCU -->|CS1| S1
    MCU -->|MOSI| S2[SD Card]
    S2 -->|MISO| MCU
    MCU -->|SCK| S2
    MCU -->|CS2| S2

The Four Wires

  • MOSI (Master Out, Slave In) — data sent from master to slave.
  • MISO (Master In, Slave Out) — data sent from slave to master.
  • SCK (Serial Clock) — clock signal generated by the master.
  • CS/SS (Chip Select / Slave Select) — one line per slave, active-low, selecting which device is currently communicating.

Because MOSI and MISO are separate lines, SPI is inherently full-duplex — data can flow in both directions simultaneously on every clock pulse, unlike I2C’s half-duplex, shared SDA line.

How a Transaction Works

On every SCK pulse, one bit is shifted out on MOSI (from master to slave) and simultaneously one bit is shifted in on MISO (from slave to master). This happens through two shift registers — one in the master, one in the slave — effectively connected in a ring.

graph LR
    subgraph Master
    MSR[Shift Register]
    end
    subgraph Slave
    SSR[Shift Register]
    end
    MSR -->|MOSI| SSR
    SSR -->|MISO| MSR

Every SPI transfer, even a “read,” is technically a simultaneous send-and-receive: to read a byte from a slave, the master must clock out a byte (often a dummy 0x00 or 0xFF) while clocking in the response.

Clock Polarity and Phase (CPOL/CPHA)

SPI has four possible “modes,” defined by two settings:

  • CPOL (Clock Polarity) — whether the clock idles high (1) or low (0) when no data is being transferred.
  • CPHA (Clock Phase) — whether data is sampled on the first clock edge (0) or the second clock edge (1) after CS goes active.
ModeCPOLCPHADescription
000Clock idle low, sample on rising edge
101Clock idle low, sample on falling edge
210Clock idle high, sample on falling edge
311Clock idle high, sample on rising edge

Getting the mode wrong is one of the most common SPI bugs — the bus will often appear to “almost work,” returning shifted or garbled data, because master and slave are sampling at different points in the clock cycle. Always check the target device’s datasheet for its required mode.

Timing Diagram: SPI Mode 0 Transfer

sequenceDiagram
    participant CS
    participant SCK
    participant MOSI
    participant MISO
    CS->>CS: Goes LOW (select slave)
    loop 8 clock cycles
        SCK->>SCK: Rising edge - sample data
        MOSI->>MOSI: Bit driven, valid before rising edge
        MISO->>MISO: Bit driven, valid before rising edge
    end
    CS->>CS: Goes HIGH (deselect slave, end transfer)

Configuring SPI on an STM32 (HAL Example)

#include "stm32f4xx_hal.h"

SPI_HandleTypeDef hspi1;

void SPI1_Init(void) {
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;   // CPOL = 0
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;        // CPHA = 0  -> Mode 0
    hspi1.Init.NSS = SPI_NSS_SOFT;                // Software-controlled CS
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    HAL_SPI_Init(&hspi1);
}

uint8_t SPI1_Transfer(uint8_t data) {
    uint8_t rx;
    HAL_SPI_TransmitReceive(&hspi1, &data, &rx, 1, 100);
    return rx;
}

void CS_Select(void)   { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); }
void CS_Deselect(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);   }

Example: Reading a Register from an SPI Flash Chip

#define CMD_READ_STATUS  0x05

uint8_t flash_read_status(void) {
    uint8_t status;
    CS_Select();
    SPI1_Transfer(CMD_READ_STATUS);
    status = SPI1_Transfer(0xFF);   // Dummy byte to clock in response
    CS_Deselect();
    return status;
}

void flash_read_data(uint32_t addr, uint8_t *buf, uint16_t len) {
    CS_Select();
    SPI1_Transfer(0x03);                    // READ command
    SPI1_Transfer((addr >> 16) & 0xFF);
    SPI1_Transfer((addr >> 8) & 0xFF);
    SPI1_Transfer(addr & 0xFF);
    for (uint16_t i = 0; i < len; i++) {
        buf[i] = SPI1_Transfer(0xFF);
    }
    CS_Deselect();
}

Configuring SPI on Arduino

#include <SPI.h>

const int csPin = 10;

void setup() {
    pinMode(csPin, OUTPUT);
    digitalWrite(csPin, HIGH);
    SPI.begin();
    SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
}

uint8_t spiTransfer(uint8_t data) {
    digitalWrite(csPin, LOW);
    uint8_t response = SPI.transfer(data);
    digitalWrite(csPin, HIGH);
    return response;
}

Multiple Slaves on One SPI Bus

Since MOSI, MISO, and SCK are shared, adding more devices to an SPI bus only costs one extra GPIO pin per additional device (for its CS line) — the master simply asserts the correct CS line before initiating a transaction with that device, and deasserts all others.

graph TD
    MCU[Microcontroller] -->|Shared MOSI/MISO/SCK| Bus((SPI Bus Lines))
    Bus --- D1[Display - CS1]
    Bus --- D2[SD Card - CS2]
    Bus --- D3[Flash Memory - CS3]
    MCU -->|CS1| D1
    MCU -->|CS2| D2
    MCU -->|CS3| D3

This pin cost is the main practical trade-off compared to I2C — with eight SPI slaves you’d need eight dedicated CS pins (plus the three shared lines), whereas eight I2C slaves still use only two shared pins total.

DMA-Based SPI for High Throughput

For applications like driving a TFT/LCD display with thousands of pixels per frame, byte-by-byte polling would be far too slow. Using DMA, the SPI peripheral can stream an entire framebuffer out to the display with minimal CPU involvement:

HAL_SPI_Transmit_DMA(&hspi1, framebuffer, sizeof(framebuffer));

The CPU is freed to do other work (updating the next frame, handling touch input, etc.) while the DMA controller handles the actual byte-by-byte transfer in the background, only interrupting once the whole buffer has gone out.

SPI vs I2C vs UART

FeatureSPII2CUART
Wires4+ (MOSI, MISO, SCK, CS per device)2 (SDA, SCL)2 (TX, RX)
DuplexFullHalfFull
SpeedVery high (tens of MHz)Moderate (up to 3.4 MHz)Moderate
AddressingVia CS linesBuilt-in device addressNone
ComplexityLow-moderateModerateLow

Real-World Applications of SPI

  • SD/microSD cards: SD cards support an SPI mode alongside their native protocol, making them accessible from almost any microcontroller with basic SPI support.
  • TFT/OLED displays: ILI9341, ST7735, and similar display controllers use SPI for fast framebuffer updates.
  • External Flash/EEPROM: W25Q series SPI flash chips are extremely common for storing firmware images, logs, or configuration data.
  • Wireless modules: nRF24L01+ and similar RF transceivers use SPI for configuration and data transfer.
  • Sensors requiring high data rates: Some IMUs and ADCs offer SPI interfaces specifically for applications needing faster sampling than I2C could support.

IoT/Practical Example: Data Logger with SD Card

In a field data logger I built around an STM32, sensor readings were buffered in RAM and periodically flushed to an SD card over SPI using FatFs (a common embedded FAT filesystem library) layered on top of the SPI driver:

FRESULT write_log_entry(float temp, float humidity) {
    FIL file;
    char line[64];
    FRESULT res = f_open(&file, "log.csv", FA_OPEN_APPEND | FA_WRITE);
    if (res != FR_OK) return res;

    UINT bw;
    int len = snprintf(line, sizeof(line), "%.2f,%.2f\n", temp, humidity);
    f_write(&file, line, len, &bw);
    f_close(&file);
    return FR_OK;
}

FatFs issues its own sequence of SPI commands under the hood (via the low-level disk I/O layer you provide), which in turn call functions much like SPI1_Transfer() shown earlier.

Performance Considerations

SPI clock speeds are largely limited by the physical wiring (trace length, capacitance) and the target device’s maximum supported clock. Short, clean traces on a PCB can often run SPI at 20–50 MHz reliably; longer wires (breadboard jumper cables) usually need to be kept well under 1–4 MHz to avoid signal integrity issues, especially without proper termination.

Debugging SPI Issues

The recurring problems I run into, in order of frequency:

  1. Wrong SPI mode (CPOL/CPHA) — always the first thing I check when a device returns all zeros, all 0xFF, or consistently shifted/garbled data.
  2. CS timing — some devices are picky about CS needing to stay low for the entire multi-byte transaction, not toggled between bytes.
  3. MISO/MOSI swapped — an easy wiring mistake, especially when breakout boards label pins inconsistently.
  4. Clock speed too high for the wiring — works fine on a short jumper, fails intermittently on a longer cable; lowering the SPI clock often fixes “flaky” behavior instantly.

A logic analyzer with SPI decoding will show you CS, SCK, MOSI, and MISO all aligned in time, which makes mode mismatches and framing issues very easy to spot.

Security Considerations

Like I2C and UART, SPI has no built-in encryption or authentication. On products where sensitive data (like firmware images or credentials) sits in external SPI flash, that flash chip can often be desoldered or probed directly to extract its contents unless it’s encrypted at rest or the flash is inside a secure enclosure. For high-security designs, look at options like SPI flash with built-in encryption or verified/secure boot chains that check firmware integrity before execution.

Frequently Asked Questions

Can SPI slaves talk to each other directly? No — SPI is strictly master-controlled. All communication is initiated by the master; slaves cannot communicate with each other without the master relaying data between them.

Why does SPI need dummy bytes for reads? Because SPI is full-duplex and clock-driven by the master, every clock pulse shifts a bit in both directions. To receive data from a slave, the master must still generate clock pulses, which it does by transmitting dummy bytes (commonly 0x00 or 0xFF), even though the actual content of those dummy bytes doesn’t matter.

How many devices can share one SPI bus? There’s no strict protocol limit, but practically it’s limited by how many CS/GPIO pins your microcontroller can spare, plus signal integrity concerns as more devices load down the shared MOSI/MISO/SCK lines.

Is SPI always faster than I2C? In virtually all common implementations, yes — SPI’s typical operating range (several MHz to tens of MHz) is significantly faster than I2C’s typical range (100kHz–3.4MHz), which is why SPI is preferred for displays, flash memory, and other high-throughput peripherals.

Summary

SPI is a fast, full-duplex, master-driven serial protocol built around a shared clock and per-device chip-select lines, making it the go-to choice whenever throughput matters more than pin count — displays, SD cards, external flash, and high-speed sensors are classic use cases. Getting SPI right comes down to matching clock mode (CPOL/CPHA), managing chip-select timing carefully, and, for high-throughput use cases, offloading transfers to DMA so the CPU isn’t stuck shifting bytes one at a time. Once those fundamentals click, adding a new SPI peripheral to a design is usually a quick, predictable process.

References and Further Reading

  • Motorola/NXP SPI Block Guide (original specification reference) — nxp.com
  • STMicroelectronics STM32 SPI Peripheral Reference Manual — st.com
  • Arduino SPI Library Documentation — docs.arduino.cc/language-reference/en/functions/communication/spi
  • Espressif ESP32 SPI Master Driver Documentation — docs.espressif.com
  • Winbond W25Q Series SPI Flash Datasheets — winbond.com
  • FatFs Generic FAT Filesystem Module Documentation — elm-chan.org/fsw/ff
Total
0
Shares

Leave a Reply

Previous Post
What is an ADC (Analog-to-Digital Converter) in the context of embedded systems?

What Is an ADC (Analog-to-Digital Converter) in the Context of Embedded Systems?

Next Post
What is an I2C (Inter-Integrated Circuit) in embedded systems

What Is I2C (Inter-Integrated Circuit) in Embedded Systems

Related Posts