The I2C bus is the most common peripheral interconnect in embedded systems โ temperature sensors, IMUs, EEPROMs, DACs, IO expanders, power management ICs โ but it is also the one that silently fails most often in production. A missing pull-up, a stuck SCL line, a NACK from a powered-down slave, and your entire sensor read returns garbage without a single HardFault.
This article covers STM32's I2C peripheral in master mode at register level, focusing on the modern I2C v2 core used on STM32G4, STM32H7, STM32U5, STM32L4+, and most post-2019 STM32 series. We will look at timing calculation, the transmit and receive sequence, and โ most importantly โ how to detect and recover from every error the bus can throw at you.
I2C v2 Register Overview
The I2C v2 peripheral replaces the legacy shared-flag approach with a single 32-bit status register (I2C_ISR) and a dedicated clear register (I2C_ICR). Writing a flag in ICR clears the corresponding bit in ISR, avoiding the read-modify-write hazards of the old v1 peripheral.
| Register | Purpose |
|---|---|
I2C_CR1 | Control: enable, NACK/STOP generation, reset, DMA |
I2C_CR2 | Transfer control: slave address, direction, NBYTES, START/STOP, autoend |
I2C_TIMINGR | SCL timing: prescaler, SCLDEL, SDADEL, SCLH, SCLL |
I2C_ISR | Status flags: TXIS, RXNE, TC, NACKF, BUSY, ARLO, BERR, OVR, TIMEOUT |
I2C_ICR | Clear flags: write 1 to the bit you want to clear |
I2C_TXDR | Transmit data (8-bit, 16-bit for 10-bit addr) |
I2C_RXDR | Receive data (8-bit) |
The key mental model shift from the old I2C v1: you do not poll SB, ADDR, BTF. Instead, I2C_CR2 starts a transfer, and I2C_ISR tells you what just happened.
Timing Register Calculation
Unlike the old v1 peripheral where you configured I2C_CCR based on APB clock and I2C speed, the v2 core uses a purely digital timing register. The formula is:
t_I2CCLK = (prescaler + 1) ร t_PCLK1
SCLL = t_SCL_low / t_I2CCLK - 1
SCLH = t_SCL_high / t_I2CCLK - 1
SDADEL = t_SDA_delay / t_I2CCLK - 1
SCLDEL = t_SCL_delay / t_I2CCLK - 1
Where the delays are defined by the I2C specification. For 100 kHz (Standard mode): SCL low ≥ 4.7 μs, SCL high ≥ 4.0 μs, SDA delay ≥ 0.1 μs, SCL delay ≥ 0.3 μs. For 400 kHz (Fast mode): SCL low ≥ 1.3 μs, SCL high ≥ 0.6 μs, SDA delay ≥ 0.1 μs, SCL delay ≥ 0.3 μs. For 1 MHz (Fast Mode+): SCL low ≥ 0.5 μs, SCL high ≥ 0.26 μs.
Here is a practical calculation for an STM32G474 at 170 MHz PCLK1 (170 ns period) for 400 kHz Fast Mode:
PCLK1 = 170 MHz โ t_PCLK1 = 5.88 ns
Choose prescaler = 0 โ t_I2CCLK = 5.88 ns
SCLH = ceil(0.6 ยตs / 5.88 ns) - 1 = 101 โ 0x65
SCLL = ceil(1.3 ยตs / 5.88 ns) - 1 = 220 โ 0xDC
SDADEL = 100 ns / 5.88 ns - 1 โ 16 โ 0x10
SCLDEL = 300 ns / 5.88 ns - 1 โ 50 โ 0x32
I2C_TIMINGR = (0 << 28) /* PRESC */
| (0x32 << 16) /* SCLDEL */
| (0x10 << 12) /* SDADEL */
| (0xDC << 8) /* SCLL */
| (0x65 << 0); /* SCLH */
STM32CubeMX generates I2C_TIMINGR values for you, and I recommend starting from those. But verifying by hand โ especially when you change PCLK1 frequency for power management โ prevents the most common source of "I2C stopped working" after clock reconfiguration.
Master Transmit Sequence
Writing one byte to a slave register address requires this sequence:
void I2C_Master_Transmit(I2C_TypeDef *i2c, uint8_t dev_addr,
uint8_t *data, uint16_t len, uint32_t timeout_ms)
{
uint32_t tickstart = HAL_GetTick();
/* Wait for bus idle */
while (i2c->ISR & I2C_ISR_BUSY) {
if (HAL_GetTick() - tickstart > timeout_ms) { return; /* timeout */ }
}
/* Clear any stale flags */
i2c->ICR |= I2C_ICR_NACKCF | I2C_ICR_ARLOCF | I2C_ICR_BERRCF
| I2C_ICR_OVRCF | I2C_ICR_TIMOUTCF;
/* Set slave address + direction (write = 0) + NBYTES + START */
i2c->CR2 = (dev_addr << 1) /* SADD[7:1] */
| (len << 16) /* NBYTES */
| I2C_CR2_START /* Generate START */
| I2C_CR2_AUTOEND; /* Auto STOP after NBYTES */
for (uint16_t i = 0; i < len; i++) {
/* Wait for TXIS (transmit interrupt status) */
while (!(i2c->ISR & I2C_ISR_TXIS)) {
/* Check NACK */
if (i2c->ISR & I2C_ISR_NACKF) {
i2c->ICR = I2C_ICR_NACKCF;
return; /* NACK: slave did not ACK address or data */
}
/* Check other errors */
if (i2c->ISR & (I2C_ISR_ARLO | I2C_ISR_BERR | I2C_ISR_TIMEOUT)) {
I2C_ClearError(i2c);
return;
}
if (HAL_GetTick() - tickstart > timeout_ms) { return; }
}
i2c->TXDR = data[i];
}
/* Wait for transfer complete (TC or TC flag) */
while (!(i2c->ISR & (I2C_ISR_TC | I2C_ISR_STOPF))) {
if (HAL_GetTick() - tickstart > timeout_ms) { return; }
}
i2c->ICR = I2C_ICR_STOPCF; /* Clear STOP flag */
}
Key points:
I2C_CR2_AUTOENDgenerates the STOP condition automatically after NBYTES. Without autoend, you must setI2C_CR2_STOPmanually when TC flag is set โ useful for repeated starts.TXISfires when the TXDR register is empty and ready for new data. The first TXIS appears after the address byte is ACKed.- Always check
NACKFduring theTXISwait loop. If the slave NACKs the address phase, TXIS never fires and you hang forever without the timeout guard.
Master Receive Sequence
Reading N bytes from a slave register is a combined transfer: first a write of the register address (with RESTART), then a read of the data.
void I2C_Master_Read_Register(I2C_TypeDef *i2c, uint8_t dev_addr,
uint8_t reg_addr, uint8_t *rx_buf, uint16_t len,
uint32_t timeout_ms)
{
uint32_t tickstart = HAL_GetTick();
while (i2c->ISR & I2C_ISR_BUSY) {
if (HAL_GetTick() - tickstart > timeout_ms) return;
}
i2c->ICR |= I2C_ICR_NACKCF | I2C_ICR_ARLOCF | I2C_ICR_BERRCF
| I2C_ICR_OVRCF | I2C_ICR_TIMOUTCF;
/* Phase 1: write register address (1 byte) without STOP */
i2c->CR2 = (dev_addr << 1) | (1 << 16) | I2C_CR2_START | I2C_CR2_NOSTRETCH;
/* NOSTRETCH: don't stretch SCL after TC, allows quick RESTART */
while (!(i2c->ISR & I2C_ISR_TXIS)) {
if (i2c->ISR & I2C_ISR_NACKF) {
i2c->ICR = I2C_ICR_NACKCF; return;
}
if (HAL_GetTick() - tickstart > timeout_ms) return;
}
i2c->TXDR = reg_addr;
/* Wait for TC (transfer complete, no STOP generated) */
while (!(i2c->ISR & I2C_ISR_TC)) {
if (HAL_GetTick() - tickstart > timeout_ms) return;
}
/* Phase 2: read len bytes with RESTART + NACK on last byte */
i2c->CR2 = (dev_addr << 1) | I2C_CR2_RD_WRN /* Set read direction */
| (len << 16) | I2C_CR2_START | I2C_CR2_AUTOEND;
for (uint16_t i = 0; i < len; i++) {
while (!(i2c->ISR & I2C_ISR_RXNE)) {
if (i2c->ISR & I2C_ISR_NACKF) { i2c->ICR = I2C_ICR_NACKCF; return; }
if (HAL_GetTick() - tickstart > timeout_ms) return;
}
rx_buf[i] = i2c->RXDR; /* Read clears RXNE automatically */
}
}
Note on combined transfers: The first CR2 write uses I2C_CR2_NOSTRETCH (bit 17) to prevent clock stretching after TC, which would otherwise delay the RESTART. On some slaves this isn't needed โ omit it if the slave requires clock stretching between phases.
Error Handling: Every Failure Mode
I2C error handling is where production firmware separates from hobby code. Here is each error bit in I2C_ISR and its correct handling:
NACKF โ Not Acknowledge
Set when the slave does not ACK the address byte or a data byte. The peripheral automatically stops driving SCL/SDA and waits for software intervention. Clear with I2C_ICR_NACKCF, then you can either retry or reset the peripheral. Most common cause: device not powered, wrong address, or the slave is busy processing.
ARLO โ Arbitration Lost
Set when another master on the multi-master bus starts driving the bus simultaneously and wins arbitration. The peripheral releases SDA and SCL. Clear with I2C_ICR_ARLOCF. In single-master designs (the vast majority of STM32 I2C scenarios), ARLO indicates a bus glitch โ treat it as a bus error and recover.
BERR โ Bus Error
Set when a START or STOP condition is detected at the wrong time in the frame. This happens when a noise spike or glitch on SDA/SCL is misinterpreted as a bus condition. Clear with I2C_ICR_BERRCF.
OVR โ Overrun/Underrun
Set when a new byte arrives in RXDR before the previous one was read (overrun), or when TXDR was empty when the bus needed the next byte (underrun). In master mode with proper NBYTES counting, OVR should never fire. If it does, your ISR service latency is too high โ increase the I2C priority or use DMA.
TIMEOUT โ Clock Stretch Timeout
The I2C v2 peripheral has a dedicated timeout counter (I2C_TIMEOUTR) that detects when SCL is held low by a slave for too long. This is a lifesaver: without it, a stuck slave can hang the bus indefinitely. Enable it once during init:
/* Enable timeout, TIMEOUTA = 0x0FFF bus cycles (~3 ms at 400 kHz) */
i2c->TIMEOUTR = (0x0FFF << 0) /* TIMEOUTA[11:0] */
| I2C_TIMEOUTR_TEXTEN; /* Enable timer A */
When the timer fires, I2C_ISR_TIMEOUT is set. Clear with I2C_ICR_TIMOUTCF. You must then perform bus recovery (see below) because the bus may be in an indeterminate state.
Bus Recovery Procedure
After any non-trivial error (ARLO, BERR, TIMEOUT), the I2C bus may be in a state where SDA is stuck low by a slave. The standard recovery is to toggle SCL 9 times while SDA floats high, then generate a STOP condition:
void I2C_Bus_Recovery(GPIO_TypeDef *scl_port, uint16_t scl_pin,
GPIO_TypeDef *sda_port, uint16_t sda_pin)
{
/* Configure SCL and SDA as open-drain outputs, high */
GPIO_InitTypeDef gpio = {
.Pin = scl_pin | sda_pin,
.Mode = GPIO_MODE_OUTPUT_OD,
.Pull = GPIO_NOPULL,
.Speed = GPIO_SPEED_FREQ_HIGH,
};
HAL_GPIO_Init(scl_port, &gpio);
HAL_GPIO_Init(sda_port, &gpio);
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_SET);
for (int i = 0; i < 9; i++) {
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_RESET);
delay_us(5); /* > 4.7 ยตs low */
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
delay_us(5); /* > 4.0 ยตs high */
}
/* Generate STOP: SDA low while SCL high, then SDA high */
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_RESET);
delay_us(5);
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
delay_us(5);
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_SET);
delay_us(5);
/* Re-configure back to alternate function */
/* ... restore your AF configuration ... */
}
After recovery, perform a software reset of the I2C peripheral by setting and clearing I2C_CR1_PE:
void I2C_Reset(I2C_TypeDef *i2c)
{
i2c->CR1 &= ~I2C_CR1_PE; /* Disable peripheral */
__HAL_I2C_SOFT_RESET(i2c); /* Reset state machine */
i2c->CR1 |= I2C_CR1_PE; /* Re-enable */
}
Practical Example: Reading a Temperature Sensor
Here is a complete example reading the temperature register (0x00, 2 bytes) from an STTS751 sensor at address 0x4C on an STM32G474:
#define STTS751_ADDR 0x4C
#define STTS751_TEMP 0x00
int16_t STTS751_Read_Temp(void)
{
uint8_t data[2];
I2C_Master_Read_Register(I2C1, STTS751_ADDR, STTS751_TEMP, data, 2, 100);
/* Data is big-endian: MSB first, LSB = 0.0625 ยฐC */
int16_t raw = (data[0] << 8) | data[1];
return (raw >> 4); /* 12-bit signed, LSB = 0.0625 ยฐC */
}
void main(void)
{
HAL_Init();
SystemClock_Config(); /* HSE โ PLL โ 170 MHz */
/* I2C1: PB6=SCL, PB7=SDA, AF4 on G4 */
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_I2C1_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7;
gpio.Mode = GPIO_MODE_AF_OD;
gpio.Pull = GPIO_PULLUP;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
gpio.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(GPIOB, &gpio);
/* I2C1 init at register level */
I2C1->CR1 = 0;
I2C1->TIMINGR = (0 << 28) | (0x32 << 16) | (0x10 << 12)
| (0xDC << 8) | (0x65 << 0);
/* Enable clock stretch timeout */
I2C1->TIMEOUTR = (0x0FFF << 0) | I2C_TIMEOUTR_TEXTEN;
I2C1->CR1 |= I2C_CR1_PE; /* Enable peripheral */
int16_t temp = STTS751_Read_Temp();
float temp_c = temp * 0.0625f;
/* temp_c now holds the die temperature in Celsius */
while (1) {
HAL_Delay(1000);
temp = STTS751_Read_Temp();
temp_c = temp * 0.0625f;
/* Log or display */
}
}
For production, add error classification: if NACK occurs three times in a row, report "missing device" instead of retrying forever. If TIMEOUT occurs, run bus recovery and reset the peripheral before retrying.
Error classification strategy
| Error | Likely cause | Action |
|---|---|---|
| NACK | Device missing, wrong address, powered down | Retry 2ร; if persistent, report "device offline" |
| ARLO | Noise glitch / multi-master collision | Clear flag, retry immediately; if repeated, run bus recovery |
| BERR | Noise glitch on SDA/SCL | Clear flag, retry; if persistent, run bus recovery |
| TIMEOUT | Slave stuck holding SCL low | Must run bus recovery + peripheral reset |
| OVR | ISR latency too high for I2C speed | Increase I2C IRQ priority or switch to DMA |
Practical Checklist
- Always verify I2C_TIMINGR after changing PCLK1 โ the most common "I2C broke after clock change" root cause is a stale timing register.
- Enable TIMEOUTR for production builds โ without it, a stuck slave hangs the bus forever. This is non-negotiable for industrial firmware.
- Check NACKF in every TXIS/RXNE wait loop โ otherwise a missing device hangs the task permanently. Pair with a timeout counter.
- Use AUTOEND for simple transfers and manual STOP + TC for combined transfers (write register address, then read).
- Reset the peripheral after bus recovery โ clearing flags is not enough if the state machine is in an inconsistent state.
- Verify with a scope first โ connect an oscilloscope to SDA/SCL and confirm the START, address, ACK/NACK, data, and STOP timing before trusting the firmware.
- Pull-ups matter: 4.7 kฮฉ for 100 kHz, 2.2 kฮฉ for 400 kHz, 1 kฮฉ for 1 MHz. Use the I2C TIMINGR to compensate for rise time, not to fix missing pull-ups.
How I Would Approach This on a Client Project
On a recent STM32G474-based battery management system, I had twelve I2C temperature sensors, a fuel-gauge IC, and an EEPROM on three separate I2C buses. Each bus ran at 400 kHz with fully register-level drivers โ no HAL_I2C in sight, because the HAL's timeout model in polling mode is synchronous per transfer and blocks the control loop.
I structured each I2C transaction as a finite state machine with three outcomes: success, NACK-retryable, and fatal (needs bus recovery). A supervisor task ran bus recovery every 10 seconds on any bus that had accumulated more than 3 errors per minute, logging the event to the EEPROM. Over six months of field data, bus errors occurred roughly once per 100,000 transfers โ always from connector micro-disconnections on the external sensor harness. Without the TIMEOUT detection and automatic recovery, each such glitch would have required a power cycle.
The single biggest lesson: never assume I2C is reliable. Build error detection and recovery into the driver from day one, not as an afterthought when the field reports start coming in.
Sources
- STM32G4 Reference Manual (RM0440) โ I2C chapter (v2 core, registers and programming sequences)
- STM32H7 Reference Manual (RM0399) โ I2C timing register calculation examples
- ST Application Note AN4234 โ STM32 I2C interfacing and optimization tips
- ST Application Note AN4221 โ I2C protocol tutorial for STM32
- I2C-bus specification and user manual (UM10204, NXP) โ timing characteristics for Standard/Fast/Fast-mode Plus
- ST STTS751 datasheet โ temperature sensor I2C command set
๐ฌ Comments / discussion
Prefer email: comments@carrese.eu โ include the article URL so I can follow up. For corrections or deeper questions, I typically reply within 48 hours.