Skip to content

Part 2 · C Meets the Metal39 min read

SPI and I2C

Almost every sensor, memory chip, display and codec you will ever connect to a microcontroller speaks SPI or I2C. In this lesson you will configure both peripherals on the STM32F407 directly from registers, read the LIS3DSH accelerometer over SPI and the CS43L22 audio DAC over I2C on the Discovery board, and wrap both buses behind one regdev_read() interface so sensor drivers never care which wire they sit on. You will also learn what a healthy transaction looks like on a logic analyzer and how to rescue an I2C bus that a slave is holding low.

SPI (Serial Peripheral Interface) is a synchronous, full-duplex shift register shared between two chips. The master drives four signals:

SignalDirectionMeaning
SCKmaster to slaveClock. The master generates every edge.
MOSImaster to slaveMaster Out, Slave In data.
MISOslave to masterMaster In, Slave Out data. Tri-stated by unselected slaves.
CS (or NSS, SS)master to slaveChip select, active low. Frames a transaction.

There is no addressing, no acknowledgment, no start or stop symbol. The slave listens while its CS is low and ignores everything while CS is high. That simplicity is why SPI runs at tens of MHz while I2C usually tops out at 400 kHz.

Inside both chips sits an 8-bit shift register. Each clock pulse moves one bit from the master’s register out on MOSI and one bit from the slave’s register in on MISO. After eight clocks the two registers have swapped contents.

master slave
+---------------+ MOSI +---------------+
| 7 6 5 4 3 2 1 0 |------->| 7 6 5 4 3 2 1 0 |
+---------------+ +---------------+
^ |
| MISO |
+----------------------------+
SCK (from master) clocks both

The consequence: you cannot read without writing. To receive a byte the master must clock out a dummy byte (usually 0xFF or 0x00). And when you only want to write, a byte still arrives on every transfer, and if you never read it the receive buffer overruns. Every SPI driver bug where “the second read returns the first value” comes from forgetting this.

Two bits decide which clock edge the data is sampled on:

  • CPOL: the idle level of SCK (0 = idle low, 1 = idle high).
  • CPHA: 0 = sample on the first edge after CS falls, 1 = sample on the second edge.
ModeCPOLCPHAIdle SCKSample edgeShift edge
000lowrisingfalling
101lowfallingrising
210highfallingrising
311highrisingfalling
Mode 0 and mode 3, MSB first, sending 0xA5 (1010 0101)
CS ‾‾‾\_____________________________________________/‾‾‾
SCK m0 ___/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_____________
SCK m3 ‾‾‾‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾‾‾‾‾‾‾‾‾‾‾
MOSI ---< 1 >< 0 >< 1 >< 0 >< 0 >< 1 >< 0 >< 1 >----
^ sampled on each rising edge in both modes

Modes 0 and 3 both sample on the rising edge, which is why many devices accept either. The device datasheet tells you which modes it supports, usually with a timing diagram rather than a mode number. Read the diagram: where is SCK when CS falls, and on which edge does the slave latch MOSI?

Bit order is almost always MSB first. Some devices (a few LCD controllers and shift registers) want LSB first, which the STM32 supports with LSBFIRST.

CS is not just “enable”. For most devices the falling edge resets the slave’s internal state machine and the rising edge commits the operation. Rules that hold for nearly every part:

  1. CS must stay low for the whole transaction, including the command byte and all data bytes.
  2. Do not release CS until the last bit has actually left the shift register, which is later than “I wrote the last byte to DR”.
  3. Many parts need CS high between commands to start a new one. Flash chips, for example, only begin a page program on the rising edge of CS.
  4. Drive CS from a plain GPIO and set it high before switching the pin to output, so the slave never sees a glitch at boot.

With several slaves you share SCK, MOSI and MISO and give each slave its own CS line. Only the selected slave drives MISO; the others hold it in high impedance. A slave that does not tri-state MISO properly (cheap modules sometimes have a level shifter that drives it always) breaks the whole bus.

In a daisy chain you connect the MISO of one device to the MOSI of the next and share a single CS. Clocking 24 bits through three 8-bit shift registers loads all three; the rising CS edge latches them simultaneously. LED drivers and 74HC595 shift registers are designed for this.

SPI1 lives on APB2 at 0x40013000, enabled by RCC_APB2ENR bit 12. On the Discovery it is wired to the accelerometer: PA5 = SCK, PA6 = MISO, PA7 = MOSI (all AF5), and chip select on PE3 driven as a GPIO.

OffsetRegisterWhat you use
0x00CR1mode, master, baud prescaler, enable
0x04CR2interrupt and DMA enables
0x08SRRXNE, TXE, BSY, OVR, MODF
0x0CDRdata, write to transmit, read to receive

The CR1 bits you need (RM0090 positions):

BitNameMeaning
0CPHAclock phase
1CPOLclock polarity
2MSTR1 = master
5:3BRbaud rate = fPCLK / 2^(BR+1): 000 = /2, 001 = /4, … 111 = /256
6SPEperipheral enable
7LSBFIRSTbit order
8SSIinternal slave select level when SSM = 1
9SSMsoftware slave management
11DFF0 = 8-bit frames, 1 = 16-bit

SSM and SSI deserve a sentence. The SPI peripheral has a hardware NSS input. In master mode, if NSS is seen low, the peripheral assumes another master has taken the bus, raises a mode fault (MODF) and silently clears MSTR and SPE. Setting SSM = 1 disconnects the pin and SSI = 1 tells the peripheral “NSS is high”, so it stays master. Forget SSI and your SPI works for zero bytes, then SPE reads back as 0. That is the most common STM32 SPI bring-up bug.

