armhub.dev
Peripherals · 19 pages
Peripherals

SPI

Synchronous serial — clock polarity and phase, master/slave, multi-slave, full-duplex DMA, and the gotchas you only learn on the bench.

2 min read · 537 wordsupdated · May 12, 2026

SPI is the fast serial bus on Cortex-M. Up to half the peripheral clock as bit rate (so 42 MHz on an 84 MHz STM32F4 — though signal integrity usually limits you lower). Four wires, no addressing, point-to-point or one-master-many-slaves with chip-select gating.

The four signals

SignalDirection (master perspective)What it does
SCKoutputClock — drives shift
MOSIoutputMaster Out, Slave In
MISOinputMaster In, Slave Out
nCS / SSoutput (multi-CS: one per slave)Active-low chip select

SPI is full-duplex — every clock edge shifts a bit out of MOSI and a bit in on MISO simultaneously. You always send and receive the same number of bytes.

CPOL and CPHA — the four modes

The two timing knobs that trip everyone up:

  • CPOL (Clock Polarity) — 0 = SCK idles low, 1 = idles high.
  • CPHA (Clock Phase) — 0 = sample on first edge, 1 = sample on second edge.
ModeCPOLCPHASCK idleSampled on
000lowrising
101lowfalling
210highfalling
311highrising

Most peripherals are mode 0 or mode 3. Check the device datasheet first. A timing diagram in the datasheet that shows "data is valid on the rising edge of SCK and SCK idles low" → that's mode 0.

SPI mode 0 (CPOL=0, CPHA=0)
CSSCKMOSIMISOsample bit 0sample bit 1sample bit 2
CS frames the transaction, SCK idles low, and both master and slave latch the data line on the rising edge while the opposite side shifts on the falling edge.

Bit order

Most SPI peripherals send MSB first. A handful of older devices (SD cards in SPI mode use MSB; some sensors use LSB) flip it. STM32's LSBFIRST bit in CR1 selects the direction.

A full read/write transaction

uint8_t spi_xfer(uint8_t tx) {
    while (!(SPI1->SR & SPI_SR_TXE));    // wait for TX buffer empty
    *(volatile uint8_t *)&SPI1->DR = tx; // 8-bit write — IMPORTANT
    while (!(SPI1->SR & SPI_SR_RXNE));   // wait for RX byte
    return *(volatile uint8_t *)&SPI1->DR;
}
 
void read_id(uint8_t *out) {
    cs_low();
    spi_xfer(0x9F);                       // JEDEC ID command
    out[0] = spi_xfer(0xFF);              // dummy byte → manufacturer
    out[1] = spi_xfer(0xFF);              // type
    out[2] = spi_xfer(0xFF);              // capacity
    cs_high();
}

Multi-slave with software CS

The peripheral can drive one hardware CS pin. If you have multiple slaves on the same SPI bus, you drive each CS as a regular GPIO.

void with_slave(uint8_t cs_pin, void (*op)(void)) {
    GPIOA->BSRR = (1 << (cs_pin + 16));   // CS low
    op();
    GPIOA->BSRR = (1 << cs_pin);           // CS high
}

Keep the CS low across the entire transaction. Some chips reset state machines when CS goes high.

Full-duplex DMA

For long transfers, set up two DMA streams — one MOSI-write, one MISO-read — both triggered by the SPI peripheral's TX-empty / RX-not-empty events:

// TX DMA
DMA2_Stream3->PAR = (uint32_t)&SPI1->DR;
DMA2_Stream3->M0AR = (uint32_t)tx_buf;
DMA2_Stream3->NDTR = len;
DMA2_Stream3->CR  = DMA_SxCR_CHSEL_0 | DMA_SxCR_DIR_0 | DMA_SxCR_MINC | DMA_SxCR_EN;
 
// RX DMA
DMA2_Stream2->PAR = (uint32_t)&SPI1->DR;
DMA2_Stream2->M0AR = (uint32_t)rx_buf;
DMA2_Stream2->NDTR = len;
DMA2_Stream2->CR  = DMA_SxCR_CHSEL_0 | DMA_SxCR_MINC | DMA_SxCR_EN;
 
SPI1->CR2 |= SPI_CR2_TXDMAEN | SPI_CR2_RXDMAEN;
SPI1->CR1 |= SPI_CR1_SPE;

The RX DMA must be enabled before TX DMA or you'll lose the first byte (because TX completes faster than you can configure RX).

If you only need to send and discard, point the RX DMA at a 1-byte sink with MINC = 0 so all bytes overwrite the same location.

Slave mode

The same peripheral can be a slave. The master drives SCK and CS; your firmware just responds:

SPI1->CR1 = 0;                  // clear master mode bit
SPI1->CR1 |= SPI_CR1_SPE;

CS goes to NSS pin. The peripheral's TX FIFO must be pre-loaded before the master starts clocking — otherwise the first byte is whatever was in DR before (often 0xFF).

This is where DMA shines: keep a DMA continuously refilling the TX FIFO so the slave can respond at line rate.

QSPI / OctoSPI

For external flash, dedicated Quad SPI (4 data lines) or OctoSPI (8 data lines + DDR) peripherals run 4–16× faster than standard SPI and present the flash as memory-mapped — you can ldr directly from a memory-mapped flash address and the peripheral fetches transparently. Configuration is significantly more involved than regular SPI; vendors provide HAL drivers for the common flash command sets (Winbond, Macronix, Cypress).

See also

  • DMA — pairing SPI with DMA in depth.
  • GPIO — pin AF assignment.
  • Memory model — barriers around DR reads/writes.