The Symptom
You are reading from an environmental sensor — SHT31, BME280, or similar — over I²C on
STM32F4 or STM32H7. Everything works fine until the board experiences a power glitch, a
noisy environment event, or the sensor is unexpectedly reset mid-transaction. After that,
every call to HAL_I2C_Master_Transmit() or
HAL_I2C_Master_Receive() returns HAL_BUSY, and nothing you do
through the HAL API clears it.
Calling HAL_I2C_DeInit() followed by HAL_I2C_Init() does not
help. Neither does toggling the peripheral clock reset. The BUSY flag in the I2C_SR2
register (STM32F4) or ISR register (STM32H7) remains set.
Root Cause
ST's errata document for the STM32F4 (section 2.14.4, "Spurious Bus Error detection in Master mode") describes this scenario precisely. When the I²C bus encounters an unexpected START or STOP condition — or when SDA is held low by a slave that was interrupted mid-byte — the peripheral's internal state machine reaches a state it cannot leave through a software reset alone.
The BUSY flag is driven partly by the hardware detection of bus activity at the GPIO level, not just internal register state. A peripheral reset clears the internal state, but if SDA or SCL is being held by the slave device, the hardware sees the bus as still active and immediately re-asserts BUSY after the reset completes.
The Recovery Sequence
The correct approach is to temporarily reconfigure the SCL and SDA pins as GPIO outputs, manually clock the bus, generate a STOP condition, and then re-initialise the I²C peripheral. Here is the complete implementation for STM32F4 (I²C1 on PB6/PB7). Adapt pin and port references to match your hardware.
#include "stm32f4xx_hal.h"
/**
* I2C_RecoverBus — unstick a slave holding SDA low.
*
* Call this when HAL_I2C_Master_Transmit / Receive returns HAL_BUSY
* and a DeInit+Init cycle has not helped.
*
* @param hi2c Pointer to I2C handle
* @param scl_port GPIO port for SCL (e.g. GPIOB)
* @param scl_pin GPIO pin for SCL (e.g. GPIO_PIN_6)
* @param sda_port GPIO port for SDA (e.g. GPIOB)
* @param sda_pin GPIO pin for SDA (e.g. GPIO_PIN_7)
*/
HAL_StatusTypeDef I2C_RecoverBus(I2C_HandleTypeDef *hi2c,
GPIO_TypeDef *scl_port, uint16_t scl_pin,
GPIO_TypeDef *sda_port, uint16_t sda_pin)
{
GPIO_InitTypeDef gpio = {0};
/* Step 1 — de-init the I2C peripheral */
HAL_I2C_DeInit(hi2c);
/* Step 2 — reconfigure SCL and SDA as open-drain GPIO outputs */
gpio.Mode = GPIO_MODE_OUTPUT_OD;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
gpio.Pin = scl_pin;
HAL_GPIO_Init(scl_port, &gpio);
gpio.Pin = sda_pin;
HAL_GPIO_Init(sda_port, &gpio);
/* Both lines idle high */
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_SET);
HAL_Delay(1);
/* Step 3 — clock SCL nine times to complete any in-progress byte */
for (int i = 0; i < 9; i++) {
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
HAL_Delay(1);
/*
* If SDA is released (high) the slave has finished its byte.
* We still complete all 9 pulses to be safe.
*/
if (HAL_GPIO_ReadPin(sda_port, sda_pin) == GPIO_PIN_SET) {
/* Slave released SDA — we can stop early after this pulse */
}
}
/* Step 4 — generate a STOP condition: SDA low → high while SCL high */
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
HAL_Delay(1);
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_SET);
HAL_Delay(1);
/* Step 5 — re-initialise the I2C peripheral */
return HAL_I2C_Init(hi2c);
}
Wiring It Into Your Application
The recovery function is most useful wrapped in a retry helper so the application code
stays clean. The pattern below attempts a transmission, and if it gets HAL_BUSY
or HAL_TIMEOUT, runs bus recovery and retries once.
/* I2C1 on PB6 (SCL) / PB7 (SDA) for STM32F4 */
#define I2C_SCL_PORT GPIOB
#define I2C_SCL_PIN GPIO_PIN_6
#define I2C_SDA_PORT GPIOB
#define I2C_SDA_PIN GPIO_PIN_7
HAL_StatusTypeDef I2C_TransmitWithRecovery(I2C_HandleTypeDef *hi2c,
uint16_t dev_addr,
uint8_t *data, uint16_t len)
{
HAL_StatusTypeDef status;
status = HAL_I2C_Master_Transmit(hi2c, dev_addr, data, len, 25);
if (status == HAL_BUSY || status == HAL_TIMEOUT) {
/* Bus is stuck — run the nine-clock recovery */
I2C_RecoverBus(hi2c,
I2C_SCL_PORT, I2C_SCL_PIN,
I2C_SDA_PORT, I2C_SDA_PIN);
/* Single retry with a more generous timeout */
status = HAL_I2C_Master_Transmit(hi2c, dev_addr, data, len, 100);
}
return status;
}
Clock Stretching Timeouts Are a Separate Issue
If you are using sensors that perform long measurements before responding — MLX90614 (thermopile IR), SHT31 in high-repeatability mode, or any sensor with on-board processing — clock stretching timeout is a distinct problem from the stuck-BUSY errata.
The slave holds SCL low legitimately while it completes a measurement. The HAL default
timeout of 25 ms can expire before the measurement is done. The sensor then returns
HAL_TIMEOUT, not HAL_BUSY. The fix here is a larger timeout
value — consult the sensor datasheet for maximum measurement time and add 20% margin.
/*
* SHT31 high-repeatability measurement takes up to 15 ms.
* SHT31 datasheet table 4 — use 20 ms minimum, 30 ms with margin.
*/
#define SHT31_MEAS_TIMEOUT_MS 30
HAL_I2C_Master_Receive(&hi2c1,
SHT31_ADDR << 1,
rx_buf, sizeof(rx_buf),
SHT31_MEAS_TIMEOUT_MS);
Prevention: A Supervisory Task
In production systems running FreeRTOS, a lightweight supervisor task that periodically checks the I2C state and runs recovery if needed is more robust than relying on inline retry logic. This is especially important in systems where the I2C bus is shared between multiple peripherals.
void I2C_SupervisorTask(void *arg)
{
I2C_HandleTypeDef *hi2c = (I2C_HandleTypeDef *)arg;
for (;;) {
vTaskDelay(pdMS_TO_TICKS(5000)); /* check every 5 seconds */
if (hi2c->State == HAL_I2C_STATE_BUSY ||
hi2c->State == HAL_I2C_STATE_BUSY_TX ||
hi2c->State == HAL_I2C_STATE_BUSY_RX)
{
/* I2C has been stuck for at least 5 seconds — recover */
I2C_RecoverBus(hi2c,
I2C_SCL_PORT, I2C_SCL_PIN,
I2C_SDA_PORT, I2C_SDA_PIN);
/* Log the event for diagnostics */
Error_Log("I2C bus recovered by supervisor");
}
}
}
Summary
- The stuck-BUSY condition is a documented STM32 silicon errata, not a software bug
- Nine SCL pulses as GPIO output release any slave stuck mid-byte, then generate a STOP
- Wrap transmit/receive calls with a recovery-and-retry helper to keep application code clean
- Clock stretching timeouts are a separate, configuration issue — increase the timeout to match the sensor datasheet
- In production, a FreeRTOS supervisor task provides a backstop against indefinitely stuck buses