The SR bits: RXNE (bit 0) a byte is waiting in DR; TXE (bit 1) the transmit buffer is empty; OVR (bit 6) a received byte was lost; BSY (bit 7) the shift register is still busy.

Before the driver, two small helpers used through the rest of Part 2. First, the register definitions this lesson needs. Append them to the stm32f407_regs.h you wrote in Memory-mapped registers in C. The guards make the block harmless if your header already defines some of them.

stm32f407_regs.h (additions)
/* ---- GPIO ports beyond A and D ---- */
#ifndef GPIOB
#define GPIOB ((GPIO_TypeDef *)0x40020400U)
#endif
#ifndef GPIOE
#define GPIOE ((GPIO_TypeDef *)0x40021000U)
#endif
/* ---- SPI ---- */
#ifndef SPI1
typedef struct {
volatile uint32_t CR1; /* 0x00 */
volatile uint32_t CR2; /* 0x04 */
volatile uint32_t SR; /* 0x08 */
volatile uint32_t DR; /* 0x0C */
volatile uint32_t CRCPR; /* 0x10 */
volatile uint32_t RXCRCR; /* 0x14 */
volatile uint32_t TXCRCR; /* 0x18 */
volatile uint32_t I2SCFGR; /* 0x1C */
volatile uint32_t I2SPR; /* 0x20 */
} SPI_TypeDef;
#define SPI1 ((SPI_TypeDef *)0x40013000U)
#endif
/* ---- I2C ---- */
#ifndef I2C1
typedef struct {
volatile uint32_t CR1; /* 0x00 */
volatile uint32_t CR2; /* 0x04 */
volatile uint32_t OAR1; /* 0x08 */
volatile uint32_t OAR2; /* 0x0C */
volatile uint32_t DR; /* 0x10 */
volatile uint32_t SR1; /* 0x14 */
volatile uint32_t SR2; /* 0x18 */
volatile uint32_t CCR; /* 0x1C */
volatile uint32_t TRISE; /* 0x20 */
volatile uint32_t FLTR; /* 0x24 */
} I2C_TypeDef;
#define I2C1 ((I2C_TypeDef *)0x40005400U)
#endif
/* ---- RCC enable bits used from here on ---- */
#define RCC_AHB1ENR_GPIOAEN (1u << 0)
#define RCC_AHB1ENR_GPIOBEN (1u << 1)
#define RCC_AHB1ENR_GPIODEN (1u << 3)
#define RCC_AHB1ENR_GPIOEEN (1u << 4)
#define RCC_APB1ENR_USART2EN (1u << 17)
#define RCC_APB1ENR_I2C1EN (1u << 21)
#define RCC_APB2ENR_SPI1EN (1u << 12)

If your header already defines any of the RCC_ bit macros with the same values, delete the duplicates here; identical redefinitions are legal C, but different spellings of the same value will trigger a warning.

Second, pin helpers. They assume the CMSIS layout where the two alternate-function registers are an array AFR[2] (AFRL at 0x20, AFRH at 0x24).

gpio_util.h
#ifndef GPIO_UTIL_H
#define GPIO_UTIL_H
#include <stdint.h>
#include "stm32f407_regs.h"
enum { GPIO_IN = 0u, GPIO_OUT = 1u, GPIO_AF = 2u, GPIO_ANALOG = 3u };
static inline void gpio_mode(GPIO_TypeDef *p, uint32_t pin, uint32_t mode)
{
p->MODER = (p->MODER & ~(3u << (pin * 2u))) | (mode << (pin * 2u));
}
static inline void gpio_af(GPIO_TypeDef *p, uint32_t pin, uint32_t af)
{
uint32_t idx = pin >> 3;
uint32_t sh = (pin & 7u) * 4u;
p->AFR[idx] = (p->AFR[idx] & ~(0xFu << sh)) | (af << sh);
gpio_mode(p, pin, GPIO_AF);
}
static inline void gpio_set(GPIO_TypeDef *p, uint32_t pin) { p->BSRR = 1u << pin; }
static inline void gpio_clear(GPIO_TypeDef *p, uint32_t pin) { p->BSRR = 1u << (pin + 16u); }
#endif

gpio_af writes the AF number before switching the mode, so the pin never briefly drives the wrong alternate function. Finally, a polled console so printf works. This is the minimal version of what you built in UART: serial communication, running from the 16 MHz HSI that the chip boots with.

