
When an I2C bus hangs intermittently, the system appears to freeze — sensor reads return garbage, the MCU stalls in the I2C ISR, and a logic analyzer shows SCL stuck low or SDA held down. Unlike a clean NACK error, a bus hang leaves no error code, only a silent stall that survives software resets. This guide covers diagnosing and fixing the three root causes: slave clock-stretching timeouts, stuck-slave SDA holds, and multi-master arbitration collisions.
+------------------+ +------------------+ +------------------+| I2C MASTER | | I2C SLAVE | | BUS STATE || [STM32/MPU] | <--> | [Sensor/EEPROM] | | SCL / SDA || | | | | || Sends START | ---> | ACKs | | NORMAL || Sends ADDR+R/W | ---> | ACKs | | || Sends DATA | ---> | CLOCK STRETCH | | SCL HELD LOW || WAITS... | <--- | (too long) | | TIMEOUT! || TIMEOUT/RESET | | HOLDS SDA LOW | | SDA HELD LOW |+------------------+ +------------------+ +------------------+
Consider an STM32H7-based industrial gateway polling a temperature sensor (TMP117) and an EEPROM (24LC256) on the same I2C bus at 400 kHz. Every few hours, the I2C driver returns HAL_TIMEOUT, the task blocks indefinitely, and a power cycle is the only recovery. The sensor datasheet claims 400 kHz support, and the EEPROM is rated for 1 MHz. Logic analyzer captures show clean transactions 99.9% of the time — then suddenly SCL stays low for 200 ms, the master times out, and the bus never recovers without a hardware reset.
I2C bus hangs fall into three distinct categories, each with different diagnostics and fixes. Understanding which class you’re facing is critical — the recovery strategy differs completely.
I2C slaves may stretch SCL low after receiving a byte to buy processing time. The I2C spec allows this indefinitely, but practical masters implement timeouts to prevent indefinite stalls. On STM32, the TIMEOUTR register configures TIMEOUTA (SCL low timeout) and TIMEOUTB (SCL high timeout). If disabled (default reset value), the master waits forever — the most common cause of “random” hangs that only a power cycle resolves.
Common offenders:
The timeout must exceed the slowest slave’s maximum stretch. For a mixed bus with EEPROM (5 ms) + SHT31 (15 ms), configure at least 25 ms margin.
A slave may hold SDA low after a transaction due to:
When SDA is stuck low, the master’s hardware peripheral cannot generate START or STOP conditions, rendering the bus dead to standard API calls. However, because the slave is usually stuck waiting for clock pulses to finish shifting out a pending data byte or ACK/NACK bit, software can recover the bus by bit-banging clock pulses on SCL until SDA is released.
In multi-master systems (e.g., STM32 MCU + Linux kernel i2c-dev both accessing the same physical bus), arbitration loss should cause the loser to back off cleanly. But if the loser’s driver doesn’t properly release the bus or re-initialize its state machine, it may leave SDA/SCL in a stuck state. Common scenarios:
i2c-dev userspace holds bus open across ioctl calls; crash leaves bus lockedAlways configure TIMEOUTA for SCL low timeout based on your slowest slave’s maximum stretch. The formula for TIMEOUTA at 16 MHz I2CCLK (with TIDLE=0):
TIMEOUTA = (max_stretch_s * I2CCLK / 2048) - 1
For a bus with SHT31 (15 ms max) + EEPROM (5 ms) = 20 ms worst case → 25 ms margin:
TIMEOUTA = (0.025 * 16000000 / 2048) - 1 = 194.3 ≈ 195 = 0x00C3
STM32H7 I2C1 TIMINGR for 400 kHz with 25 ms SCL low timeout:
// STM32H7 I2C1 TIMINGR for 400 kHz (I2CCLK = 16 MHz, Fast Mode)// PRESC=0, SCLDEL=1 (125 ns setup), SDADEL=0 (0 ns hold), SCLH=7, SCLL=22I2C1->TIMINGR = (0 << 28) | (0x01 << 20) | (0x00 << 16) | (0x07 << 8) | (0x16 << 0);// TIMEOUTR: TIMOUTEN=1 (bit15), TIDLE=0 (bit12), TIMEOUTA=0x00C3 (25 ms)I2C1->TIMEOUTR = (1 << 15) | (0 << 12) | (0x00C3 << 0);// Enable peripheralI2C1->CR1 |= I2C_CR1_PE;
Key fields:
TIMOUTEN=1 (bit 15): Timeout enableTIDLE=0 (bit 12): Timeout detected on SCL low (not idle)TIMEOUTA (bits 11:0): 12-bit timeout valueVerification: After programming, trigger a 70 ms slave stretch — the TIMEOUT interrupt should fire, ISR executes recovery, and bus returns to functional state within 2 ms.
When timeout fires or stuck SDA detected, execute GPIO-based recovery:
void i2c_bus_recover(I2C_TypeDef *i2c){// 1. Disable I2C peripheral immediately — stops hardware state machinei2c->CR1 &= ~I2C_CR1_PE;// 2. Reconfigure SCL/SDA as GPIO open-drain output with pull-up// Open-drain prevents electrical contention if slave drives SDA lowGPIO_InitTypeDef gpio = {0};gpio.Mode = GPIO_MODE_OUTPUT_OD;gpio.Pull = GPIO_PULLUP;gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;gpio.Pin = I2C_SCL_PIN | I2C_SDA_PIN;HAL_GPIO_Init(I2C_GPIO_PORT, &gpio);// Release SDA (set output high in open-drain floats line)HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET);// 3. Clock up to 9-16 pulses on SCL until slave releases SDA// Standard I2C bus clear: 9 clock pulses clock out any stuck data byte + NACKfor (int i = 0; i < 16; i++) {HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET);delay_us(5); // ~100 kHz recovery clockHAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET);delay_us(5);// If slave released SDA, break earlyif (HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN) == GPIO_PIN_SET) {break;}}// 4. Generate valid STOP condition: SDA rising edge while SCL is HIGH// Bring SCL LOW first so we can transition SDA LOW without generating a false STARTHAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET);delay_us(5);HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_RESET);delay_us(5);HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET);delay_us(5);HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET);delay_us(5);// 5. Re-initialize I2C peripheral with full configurationMX_I2C1_Init();}
Poll SDA state in a watchdog task or timer callback:
bool i2c_is_sda_stuck(I2C_TypeDef *i2c){// Read SDA GPIO input state directly when peripheral status indicates busy// On STM32H7 (I2C v2), the status register is ISR and busy flag is I2C_ISR_BUSYreturn (HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN) == GPIO_PIN_RESET) &&(i2c->ISR & I2C_ISR_BUSY);}
If SDA stuck low while bus marked busy, trigger recovery immediately — don’t wait for timeout.
In multi-master systems sharing an I2C bus:
HSEM) or communicate via RPMsg before calling HAL_I2C_Master_Transmit().ARLO in I2C_ISR, releases SCL/SDA, and enters slave/listening mode.// In MCU firmware: handle arbitration lost (ARLO) flagif (i2c->ISR & I2C_ISR_ARLO) {// Clear arbitration loss flagi2c->ICR = I2C_ICR_ARLOCF;// Back off with exponential jitter before retrying transferosDelay(backoff_ms);}
On the Linux side, ensure userspace applications close /dev/i2c-X file descriptors with O_CLOEXEC and rely on kernel I2C adapter retries (i2c_adapter.retries) to gracefully handle hardware arbitration loss.
// On test slave (Arduino/second MCU): stretch SCL for 30 ms on every readvoid requestEvent() {delay(30); // Exceeds 25 ms timeoutWire.write(sensor_data, 2);}
Verify master recovers via timeout ISR and logs recovery event.
// On test slave: hold SDA low after STOPvoid onStop() {pinMode(SDA_PIN, OUTPUT);digitalWrite(SDA_PIN, LOW); // Malicious hold}
Verify watchdog detects stuck SDA within 100 ms and triggers recovery.
Run Linux i2cset/i2cget loop concurrently with MCU polling for 24 hours. Verify zero bus hangs and clean arbitration (logic analyzer shows clean START/STOP from both masters).
| Approach | Pros | Cons |
|---|---|---|
| Hardware timeout (TIMEOUTR) | Zero CPU overhead, deterministic | Requires precise timing calc per bus speed |
| Software watchdog + GPIO poll | Works on any MCU, flexible thresholds | Consumes CPU cycles, jitter-sensitive |
| Bus recovery sequence | Recovers from any stuck state | Momentary bus disruption (~100 µs) |
| Hardware semaphore / ARLO (multi-master) | Eliminates arbitration corruption | Requires coordination or backoff handling |
For STM32 + FreeRTOS: combine hardware timeout (fast path) + FreeRTOS timer watchdog (backup) + ARLO retry backoff for multi-master. For bare-metal: hardware timeout + periodic GPIO poll in SysTick.
Intermittent I2C bus hangs are not random — they are deterministic failures of timeout configuration, slave state management, or multi-master discipline. The fix is threefold: (1) enable and tune TIMEOUTR timeouts for your slowest slave, (2) implement a 9-pulse GPIO recovery sequence triggered by timeout or stuck-SDA detection, (3) handle hardware arbitration loss (ARLO) and cross-core hardware semaphores when Linux and MCU share the bus. With these, the gateway above ran 30 days without a single bus hang.
Quick Links
Legal Stuff





