SPI — Serial Peripheral Interface

SPI is the fast, full-duplex synchronous bus used for Flash memories, displays, ADCs, SD cards and radios. Learn the four signals (SCLK, MOSI, MISO, CS), master/slave roles, the four clock modes (CPOL/CPHA), full-duplex shifting, chip-select handling, daisy-chaining, DMA transfers, Quad-SPI, signal-integrity limits and a real Flash-memory transaction.

SPI has no addresses, no acknowledgements and no fixed protocol — just a clock driven by the master and two data lines that shift simultaneously. That simplicity makes it fast (tens of MHz) and easy to implement, even by bit-banging GPIOs.

Each slave gets its own chip-select (CS) line. The device's datasheet defines the command set: for a SPI Flash, send a command byte, address bytes, then read or write data.

An analogy

SPI is two conveyor belts running in opposite directions, driven by the same motor (the clock). Every time the master pushes one bit onto its belt, it receives one bit from the slave's belt — you can't send without receiving.

Signals and roles

The master generates SCLK and selects one slave by pulling its CS low. On every clock, one bit goes out on MOSI and one comes back on MISO. When CS is high, the slave ignores the clock and releases MISO (high-impedance), so several slaves can share SCLK/MOSI/MISO.

The four SPI modes

CPOL sets the idle clock level; CPHA sets whether data is sampled on the first or second edge. Master and slave must match — a wrong mode typically shifts data by one bit (values doubled/halved) or gives garbage. Mode 0 and mode 3 are the most common; the datasheet timing diagram tells you which.

Performance: DMA and multi-I/O

At 20+ MHz the CPU can't keep up byte-by-byte, so use DMA for display frames and Flash pages. Dual/Quad-SPI send 2 or 4 bits per clock (Quad at 80 MHz ≈ 40 MB/s) and many MCUs can memory-map a QSPI Flash to execute code directly (XIP).

Signal integrity and system design

SPI was designed for on-board use. Long wires add propagation delay: the MISO data returns late relative to the master's sampling edge, limiting clock speed. Keep traces short, add series termination (22–33 Ω) at drivers, and reduce clock for cables. With many slaves, use a decoder for CS lines or daisy-chain devices (shift registers, LED drivers) so data passes through all of them in one long transfer.

Formulas

Worked example: Flash read time

Given: 20 MHz SCLK; READ 0x03 + 3 address bytes + 256 data bytes

Find: Transfer time.

Answer: 104 µs (≈ 2.5 MB/s)

Worked example: Wrong mode symptom

Given: ADC should return 0x40, MCU reads 0x80

Find: Likely cause.

Answer: SPI mode (CPHA) mismatch

Worked example: Display frame rate

Given: 240×240 RGB565 display, 40 MHz SPI

Find: Max full-frame rate.

Answer: ≈ 43 fps (before overheads)

Reading a SPI Flash (W25Q-series) with HAL

#define FLASH_CS_LOW()   HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define FLASH_CS_HIGH()  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

uint32_t flash_read_jedec_id(void)
{
    uint8_t tx[4] = { 0x9F, 0, 0, 0 }, rx[4];
    FLASH_CS_LOW();
    HAL_SPI_TransmitReceive(&hspi1, tx, rx, 4, 10);   /* full duplex      */
    FLASH_CS_HIGH();
    return (rx[1] << 16) | (rx[2] << 8) | rx[3];      /* e.g. 0xEF4018 = Winbond 128 Mbit */
}

void flash_read(uint32_t addr, uint8_t *dst, uint16_t len)
{
    uint8_t cmd[4] = { 0x03, addr >> 16, addr >> 8, addr };   /* READ + 24-bit address */
    FLASH_CS_LOW();
    HAL_SPI_Transmit(&hspi1, cmd, 4, 10);
    HAL_SPI_Receive(&hspi1, dst, len, 100);           /* clocks out dummy 0x00s */
    FLASH_CS_HIGH();
}

Reading the JEDEC ID is the standard first bring-up test: if you get 0xFFFFFF or 0x000000, check wiring, CS, mode and clock. Note the first received byte (during the command) is meaningless.

Bit-banged SPI mode 0 (portable fallback)

uint8_t spi_bb_transfer(uint8_t out)
{
    uint8_t in = 0;
    for (int i = 7; i >= 0; i--) {             /* MSB first            */
        gpio_write(MOSI, (out >> i) & 1u);     /* set data while SCLK low */
        delay_half_bit();
        gpio_write(SCLK, 1);                   /* rising edge: both sample */
        in = (uint8_t)((in << 1) | gpio_read(MISO));
        delay_half_bit();
        gpio_write(SCLK, 0);                   /* falling edge: shift     */
    }
    return in;
}

Mode 0 (CPOL 0, CPHA 0): data changes on the falling edge and is sampled on the rising edge. Writing it by hand makes the mode diagram concrete.

Common mistakes

Applications

Key takeaways

Going further