console.c
#include <stdio.h>
#include <stdint.h>
#include "stm32f407_regs.h"
#include "gpio_util.h"
void console_init(void);
int _write(int fd, char *buf, int len);
void console_init(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
(void)RCC->APB1ENR; /* let the clock enable settle */
gpio_af(GPIOA, 2u, 7u); /* PA2 = USART2_TX, AF7 */
USART2->BRR = (8u << 4) | 11u; /* 16 MHz / (16 * 8.6875) = 115107 baud */
USART2->CR1 = (1u << 13) | (1u << 3); /* UE | TE */
setvbuf(stdout, NULL, _IONBF, 0); /* newlib would otherwise buffer */
}
int _write(int fd, char *buf, int len)
{
(void)fd;
for (int i = 0; i < len; i++) {
if (buf[i] == '\n') {
while ((USART2->SR & (1u << 7)) == 0u) { } /* TXE */
USART2->DR = '\r';
}
while ((USART2->SR & (1u << 7)) == 0u) { }
USART2->DR = (uint8_t)buf[i];
}
return len;
}
spi.h
#ifndef SPI_H
#define SPI_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "stm32f407_regs.h"
#define SPI_CR1_CPHA (1u << 0)
#define SPI_CR1_CPOL (1u << 1)
#define SPI_CR1_MSTR (1u << 2)
#define SPI_CR1_BR_Pos 3u
#define SPI_CR1_SPE (1u << 6)
#define SPI_CR1_SSI (1u << 8)
#define SPI_CR1_SSM (1u << 9)
#define SPI_SR_RXNE (1u << 0)
#define SPI_SR_TXE (1u << 1)
#define SPI_SR_OVR (1u << 6)
#define SPI_SR_BSY (1u << 7)
typedef struct {
SPI_TypeDef *spi;
GPIO_TypeDef *cs_port;
uint32_t cs_pin;
} spi_dev_t;
void spi1_init(void);
bool spi_transfer(SPI_TypeDef *spi, const uint8_t *tx, uint8_t *rx, size_t len);
void spi_cs_assert(const spi_dev_t *d);
void spi_cs_release(const spi_dev_t *d);
#endif
spi.c
#include "spi.h"
#include "gpio_util.h"
#define SPI_TIMEOUT 100000u
static bool wait_flag(volatile uint32_t *reg, uint32_t mask, bool want_set)
{
for (uint32_t n = SPI_TIMEOUT; n != 0u; n--) {
bool is_set = (*reg & mask) != 0u;
if (is_set == want_set) {
return true;
}
}
return false;
}
void spi1_init(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOEEN;
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
(void)RCC->APB2ENR;
gpio_af(GPIOA, 5u, 5u); /* SCK */
gpio_af(GPIOA, 6u, 5u); /* MISO */
gpio_af(GPIOA, 7u, 5u); /* MOSI */
gpio_set(GPIOE, 3u); /* CS high first ... */
gpio_mode(GPIOE, 3u, GPIO_OUT); /* ... then make it an output */
SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI
| SPI_CR1_CPOL | SPI_CR1_CPHA /* mode 3 */
| (1u << SPI_CR1_BR_Pos); /* 16 MHz / 4 = 4 MHz */
SPI1->CR1 |= SPI_CR1_SPE;
}
bool spi_transfer(SPI_TypeDef *spi, const uint8_t *tx, uint8_t *rx, size_t len)
{
for (size_t i = 0; i < len; i++) {
if (!wait_flag(&spi->SR, SPI_SR_TXE, true)) {
return false;
}
spi->DR = (tx != NULL) ? tx[i] : 0xFFu;
if (!wait_flag(&spi->SR, SPI_SR_RXNE, true)) {
return false;
}
uint8_t b = (uint8_t)spi->DR; /* always read, even if discarded */
if (rx != NULL) {
rx[i] = b;
}
}
return wait_flag(&spi->SR, SPI_SR_BSY, false);
}
void spi_cs_assert(const spi_dev_t *d) { gpio_clear(d->cs_port, d->cs_pin); }
void spi_cs_release(const spi_dev_t *d) { gpio_set(d->cs_port, d->cs_pin); }

The loop is the full-duplex rule in code: write one byte, wait for one byte, read it. tx == NULL means “clock out dummies”, rx == NULL means “throw the received bytes away”, but the read of DR still happens because reading DR is what clears RXNE. Skip it and on the next byte OVR sets and the data you read later is stale.

The final wait on BSY matters for CS discipline. RXNE sets when the last bit is sampled, but in mode 3 there is still half a clock cycle before SCK returns idle. Waiting for BSY = 0 before releasing CS guarantees the slave sees a clean end of frame.

This one-byte-in-flight design wastes the gap between bytes: the bus idles while the CPU loops. At 4 MHz that is fine. For a display or a flash chip at 21 MHz you would pipeline (write the next byte as soon as TXE sets, before reading) or hand the job to DMA, which is the subject of the next lesson.

Every wait has a timeout. A peripheral whose clock is not enabled, or a MODF that cleared SPE, would otherwise hang the firmware forever in a while with no clue where.

The Discovery’s LIS3DSH answers on SPI1 with CS on PE3. Its protocol is the standard “register device over SPI” pattern:

CS ‾\________________________________________/‾
MOSI [ R/W | A6..A0 ][ dummy ][ dummy ] ...
MISO [ ignore ][ data0 ][ data1 ] ...

The first byte is the register address with bit 7 set for a read (0x80) and clear for a write. Then each further byte clocks one register’s data in or out.

RegisterAddressUse
WHO_AM_I0x0Falways reads 0x3F
CTRL_REG40x20output data rate and axis enables; 0x67 = 100 Hz, X/Y/Z on
CTRL_REG60x25bit 4 ADD_INC enables address auto-increment for multi-byte reads
OUT_X_L .. OUT_Z_H0x28..0x2D16-bit two’s complement samples, little endian

Reading WHO_AM_I first is not ceremony. It proves the clocks, the pins, the SPI mode and the CS line all at once. If you get 0xFF, MISO is floating (no slave is driving it: wrong CS, wrong pin, slave unpowered). If you get 0x00, MISO is stuck low or the clock never reached the slave. If you get a shifted value like 0x7E or 0x1F, the mode is wrong and you are sampling on the wrong edge.

