Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
SPI Protocol — High-Speed Synchronous Serial
Complete STM32F407 SPI guide. CPOL/CPHA modes explained, CR1/CR2/SR/DR registers bit-by-bit, software/hardware NSS, full-duplex transfers, and W25Q32 flash memory example.
SPI Protocol Overview
SPI (Serial Peripheral Interface) uses 4 wires: SCK (clock), MOSI (Master Out Slave In), MISO (Master In Slave Out), and NSS/CS (Chip Select, active low). It’s a full-duplex protocol — master and slave exchange data simultaneously every clock edge.
Key advantages over I2C: much faster (up to 42 MHz on STM32F407), simpler protocol (no ACK/NACK/addressing), full-duplex. Disadvantage: needs one CS line per slave device, plus 3 shared signal wires.
SPI Instances on STM32F407
| Instance | Bus | IRQ# | Max Speed | MOSI | MISO | SCK |
|---|---|---|---|---|---|---|
| SPI1 | APB2 | 35 | 42 MHz | PA7 or PB5 (AF5) | PA6 or PB4 (AF5) | PA5 or PB3 (AF5) |
| SPI2 | APB1 | 36 | 21 MHz | PB15 or PC3 (AF5) | PB14 or PC2 (AF5) | PB13 or PB10 (AF5) |
| SPI3 | APB1 | 51 | 21 MHz | PB5 or PC12 (AF6) | PB4 or PC11 (AF6) | PB3 or PC10 (AF6) |
On the STM32F407 Discovery, SPI1 is used for the LIS3DSH accelerometer (PA5=SCK, PA6=MISO, PA7=MOSI, PE3=CS).
CPOL and CPHA — Clock Phase and Polarity
SPI has 4 modes defined by two bits: CPOL (clock polarity when idle) and CPHA (clock edge for data capture). You must match the slave device’s requirements — check its datasheet for “SPI Mode” or “CPOL/CPHA”.
Mode 0 — CPOL=0, CPHA=0 (most common)
Used by: SD cards, W25Q32, many sensors
Mode 1 — CPOL=0, CPHA=1
Used by: some ADCs
Mode 2 — CPOL=1, CPHA=0
Less common
Mode 3 — CPOL=1, CPHA=1
Used by: LIS3DSH accelerometer
SPI Registers — SPI1 Base: 0x40013000
SPI_CR1 — Control Register 1
| Bits | Name | Description |
|---|---|---|
| 0 | CPHA | Clock Phase. 0=first edge capture, 1=second edge capture. |
| 1 | CPOL | Clock Polarity. 0=idle LOW, 1=idle HIGH. |
| 2 | MSTR | Master Mode. 1=master (generates SCK). 0=slave. |
| [5:3] | BR[2:0] | Baud Rate Control. Sets SCK = f_PCLK / (2^(BR+1)). 000=÷2, 001=÷4, …, 111=÷256. |
| 6 | SPE | SPI Enable. Set LAST after all configuration. |
| 7 | LSBFIRST | Frame Format. 0=MSB first (standard), 1=LSB first. |
| 8 | SSI | Internal Slave Select. When SSM=1, this bit value is applied to NSS signal internally. |
| 9 | SSM | Software Slave Management. 1=NSS driven by SSI bit (software). 0=NSS pin used (hardware). |
| 10 | RXONLY | Receive Only. 1=disable MOSI output (half-duplex receive). |
| 11 | DFF | Data Frame Format. 0=8-bit, 1=16-bit frames. |
| 12 | CRCNEXT | CRC Transfer Next. Send CRC after next transfer. |
| 13 | CRCEN | Hardware CRC Enable. |
| 14 | BIDIOE | Output Enable in Bidirectional Mode. |
| 15 | BIDIMODE | Bidirectional Data Mode. 1=1-wire bidirectional. |
/* SPI baud rates from SPI1 on APB2 = 84 MHz: */
/* BR=000 → 84MHz/2 = 42 MHz */
/* BR=001 → 84MHz/4 = 21 MHz */
/* BR=010 → 84MHz/8 = 10.5 MHz */
/* BR=011 → 84MHz/16 = 5.25 MHz */
/* BR=100 → 84MHz/32 = 2.625 MHz */
/* BR=101 → 84MHz/64 = 1.3125 MHz */
/* BR=110 → 84MHz/128 = 656 kHz */
/* BR=111 → 84MHz/256 = 328 kHz */
#define SPI_BR_DIV2 (0b000 << 3)
#define SPI_BR_DIV4 (0b001 << 3)
#define SPI_BR_DIV8 (0b010 << 3)
#define SPI_BR_DIV16 (0b011 << 3)
#define SPI_BR_DIV32 (0b100 << 3)
#define SPI_BR_DIV64 (0b101 << 3)
#define SPI_BR_DIV128 (0b110 << 3)
#define SPI_BR_DIV256 (0b111 << 3)
SPI_SR — Status Register
| Bit | Name | Description |
|---|---|---|
| 0 | RXNE | Receive Buffer Not Empty. 1=data available in DR to read. |
| 1 | TXE | Transmit Buffer Empty. 1=DR is empty, can write next byte. |
| 4 | CRCERR | CRC Error flag. |
| 5 | MODF | Mode Fault. NSS went low while in master mode. |
| 6 | OVR | Overrun flag. New data arrived before old was read. |
| 7 | BSY | Busy flag. 1=SPI is transferring data. Do NOT disable SPI while BSY=1. |
| 8 | FRE | Frame format error (TI mode). |
SPI Initialization and Transfer Functions
/*
* SPI1 initialization — Mode 0 (CPOL=0, CPHA=0), 8-bit, MSB first
* PA5=SCK, PA6=MISO, PA7=MOSI (AF5), CS managed by software
* SPI1 on APB2 = 84MHz, BR=001 → 84/4 = 21 MHz
*/
#include <stdint.h>
#define RCC_AHB1ENR (*((volatile uint32_t*)0x40023830))
#define RCC_APB2ENR (*((volatile uint32_t*)0x40023844))
#define GPIOA_MODER (*((volatile uint32_t*)0x40020000))
#define GPIOA_OSPEEDR (*((volatile uint32_t*)0x40020008))
#define GPIOA_AFRL (*((volatile uint32_t*)0x40020020))
#define SPI1_BASE 0x40013000UL
#define SPI1_CR1 (*((volatile uint32_t*)(SPI1_BASE + 0x00)))
#define SPI1_CR2 (*((volatile uint32_t*)(SPI1_BASE + 0x04)))
#define SPI1_SR (*((volatile uint32_t*)(SPI1_BASE + 0x08)))
#define SPI1_DR (*((volatile uint32_t*)(SPI1_BASE + 0x0C)))
/* Software NSS — use any GPIO as CS */
#define GPIOA_BSRR (*((volatile uint32_t*)0x40020018))
#define CS_LOW() GPIOA_BSRR = (1 << (4+16)) /* PA4 low */
#define CS_HIGH() GPIOA_BSRR = (1 << 4) /* PA4 high */
void SPI1_Init(void) {
/* Enable clocks */
RCC_AHB1ENR |= (1<<0); /* GPIOA */
RCC_APB2ENR |= (1<<12); /* SPI1 */
/* PA4=CS output, PA5=SCK, PA6=MISO, PA7=MOSI */
/* PA4: output mode (10 in MODER) */
GPIOA_MODER &= ~((3<<8)|(3<<10)|(3<<12)|(3<<14));
GPIOA_MODER |= (1<<8)|(2<<10)|(2<<12)|(2<<14);
/* PA4=01 (output), PA5=10 (AF), PA6=10 (AF), PA7=10 (AF) */
/* High speed for all SPI pins */
GPIOA_OSPEEDR |= (3<<10)|(3<<12)|(3<<14);
/* AF5 (SPI1) for PA5, PA6, PA7 */
GPIOA_AFRL &= ~((0xF<<20)|(0xF<<24)|(0xF<<28));
GPIOA_AFRL |= (5<<20)|(5<<24)|(5<<28);
/* CS high (deselected) */
CS_HIGH();
/* SPI1 CR1 configuration */
SPI1_CR1 = 0; /* Reset */
SPI1_CR1 =
(0 << 0) | /* CPHA=0: first edge capture */
(0 << 1) | /* CPOL=0: idle LOW */
(1 << 2) | /* MSTR=1: master mode */
(1 << 3) | /* BR=001: 84MHz/4 = 21MHz */
(0 << 7) | /* LSBFIRST=0: MSB first */
(1 << 8) | /* SSI=1: NSS driven high internally */
(1 << 9) | /* SSM=1: software NSS management */
(0 << 11); /* DFF=0: 8-bit */
SPI1_CR2 = 0;
SPI1_CR1 |= (1 << 6); /* SPE=1: enable SPI (LAST) */
}
/* Send and receive 1 byte (full-duplex) */
uint8_t SPI1_Transfer(uint8_t tx) {
while (!(SPI1_SR & (1<<1))); /* Wait TXE */
SPI1_DR = tx; /* Write byte to transmit */
while (!(SPI1_SR & (1<<0))); /* Wait RXNE */
return (uint8_t)SPI1_DR; /* Read received byte */
}
/* Send multiple bytes — discard received data */
void SPI1_Write(const uint8_t* buf, uint32_t len) {
for (uint32_t i = 0; i < len; i++) SPI1_Transfer(buf[i]);
}
/* Read multiple bytes — send dummy 0xFF */
void SPI1_Read(uint8_t* buf, uint32_t len) {
for (uint32_t i = 0; i < len; i++) buf[i] = SPI1_Transfer(0xFF);
}
/* IMPORTANT: Wait for BSY=0 before raising CS */
void SPI1_CS_Deassert(void) {
while (SPI1_SR & (1<<7)); /* Wait BSY=0 (transfer complete) */
CS_HIGH();
}
NSS — Chip Select Management
STM32 SPI NSS can be managed two ways: software (SSM=1) or hardware (SSM=0). For most applications with multiple slave devices, software NSS is preferable — you manually control GPIO pins and can select any device.
| Mode | SSM bit | NSS pin usage | Best for |
|---|---|---|---|
| Software NSS (SSM=1) | 1 | NSS pin unused — use any GPIO as CS | Multiple slaves, most cases |
| Hardware NSS output (SSM=0, SSOE=1) | 0 | NSS automatically driven LOW during transfer, HIGH after | Single slave, DMA-driven SPI |
| Hardware NSS input (SSM=0, SSOE=0) | 0 | NSS monitors for multi-master conflicts | Multi-master environments |
/* Hardware NSS output (SSOE=1 in CR2) — single slave */
SPI1_CR1 &= ~(1<<9); /* SSM=0: hardware NSS */
SPI1_CR2 |= (1<<2); /* SSOE=1: NSS output enabled */
/* NSS (PA4 on SPI1) goes LOW automatically when SPE=1 and transfer starts */
/* NSS goes HIGH automatically after STOP */
/* Software NSS with multiple slaves */
/* Define CS for each slave separately: */
#define FLASH_CS_LOW() GPIOA_BSRR = (1<<(4+16)) /* PA4 */
#define FLASH_CS_HIGH() GPIOA_BSRR = (1<<4)
#define ACCEL_CS_LOW() GPIOE_BSRR = (1<<(3+16)) /* PE3 (LIS3DSH) */
#define ACCEL_CS_HIGH() GPIOE_BSRR = (1<<3)
W25Q32 SPI Flash Memory Example
W25Q32 is a 32-Mbit (4MB) SPI NOR flash. It uses SPI Mode 0 or Mode 3, up to 80 MHz. Basic operations: Read (opcode 0x03), Read JEDEC ID (0x9F), Write Enable (0x06), Page Program (0x02), Sector Erase (0x20).
/* W25Q32 command opcodes */
#define W25_CMD_READ_ID 0x9F /* Read JEDEC ID: EF 15 xx (manufacturer+density) */
#define W25_CMD_READ_SR 0x05 /* Read Status Register */
#define W25_CMD_WRITE_EN 0x06 /* Write Enable (must call before write/erase) */
#define W25_CMD_READ 0x03 /* Read Data (24-bit address) */
#define W25_CMD_PAGE_PROG 0x02 /* Page Program (write up to 256 bytes) */
#define W25_CMD_SECTOR_ERASE 0x20 /* Erase 4KB sector */
#define W25_SR_BUSY 0x01 /* Status Register BUSY bit */
void W25_WaitBusy(void) {
uint8_t sr;
do {
CS_LOW();
SPI1_Transfer(W25_CMD_READ_SR);
sr = SPI1_Transfer(0xFF);
SPI1_CS_Deassert();
} while (sr & W25_SR_BUSY);
}
uint32_t W25_ReadID(void) {
CS_LOW();
SPI1_Transfer(W25_CMD_READ_ID);
uint8_t mfr = SPI1_Transfer(0xFF);
uint8_t type = SPI1_Transfer(0xFF);
uint8_t cap = SPI1_Transfer(0xFF);
SPI1_CS_Deassert();
return ((uint32_t)mfr << 16) | ((uint32_t)type << 8) | cap;
/* W25Q32: returns 0xEF4016 */
}
void W25_Read(uint32_t addr, uint8_t* buf, uint32_t len) {
CS_LOW();
SPI1_Transfer(W25_CMD_READ);
SPI1_Transfer((addr >> 16) & 0xFF); /* Address byte 2 (MSB) */
SPI1_Transfer((addr >> 8) & 0xFF); /* Address byte 1 */
SPI1_Transfer((addr ) & 0xFF); /* Address byte 0 (LSB) */
SPI1_Read(buf, len);
SPI1_CS_Deassert();
}
void W25_WritePage(uint32_t addr, const uint8_t* data, uint32_t len) {
/* Write Enable first — required before any write/erase */
CS_LOW();
SPI1_Transfer(W25_CMD_WRITE_EN);
SPI1_CS_Deassert();
CS_LOW();
SPI1_Transfer(W25_CMD_PAGE_PROG);
SPI1_Transfer((addr >> 16) & 0xFF);
SPI1_Transfer((addr >> 8) & 0xFF);
SPI1_Transfer((addr ) & 0xFF);
SPI1_Write(data, len);
SPI1_CS_Deassert();
W25_WaitBusy(); /* Page program takes ~0.7ms */
}
void W25_SectorErase(uint32_t addr) {
CS_LOW();
SPI1_Transfer(W25_CMD_WRITE_EN);
SPI1_CS_Deassert();
CS_LOW();
SPI1_Transfer(W25_CMD_SECTOR_ERASE);
SPI1_Transfer((addr >> 16) & 0xFF);
SPI1_Transfer((addr >> 8) & 0xFF);
SPI1_Transfer((addr ) & 0xFF);
SPI1_CS_Deassert();
W25_WaitBusy(); /* Sector erase takes ~50ms */
}
int main(void) {
SystemClock_Config();
SysTick_Init(168000000);
USART1_Init(84000000, 115200);
SPI1_Init();
uint32_t id = W25_ReadID();
printf("Flash ID: 0x%06lX\r\n", id); /* Should be 0xEF4016 for W25Q32 */
uint8_t buf[16];
W25_Read(0x000000, buf, 16); /* Read first 16 bytes */
while(1) {}
}
Summary
SPI_CR1
CPOL/CPHA select mode. BR[2:0] set speed. MSTR=1 for master. SPE=1 to enable (last).
SPI_SR
TXE(1): can write. RXNE(0): data ready. BSY(7): transfer in progress. Wait BSY=0 before raising CS.
SPI Transfer
Wait TXE, write DR, wait RXNE, read DR. Always full-duplex — master clocks in and out simultaneously.
NSS Software
SSM=1, SSI=1 in CR1. Control CS manually via GPIO BSRR for multiple slaves.
Mode selection
Mode 0 (CPOL=0,CPHA=0): most sensors. Mode 3 (CPOL=1,CPHA=1): LIS3DSH accelerometer.
Speed
BR=000: APB_clk/2. SPI1 max = 42 MHz. SPI2/3 max = 21 MHz (APB1-based).
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments