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.
- Mode 0: idle low, sample rising
- Mode 1: idle low, sample falling
- Mode 2: idle high, sample falling
- Mode 3: idle high, sample rising
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
- Raw throughput:
Bytes/s = f_SCLK / 8 (single I/O) → ×4 for Quad-SPI - Transaction time:
t = (command + address + dummy + data bytes) × 8 / f_SCLK + CS overhead - Max clock (timing):
f_max ≈ 1 / (2 × (t_prop,round-trip + t_setup + t_clk→Q)) - Mode number:
Mode = 2·CPOL + CPHA (Mode 0: CPOL 0, CPHA 0)
Worked example: Flash read time
Given: 20 MHz SCLK; READ 0x03 + 3 address bytes + 256 data bytes
Find: Transfer time.
- (4 + 256) × 8 = 2080 bits
- 2080 / 20 MHz
Answer: 104 µs (≈ 2.5 MB/s)
Worked example: Wrong mode symptom
Given: ADC should return 0x40, MCU reads 0x80
Find: Likely cause.
- Value doubled = shifted one bit left
- Sampling on the wrong edge
Answer: SPI mode (CPHA) mismatch
Worked example: Display frame rate
Given: 240×240 RGB565 display, 40 MHz SPI
Find: Max full-frame rate.
- Frame = 240×240×2 = 115 200 B = 921 600 bits
- 40 MHz / 921 600
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
- Wrong: Toggling CS between bytes of one command. — Right: Keep CS low for the whole transaction.
- Wrong: Mismatched CPOL/CPHA. — Right: Read the slave's timing diagram and configure the master to match.
- Wrong: Forgetting that every TX clocks in RX data. — Right: Discard or use received bytes; flush RX before reading.
- Wrong: Running full speed over long wires. — Right: Lower clock, add series termination, keep traces short.
Applications
- Storage: NOR/NAND Flash, SD cards in SPI mode, FRAM.
- Displays: TFT/OLED controllers with DMA frame transfers.
- Data converters: High-speed ADCs and DACs.
- Radios & sensors: LoRa, nRF24, IMUs, Ethernet controllers (W5500).
Key takeaways
- 4 wires: SCLK, MOSI, MISO, CS.
- Match the mode (CPOL/CPHA).
- Every transfer is full-duplex.
- Fast and simple but short-range; use DMA for bulk data.
Going further
- Quad-SPI/Octo-SPI XIP: Execute code directly from external Flash.
- SPI slave mode: MCU as slave needs fast DMA to meet unknown master timing.
- Isolated SPI: Digital isolators for high-voltage measurement (BMS).