I2C (Inter-Integrated Circuit) uses two lines, SCL (clock) and SDA (data), shared by every device on the bus. Both lines are open-drain: a device can pull the line low or let go, never drive it high. A pull-up resistor per line restores the high level. This gives the “wired-AND” property: if anyone pulls low, the line is low. Arbitration, acknowledgment and clock stretching are all built on it.

The pull-up resistor and the bus capacitance form an RC circuit that sets the rise time. Too large a resistor and the edges are too slow; too small and devices cannot sink enough current to pull the line below the low threshold.

  • Maximum: Rp_max = t_r / (0.8473 * C_bus). At 100 kHz the spec allows t_r = 1000 ns; with 200 pF of bus capacitance that gives about 5.9 kΩ. At 400 kHz (t_r = 300 ns) it gives about 1.8 kΩ.
  • Minimum: Rp_min = (VDD - 0.4 V) / 3 mA, about 970 Ω at 3.3 V.

4.7 kΩ for a short 100 kHz bus and 2.2 kΩ for 400 kHz are the usual picks. The STM32’s internal pull-ups (around 40 kΩ) are far too weak for anything but a few centimetres at low speed. The Discovery fits external pull-ups on PB6/PB9 for the audio DAC; on your own boards you must provide them.

SDA may only change while SCL is low. The two exceptions are the framing symbols:

  • START: SDA falls while SCL is high.
  • STOP: SDA rises while SCL is high.

After START the master sends a byte holding the 7-bit address in bits 7:1 and the R/W bit in bit 0 (0 = write, 1 = read). Every byte is followed by a ninth clock in which the receiver drives SDA: low is ACK, high (nobody pulling) is NACK.

Write 0x01 to register 0x20 of device 0x4A
S 0x94 (0x4A<<1 | W) A 0x20 (reg) A 0x01 (data) A P
SDA ‾\_[1 0 0 1 0 1 0 0]_[A]_[0 0 1 0 0 0 0 0]_[A]_[0 0 0 0 0 0 0 1]_[A]_/‾
SCL ‾‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_ ... _/‾‾‾
^ slave pulls SDA low on the 9th clock

Reading a register from a register-style device takes two phases joined by a repeated START (a START without a STOP before it, so no other master can grab the bus in between):

S [addr|W] A [reg] A Sr [addr|R] A [data0] A [data1] A ... [dataN] NACK P
master ------------> master ------> slave -------------------->

The master ACKs every byte it wants more of and NACKs the last one, which tells the slave to release SDA so the master can generate STOP.

A NACK on the address byte means no device has that address, or it is busy (EEPROMs NACK while they write internally). A NACK on a data byte usually means the slave rejected the register or the value.

A slow slave can hold SCL low after the master releases it, pausing the transaction until it is ready. This is clock stretching. The master’s peripheral must notice that SCL did not rise and wait. The STM32 I2C peripheral handles this in hardware, which is one more reason every wait in your driver needs a timeout: a crashed slave can stretch forever.

Standard speeds are 100 kHz (standard mode), 400 kHz (fast mode), 1 MHz (fast mode plus) and 3.4 MHz (high speed, rare). The STM32F4 I2C peripheral supports up to 400 kHz.

I2C1 sits on APB1 at 0x40005400 (RCC_APB1ENR bit 21). On the Discovery it uses PB6 = SCL and PB9 = SDA, AF4, configured open-drain.

OffsetRegisterFields used here
0x00CR1PE (0), START (8), STOP (9), ACK (10), POS (11), SWRST (15)
0x04CR2FREQ[5:0]: APB1 clock in MHz
0x10DRdata
0x14SR1SB (0), ADDR (1), BTF (2), RXNE (6), TXE (7), BERR (8), ARLO (9), AF (10)
0x18SR2MSL (0), BUSY (1), TRA (2)
0x1CCCRCCR[11:0], DUTY (14), F/S (15)
0x20TRISEmaximum rise time in APB1 cycles, plus 1

Timing at the boot clock of 16 MHz: FREQ = 16. In standard mode SCL high and low times are each CCR APB1 cycles, so CCR = 16 MHz / (2 * 100 kHz) = 80. TRISE = 1000 ns / 62.5 ns + 1 = 17.

The STM32F1/F2/F4 I2C peripheral is famous for being awkward. It is driven by status events, and several flags are cleared by reading specific registers in a specific order. You must follow the sequence from the reference manual exactly.

Master write:

  1. Set START. Wait for SR1.SB (start bit generated). Reading SR1 then writing DR clears SB.
  2. Write address << 1 | 0 to DR. Wait for SR1.ADDR (address sent and ACKed) or SR1.AF (NACK).
  3. Clear ADDR by reading SR1 and then SR2. Until you do, the peripheral stretches SCL.
  4. For each byte: wait for TXE, write DR.
  5. Wait for BTF (last byte fully shifted out and ACKed), then set STOP.

Master read is worse, because the peripheral starts receiving the next byte (and decides whether to ACK it) as soon as the current one lands. To NACK the last byte, you must clear ACK before the second-to-last byte finishes. The reference manual gives three recipes:

  • N = 1: clear ACK before clearing ADDR, then set STOP right after clearing ADDR, then wait RXNE and read.
  • N = 2: clear ACK and set POS before clearing ADDR (POS makes the ACK bit apply to the next byte). Wait BTF, set STOP, read DR twice.
  • N > 2: keep ACK set and read normally until three bytes remain. Wait BTF (byte N-2 in DR, N-1 in the shift register), clear ACK, read N-2, wait BTF again, set STOP, read N-1, wait RXNE, read N.
i2c.h
#ifndef I2C_H
#define I2C_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
void i2c1_init(void);
void i2c1_bus_recover(void);
bool i2c1_probe(uint8_t addr7);
bool i2c1_mem_write(uint8_t addr7, uint8_t reg, const uint8_t *buf, size_t len);
bool i2c1_mem_read(uint8_t addr7, uint8_t reg, uint8_t *buf, size_t len);
#endif
i2c.c
#include "i2c.h"
#include "stm32f407_regs.h"
#include "gpio_util.h"
#define I2C_CR1_PE (1u << 0)
#define I2C_CR1_START (1u << 8)
#define I2C_CR1_STOP (1u << 9)
#define I2C_CR1_ACK (1u << 10)
#define I2C_CR1_POS (1u << 11)
#define I2C_CR1_SWRST (1u << 15)
#define I2C_SR1_SB (1u << 0)
#define I2C_SR1_ADDR (1u << 1)
#define I2C_SR1_BTF (1u << 2)
#define I2C_SR1_RXNE (1u << 6)
#define I2C_SR1_TXE (1u << 7)
#define I2C_SR1_AF (1u << 10)
#define I2C_SR2_BUSY (1u << 1)
#define I2C_TIMEOUT 100000u
#define SCL_PIN 6u
#define SDA_PIN 9u
static void short_delay(uint32_t n)
{
while (n-- != 0u) {
__asm volatile ("nop");
}
}
/* Wait for any bit in mask in SR1. A NACK (AF) aborts the wait. */
static bool i2c_wait(uint32_t mask)
{
for (uint32_t n = I2C_TIMEOUT; n != 0u; n--) {
uint32_t sr1 = I2C1->SR1;
if ((sr1 & I2C_SR1_AF) != 0u) {
I2C1->SR1 = ~I2C_SR1_AF; /* AF is cleared by writing 0 */
return false;
}
if ((sr1 & mask) != 0u) {
return true;
}
}
return false;
}
static bool i2c_wait_idle(void)
{
for (uint32_t n = I2C_TIMEOUT; n != 0u; n--) {
if ((I2C1->SR2 & I2C_SR2_BUSY) == 0u) {
return true;
}
}
return false;
}
static bool i2c_start(uint8_t addr7, bool read)
{
I2C1->CR1 |= I2C_CR1_START;
if (!i2c_wait(I2C_SR1_SB)) {
return false;
}
I2C1->DR = ((uint32_t)addr7 << 1) | (read ? 1u : 0u);
return i2c_wait(I2C_SR1_ADDR); /* ADDR is left set for the caller */
}
static void i2c_clear_addr(void)
{
(void)I2C1->SR1; /* the order matters: SR1, then SR2 */
(void)I2C1->SR2;
}
static void i2c_stop(void)
{
I2C1->CR1 |= I2C_CR1_STOP;
}
static bool i2c_send(uint8_t b)
{
if (!i2c_wait(I2C_SR1_TXE)) {
return false;
}
I2C1->DR = b;
return true;
}
void i2c1_bus_recover(void)
{
/* Take the pins as open-drain GPIOs, released (high). */
GPIOB->OTYPER |= (1u << SCL_PIN) | (1u << SDA_PIN);
gpio_set(GPIOB, SCL_PIN);
gpio_set(GPIOB, SDA_PIN);
gpio_mode(GPIOB, SCL_PIN, GPIO_OUT);
gpio_mode(GPIOB, SDA_PIN, GPIO_OUT);
short_delay(80u);
/* Clock until the slave lets go of SDA (at most 9 bits). */
for (uint32_t i = 0; i < 9u && (GPIOB->IDR & (1u << SDA_PIN)) == 0u; i++) {
gpio_clear(GPIOB, SCL_PIN);
short_delay(80u);
gpio_set(GPIOB, SCL_PIN);
short_delay(80u);
}
/* Generate a STOP: SDA low while SCL low, raise SCL, then raise SDA. */
gpio_clear(GPIOB, SCL_PIN);
short_delay(80u);
gpio_clear(GPIOB, SDA_PIN);
short_delay(80u);
gpio_set(GPIOB, SCL_PIN);
short_delay(80u);
gpio_set(GPIOB, SDA_PIN);
short_delay(80u);
}
void i2c1_init(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
(void)RCC->APB1ENR;
i2c1_bus_recover(); /* free the bus before taking it */
gpio_af(GPIOB, SCL_PIN, 4u); /* still open-drain from recover */
gpio_af(GPIOB, SDA_PIN, 4u);
I2C1->CR1 = I2C_CR1_SWRST; /* reset any stale internal state */
I2C1->CR1 = 0u;
I2C1->CR2 = 16u; /* FREQ: APB1 = 16 MHz */
I2C1->CCR = 80u; /* standard mode, 100 kHz */
I2C1->TRISE = 17u;
I2C1->CR1 = I2C_CR1_PE;
}
bool i2c1_probe(uint8_t addr7)
{
if (!i2c_wait_idle()) {
return false;
}
bool ack = i2c_start(addr7, false);
if (ack) {
i2c_clear_addr();
}
i2c_stop();
return ack;
}
bool i2c1_mem_write(uint8_t addr7, uint8_t reg, const uint8_t *buf, size_t len)
{
bool ok = false;
if (!i2c_wait_idle()) {
return false;
}
if (!i2c_start(addr7, false)) {
goto out;
}
i2c_clear_addr();
if (!i2c_send(reg)) {
goto out;
}
for (size_t i = 0; i < len; i++) {
if (!i2c_send(buf[i])) {
goto out;
}
}
ok = i2c_wait(I2C_SR1_BTF);
out:
i2c_stop();
return ok;
}
bool i2c1_mem_read(uint8_t addr7, uint8_t reg, uint8_t *buf, size_t len)
{
size_t i = 0;
if (len == 0u || !i2c_wait_idle()) {
return false;
}
/* Phase 1: set the register pointer. */
if (!i2c_start(addr7, false)) {
goto fail;
}
i2c_clear_addr();
if (!i2c_send(reg) || !i2c_wait(I2C_SR1_BTF)) {
goto fail;
}
/* Phase 2: repeated START, address with R. */
if (!i2c_start(addr7, true)) {
goto fail;
}
if (len == 1u) {
I2C1->CR1 &= ~I2C_CR1_ACK;
i2c_clear_addr();
i2c_stop();
if (!i2c_wait(I2C_SR1_RXNE)) {
goto fail;
}
buf[0] = (uint8_t)I2C1->DR;
return true;
}
if (len == 2u) {
I2C1->CR1 = (I2C1->CR1 & ~I2C_CR1_ACK) | I2C_CR1_POS;
i2c_clear_addr();
if (!i2c_wait(I2C_SR1_BTF)) {
goto fail;
}
i2c_stop();
buf[0] = (uint8_t)I2C1->DR;
buf[1] = (uint8_t)I2C1->DR;
I2C1->CR1 &= ~I2C_CR1_POS;
return true;
}
I2C1->CR1 |= I2C_CR1_ACK;
i2c_clear_addr();
while (len - i > 3u) {
if (!i2c_wait(I2C_SR1_RXNE)) {
goto fail;
}
buf[i++] = (uint8_t)I2C1->DR;
}
if (!i2c_wait(I2C_SR1_BTF)) { /* N-2 in DR, N-1 in shift reg */
goto fail;
}
I2C1->CR1 &= ~I2C_CR1_ACK;
buf[i++] = (uint8_t)I2C1->DR; /* N-2; N will be NACKed */
if (!i2c_wait(I2C_SR1_BTF)) { /* N-1 in DR, N in shift reg */
goto fail;
}
i2c_stop();
buf[i++] = (uint8_t)I2C1->DR; /* N-1 */
if (!i2c_wait(I2C_SR1_RXNE)) {
goto fail;
}
buf[i] = (uint8_t)I2C1->DR; /* N */
return true;
fail:
i2c_stop();
I2C1->CR1 &= ~I2C_CR1_POS;
return false;
}

The important details:

  • i2c_wait treats AF as an immediate failure. Without this, a NACKed address makes the driver wait for ADDR until the timeout, and the scanner takes seconds instead of milliseconds. AF is a “write 0 to clear” bit, so writing ~I2C_SR1_AF clears it and writes harmless 1s everywhere else.
  • i2c_clear_addr is two reads whose values are discarded. The cast to void tells the compiler and the reader that the access is the point. Because the registers are volatile, the reads cannot be optimized out. Reversing the order (SR2 then SR1) does not clear ADDR.
  • In the one-byte read, the ACK clear, the ADDR clear and the STOP must happen back to back. If an interrupt lands between clearing ADDR and setting STOP, the peripheral may already have started a second byte. Production drivers mask interrupts around those lines (with the PRIMASK save and restore from Interrupts done right) or use DMA.
  • Every error path ends in STOP. Leaving the bus mid-transaction means the slave still owns SDA and the next call fails with BUSY forever.

If the MCU resets in the middle of a read, the slave may be halfway through sending a byte with SDA held low, waiting for clocks that will never come. The master sees BUSY set and can never generate START. Power-cycling fixes it, but a field device cannot always do that.

The fix, which i2c1_bus_recover implements, is to take the pins as GPIOs and toggle SCL up to nine times. Each clock lets the slave shift out one more bit; at worst after eight data bits and one ACK slot it releases SDA. Then generate a STOP manually so every slave resets its state machine. Run it at boot before enabling the peripheral, and again whenever a transaction times out with BUSY set.

The STM32F4 I2C peripheral itself can also wedge with BUSY stuck after glitches on the lines. SWRST in CR1 resets it; you must then reprogram CR2, CCR and TRISE, which is why i2c1_init writes them after the reset.

The Discovery’s CS43L22 audio DAC sits on I2C1 at 7-bit address 0x4A. Register 0x01 is the chip ID: bits 7:3 read 11100 and bits 2:0 are the silicon revision, so expect 0xE0 to 0xE7.

A scanner probes each legal 7-bit address (0x08 to 0x77; the others are reserved) and prints the ones that ACK. It is the first thing to run on any new I2C board: it proves wiring, pull-ups and power, and tells you the real address when a datasheet is ambiguous about whether it lists 7-bit or 8-bit addresses.

A sensor driver should not know whether its chip is on SPI or I2C; many parts, including the LIS3DSH, support both. The driver only needs “read N bytes starting at register R” and “write N bytes starting at register R”. Express that as a small vtable of function pointers:

regdev.h
#ifndef REGDEV_H
#define REGDEV_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef struct {
bool (*read)(void *ctx, uint8_t reg, uint8_t *buf, size_t len);
bool (*write)(void *ctx, uint8_t reg, const uint8_t *buf, size_t len);
} regdev_ops_t;
typedef struct {
const regdev_ops_t *ops;
void *ctx; /* bus-specific: spi_dev_t *, i2c address ... */
} regdev_t;
static inline bool regdev_read(const regdev_t *d, uint8_t reg, uint8_t *buf, size_t len)
{
return d->ops->read(d->ctx, reg, buf, len);
}
static inline bool regdev_write(const regdev_t *d, uint8_t reg, const uint8_t *buf, size_t len)
{
return d->ops->write(d->ctx, reg, buf, len);
}
#endif

Each bus provides one regdev_ops_t. The SPI version sets bit 7 for reads and frames the whole operation with CS; the I2C version uses the register-pointer-then-repeated-START sequence. The full pattern of driver vtables is developed in Driver design and hardware abstraction; this is the minimum that makes the point.

main.c
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "stm32f407_regs.h"
#include "gpio_util.h"
#include "spi.h"
#include "i2c.h"
#include "regdev.h"
void console_init(void);
/* ---- SPI binding ---- */
static bool spi_reg_read(void *ctx, uint8_t reg, uint8_t *buf, size_t len)
{
const spi_dev_t *d = ctx;
uint8_t cmd = (uint8_t)(reg | 0x80u); /* bit 7 = read */
spi_cs_assert(d);
bool ok = spi_transfer(d->spi, &cmd, NULL, 1u)
&& spi_transfer(d->spi, NULL, buf, len);
spi_cs_release(d);
return ok;
}
static bool spi_reg_write(void *ctx, uint8_t reg, const uint8_t *buf, size_t len)
{
const spi_dev_t *d = ctx;
uint8_t cmd = (uint8_t)(reg & 0x7Fu);
spi_cs_assert(d);
bool ok = spi_transfer(d->spi, &cmd, NULL, 1u)
&& spi_transfer(d->spi, buf, NULL, len);
spi_cs_release(d);
return ok;
}
static const regdev_ops_t spi_regdev_ops = { spi_reg_read, spi_reg_write };
/* ---- I2C binding: ctx points at the 7-bit address ---- */
static bool i2c_reg_read(void *ctx, uint8_t reg, uint8_t *buf, size_t len)
{
return i2c1_mem_read(*(const uint8_t *)ctx, reg, buf, len);
}
static bool i2c_reg_write(void *ctx, uint8_t reg, const uint8_t *buf, size_t len)
{
return i2c1_mem_write(*(const uint8_t *)ctx, reg, buf, len);
}
static const regdev_ops_t i2c_regdev_ops = { i2c_reg_read, i2c_reg_write };
/* ---- Devices ---- */
static spi_dev_t lis3dsh_bus = { SPI1, GPIOE, 3u };
static uint8_t cs43l22_addr = 0x4Au;
static const regdev_t accel = { &spi_regdev_ops, &lis3dsh_bus };
static const regdev_t codec = { &i2c_regdev_ops, &cs43l22_addr };
static void delay(volatile uint32_t n)
{
while (n-- != 0u) { }
}
int main(void)
{
uint8_t v = 0;
console_init();
spi1_init();
i2c1_init();
/* Release the CS43L22 from reset: PD4 high. */
RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
(void)RCC->AHB1ENR;
gpio_set(GPIOD, 4u);
gpio_mode(GPIOD, 4u, GPIO_OUT);
delay(100000u);
if (regdev_read(&accel, 0x0Fu, &v, 1u)) {
printf("LIS3DSH WHO_AM_I = 0x%02X\n", v);
} else {
printf("LIS3DSH: SPI timeout\n");
}
printf("I2C scan:");
for (uint8_t a = 0x08u; a <= 0x77u; a++) {
if (i2c1_probe(a)) {
printf(" 0x%02X", a);
}
}
printf("\n");
if (regdev_read(&codec, 0x01u, &v, 1u)) {
printf("CS43L22 ID = 0x%02X (chip 0x%02X, rev %u)\n",
v, v >> 3, v & 7u);
} else {
printf("CS43L22: no ACK\n");
}
const uint8_t ctrl4 = 0x67u; /* 100 Hz, X/Y/Z enabled */
const uint8_t ctrl6 = 0x10u; /* ADD_INC: auto-increment address */
(void)regdev_write(&accel, 0x20u, &ctrl4, 1u);
(void)regdev_write(&accel, 0x25u, &ctrl6, 1u);
for (;;) {
uint8_t raw[6];
if (regdev_read(&accel, 0x28u, raw, sizeof raw)) {
int32_t mg[3];
for (int k = 0; k < 3; k++) {
int16_t s = (int16_t)(uint16_t)(raw[2 * k] | (raw[2 * k + 1] << 8));
mg[k] = (int32_t)s * 6 / 100; /* about 0.06 mg/LSB at +-2 g */
}
printf("X=%5ld mg Y=%5ld mg Z=%5ld mg\n",
(long)mg[0], (long)mg[1], (long)mg[2]);
}
delay(2000000u);
}
}

Build it with the Makefile from Blinky from nothing, adding console.c spi.c i2c.c to the source list. With the board lying flat:

LIS3DSH WHO_AM_I = 0x3F
I2C scan: 0x4A
CS43L22 ID = 0xE3 (chip 0x1C, rev 3)
X= 14 mg Y= -22 mg Z= 1004 mg
X= 12 mg Y= -19 mg Z= 1001 mg

Notice that main reads both devices with identical calls. Swap accel’s ops and context for an I2C-wired LIS3DSH and nothing else changes. The raw sample assembly shifts bytes together rather than casting the buffer to int16_t *, for the endianness and alignment reasons from Bit manipulation. The 0.06 mg per LSB sensitivity is the datasheet’s typical figure at the default ±2 g range; confirm it against your datasheet revision.

On a logic analyzer with an SPI decoder, the WHO_AM_I read is one CS-low window about 4.5 µs long containing 16 SCK pulses. MOSI shows 0x8F then 0xFF; MISO shows a meaningless first byte then 0x3F. SCK idles high (mode 3). CS falls before the first SCK edge and rises after the last edge, with no gap in the middle. If you see CS toggling between the two bytes, your driver is releasing CS per byte and the slave is restarting each time.

On I2C at 100 kHz, the chip-ID read takes roughly 400 µs. The decoder shows START, 0x94 (write to 0x4A) with ACK, 0x01 with ACK, a repeated START (SDA falls while SCL is high, with no STOP before it), 0x95 with ACK, 0xE3 followed by NACK, then STOP. Look at the rising edges with the analog view of a scope: with correct pull-ups they are clean curves reaching 3.3 V well before the next falling edge. Shark-fin edges that barely reach the threshold mean the pull-ups are too weak or the bus is too long.

SPII2CUART
Wires3 + one CS per slave2 shared2 (point to point)
Clockmaster suppliesmaster supplies, slaves can stretchnone, both sides agree on baud
Duplexfullhalffull
Typical speed1 to 50 MHz100 to 400 kHz9600 baud to a few Mbaud
AddressingCS line7-bit address in protocolnone
Acknowledgmentnoneevery bytenone
Distanceon boardon board (bus capacitance limit)metres (with RS-232/RS-485)
Use forflash, displays, fast ADCs, radiosconfig of sensors, codecs, PMICs, EEPROMsconsoles, GPS, modems, inter-board links
Failure modesilent garbageNACK, stuck busframing errors, overrun
  • Forgetting SSI with SSM. The SPI sees NSS low, raises MODF and drops out of master mode. Set both SSM and SSI when CS is a GPIO.
  • Not reading DR after each SPI byte. OVR sets and later reads return stale data. Always read, even when discarding.
  • Releasing CS before the last bit is out. Wait for BSY = 0 before raising CS.
  • Wrong SPI mode. Values come back bit-shifted (0x7E instead of 0x3F). Read the slave’s timing diagram, not just the mode number.
  • Reading SR2 before SR1 (or the debugger reading them for you in a peripheral view). ADDR does not clear, or clears early. Do the reads in order, and close register views while single-stepping I2C code.
  • No pull-ups, or internal pull-ups only. The bus works on the bench and fails with a longer cable. Fit 2.2 to 4.7 kΩ externals.
  • Using an 8-bit address as a 7-bit one. Datasheets that say “address 0x94” mean the shifted write address; the 7-bit address is 0x4A. The scanner tells you the truth.
  • Waits without timeouts. One disconnected sensor hangs the product. Every loop that polls a flag must have an exit.
  1. Run the example. Then deliberately change the SPI mode to mode 1 (CPOL = 0, CPHA = 1) and record what WHO_AM_I returns. Explain the value from the timing diagram.
  2. Remove the line that drives PD4 high and run the scanner. Confirm the scan is empty and that i2c1_probe returns quickly rather than waiting for the timeout. Then time a full scan with the DWT cycle counter.
  3. Rewrite spi_transfer so it keeps the transmit buffer full (write byte i + 1 as soon as TXE sets, before reading byte i). Raise the SPI clock to 8 MHz and measure, with a logic analyzer or the cycle counter, how much the gap between bytes shrinks. Make sure you still never miss a received byte.
  4. Disassemble i2c_clear_addr with arm-none-eabi-objdump -d and confirm both loads are present at -O2. Then remove volatile from the I2C_TypeDef fields temporarily, rebuild, and show the loads disappear.
  5. Write a LIS3DSH driver (lis3dsh_init, lis3dsh_read_mg) that takes only a const regdev_t *. Then write a fake regdev_ops_t on the host that returns canned register values, and unit-test the driver’s conversion without hardware.

Question 1:Your SPI read routine writes the command byte, then calls a receive function that only waits for RXNE and reads DR, without writing anything. What happens?

Question 2:An STM32 SPI master configured with SSM = 1 but SSI = 0 sends nothing, and CR1 reads back with MSTR and SPE cleared. Why?

Question 3:A device's datasheet says its I2C address is 0x94 for writes and 0x95 for reads. What do you pass as the 7-bit address to i2c1_mem_read?

Question 4:After a watchdog reset during an I2C read, SR2.BUSY stays set and every START times out. What is the right recovery?

Question 5:In the F4 I2C master receive of exactly one byte, why must ACK be cleared before clearing ADDR?

  • SPI is a pair of shift registers: every byte you send clocks one byte in, so reads need dummy writes and writes need discarded reads.
  • Match CPOL/CPHA to the slave’s timing diagram, keep CS low for the whole transaction, and release it only after BSY clears.
  • I2C is open-drain with pull-ups; START/STOP are SDA edges while SCL is high, and every byte gets an ACK or NACK from the receiver.
  • The STM32F4 I2C peripheral must be driven through the exact event sequence, including clearing ADDR by reading SR1 then SR2 and setting up the NACK before the last byte.
  • Every polled wait needs a timeout and every I2C error path needs a STOP; a stuck bus is recovered with nine manual SCL clocks and a STOP.
  • Put sensor drivers behind a register-device interface so the bus is a detail chosen at board level.

Next: DMA, where you stop moving bytes with the CPU and let the DMA controller feed the UART and other peripherals for you.