How Do You Configure STM32 I2C and Recover From a BUSY-Flag Lockup?
Last updated 12 July 2026 · 9 min read
Direct Answer
STM32 I2C is configured through HAL_I2C_Init() with a Timing register value (STM32CubeMX's timing calculator generates this from the target bus speed and APB clock) — Standard Mode (100 kHz), Fast Mode (400 kHz), or Fast Mode Plus (1 MHz, on pins with FMP capability) are all set the same way, just with a different Timing value. Separately from configuration, a well-known STM32 I2C failure mode is the peripheral's BUSY flag remaining set after a bus fault — a slave holding SDA low mid-transaction, an unexpected reset, or a device power-cycling while addressed — even though the bus is now electrically idle. Because HAL_I2C_Init() and every transaction function check BUSY before proceeding, every subsequent I2C call returns HAL_BUSY or HAL_TIMEOUT indefinitely until the peripheral is recovered: disable the I2C peripheral, reconfigure SDA/SCL as GPIO, manually clock out up to 9 pulses to force any stuck slave to release SDA, generate a STOP condition, then re-enable the peripheral with HAL_I2C_Init().
Detailed Explanation
I2C is one of the most commonly used STM32 peripherals — sensors, EEPROMs, RTCs, and GPIO expanders overwhelmingly use it — but it is also one of the more fragile buses in the field, because every device on it shares the same two open-drain lines. This page covers both halves of working with it on STM32: configuring the peripheral correctly with HAL, and recovering from the specific, well-documented failure mode where the peripheral's BUSY flag gets stuck set after a bus fault, silently hanging every subsequent transaction. For the I2C protocol itself — open-drain signalling, addressing, and why pull-ups are required — see What Is I2C?; this page assumes that background and focuses on the STM32-specific peripheral and its known lockup behaviour.
STM32 I2C Peripheral Generations: v1 vs v2
STM32 parts implement two functionally different I2C hardware blocks, and which one a given part has matters for both configuration and recovery:
- I2C v1 — used on STM32F1, F2, F4, and L1 series. Configured with clock-speed and rise-time registers, and the classic source of the BUSY-flag lockup behaviour described below.
- I2C v2 — used on STM32F3, F7, G0, G4, H7, L4, L5, and later series. A redesigned peripheral with a single Timing register (calculated once for the target bus speed and APB clock, rather than derived at runtime) and native support for Fast Mode Plus and SMBus. Its internal state machine handles bus errors more predictably, though a genuinely stuck bus (a slave holding SDA low) still needs the same physical-layer recovery technique.
CubeMX abstracts most of this difference away in the generated HAL init code, but the recovery sequence later in this page differs by generation — know which peripheral your target part has before implementing it.
Configuring I2C with HAL
CubeMX's I2C configuration panel includes a Timing calculator: select the target mode (Standard 100 kHz, Fast 400 kHz, or Fast Mode Plus 1 MHz on FMP-capable pins) and CubeMX computes the correct Timing register value for the configured APB clock frequency, generating it directly into MX_I2C1_Init(). Manually calculating this value from the reference manual's timing formula is rarely necessary — trust the CubeMX-generated value.
hi2c1.Instance = I2C1;
hi2c1.Init.Timing = 0x10909CEC; // CubeMX-generated for Fast Mode, 400 kHz, on this part's APB clock
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
HAL_I2C_Init(&hi2c1);
A basic master write/read against a peripheral (an EEPROM, sensor, or RTC — see external RTC ICs for a worked example device) uses the memory-addressed variants when the target has internal registers:
uint8_t reg = 0x00;
uint8_t data;
// Write a register pointer, then read back its value — common for sensor/RTC registers
HAL_I2C_Mem_Write(&hi2c1, DEVICE_ADDR << 1, reg, I2C_MEMADD_SIZE_8BIT, &data, 1, HAL_MAX_DELAY);
HAL_I2C_Mem_Read(&hi2c1, DEVICE_ADDR << 1, reg, I2C_MEMADD_SIZE_8BIT, &data, 1, HAL_MAX_DELAY);
Note the address is left-shifted by one bit — HAL's 7-bit addressing API expects the 7-bit device address already positioned in bits [7:1], matching how the address appears on the wire alongside the R/W bit in bit 0. This is one of the most common first-time STM32 I2C mistakes: passing the raw 7-bit address without the shift, which addresses the wrong device (or no device) on the bus.
For a multi-peripheral design combined with DMA-driven transfers, see STM32 HAL DMA configuration for the equivalent HAL_I2C_Mem_Write_DMA()/HAL_I2C_Mem_Read_DMA() pattern, and STM32 NVIC interrupt priority configuration for the interrupt-priority constraints that apply if I2C DMA callbacks interact with FreeRTOS.
The BUSY-Flag Lockup Problem
The failure this page is really about: after a bus fault — a slave device losing power mid-transaction, a connector unplugged while the bus is live, an ESD event, or an unexpected external reset of a peripheral while it's holding SDA low as part of a clock-stretch or an in-progress byte — the STM32 I2C peripheral's BUSY flag can remain set even once the bus itself is electrically idle again. This is a well-known and widely reported behaviour of the I2C v1 peripheral (STM32F1/F2/F4/L1) in particular, commonly discussed in ST's own community forums and application notes on I2C bus recovery.
Because HAL_I2C_Init() and every HAL_I2C_Master_*/HAL_I2C_Mem_* function check the BUSY flag before proceeding, a stuck BUSY flag makes every subsequent I2C call on that peripheral fail — typically returning HAL_BUSY or timing out — even though nothing is electrically wrong with the bus anymore. Re-initialising the peripheral with HAL_I2C_Init() alone does not clear this condition on affected parts, because the peripheral's I2C-specific reset happens through a different mechanism than the HAL init call, which is the detail that catches most people the first time they hit this.
Recovering from a Locked I2C Bus
The reliable fix works at two levels, and both matter:
1. Peripheral-level reset (STM32-specific). On I2C v1 parts, toggle the peripheral's software reset bit (I2C_CR1_SWRST in the I2C_CR1 register) — set it, hold briefly, then clear it — which forces the peripheral's internal state machine back to its reset state independently of the HAL's own init sequence:
// I2C v1 (F1/F2/F4/L1): force a peripheral-level reset before HAL_I2C_Init()
__HAL_I2C_DISABLE(&hi2c1);
hi2c1.Instance->CR1 |= I2C_CR1_SWRST;
for (volatile int i = 0; i < 100; i++); // brief delay
hi2c1.Instance->CR1 &= ~I2C_CR1_SWRST;
HAL_I2C_Init(&hi2c1);
On I2C v2 parts (F3/F7/G0/G4/H7/L4/L5), the equivalent is disabling and re-enabling the peripheral via the PE bit in I2C_CR1, which the HAL's HAL_I2C_DeInit()/HAL_I2C_Init() pair already performs correctly — the peripheral-level half of the recovery is generally handled by a clean deinit/init cycle on this generation.
2. Bus-level unstick (any I2C bus, any MCU). If a slave is physically holding SDA low — the actual bus-level fault, independent of what the STM32 peripheral thinks — a peripheral reset alone won't fix it, because the bus itself is still stuck. This requires manually clocking the bus, which the I2C-bus specification describes as a bus-clear procedure: reconfigure SCL and SDA as plain GPIO, generate up to 9 clock pulses on SCL (enough to complete any partial byte a stuck slave might be waiting on), watch for SDA to release, then generate a STOP condition before switching the pins back to I2C alternate function and re-initialising the peripheral:
// Bus-level unstick: reconfigure as GPIO before this runs (see forum thread below
// for the full GPIO reconfiguration and pin-mode restore code)
for (int i = 0; i < 9; i++) {
HAL_GPIO_WritePin(SCL_GPIO_Port, SCL_Pin, GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(SCL_GPIO_Port, SCL_Pin, GPIO_PIN_SET);
HAL_Delay(1);
if (HAL_GPIO_ReadPin(SDA_GPIO_Port, SDA_Pin) == GPIO_PIN_SET) break; // slave released SDA
}
// Generate a STOP condition manually: SDA low-to-high while SCL is high
HAL_GPIO_WritePin(SDA_GPIO_Port, SDA_Pin, GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(SCL_GPIO_Port, SCL_Pin, GPIO_PIN_SET);
HAL_Delay(1);
HAL_GPIO_WritePin(SDA_GPIO_Port, SDA_Pin, GPIO_PIN_SET);
This bus-level technique is protocol-defined, not STM32-specific — it applies to any I2C master recovering a bus with a stuck slave. See the I2C bus-hang forum discussion for the complete GPIO reconfiguration (switching pin mode away from I2C alternate function and back) and a real field example of the fault this recovers from.
Design Considerations
- Always implement the full recovery sequence, not just re-init. Calling
HAL_I2C_Init()alone after a bus fault does not reliably clear a stuck BUSY flag on I2C v1 parts — the peripheral-level SWRST toggle is a required step, and the bus-level GPIO unstick is required on top of that if a slave is actually holding SDA low. - Never use
HAL_MAX_DELAYin production I2C code without a fallback. A blocking call with an infinite timeout against a bus fault hangs the calling task forever. Use a bounded timeout, and trigger the recovery sequence on timeout rather than retrying the same call indefinitely. - Know which I2C peripheral generation your part has before implementing recovery. The SWRST register bit approach applies to I2C v1 (F1/F2/F4/L1); on I2C v2 parts, a clean
HAL_I2C_DeInit()/HAL_I2C_Init()cycle is generally sufficient for the peripheral-level half of recovery. Zeus Design develops STM32 firmware with fault-tolerant I2C bus handling for products with field-replaceable or hot-pluggable I2C peripherals. - Hot-pluggable I2C connections are the highest-risk trigger. Any product design that allows an I2C-connected module or sensor to be connected or disconnected while powered should treat this recovery sequence as a required part of the driver, not an edge case.
Common Mistakes
- Passing the raw 7-bit device address to HAL without the left shift. HAL's addressing functions expect the 7-bit address already positioned in bits [7:1] — forgetting the
<< 1addresses the wrong device or gets no response at all, and looks identical to a wiring fault at first glance. - Assuming
HAL_I2C_Init()alone clears a stuck BUSY flag. On I2C v1 parts specifically, this is not sufficient — the SWRST bit toggle is a separate, required step that many first implementations miss. - Blocking forever on
HAL_MAX_DELAYin a driver that's supposed to survive a bus fault. This turns a recoverable I2C fault into an application hang, since the calling task never regains control to run the recovery sequence. - Treating a stuck-bus symptom as a wiring problem and never implementing recovery. A device that only occasionally hangs after a specific trigger (a hot-plug, a brownout on one sensor) is a strong signal for this exact lockup — see the forum discussion for a worked diagnosis of this symptom pattern.
- Running the bus-level GPIO unstick without first reconfiguring the pins away from I2C alternate function. The pins must be temporarily set to plain GPIO output/input mode for the manual clock pulses to have any effect, then switched back to I2C alternate function mode before re-initialising the peripheral.
Frequently Asked Questions
- Does every STM32 series have the same I2C BUSY-flag lockup behaviour?
- No — this is specific to the older I2C peripheral generation used on STM32F1, F2, F4, and L1 devices. Newer series (F3, F7, G0, G4, H7, L4, L5, and later) use a redesigned I2C peripheral with a different internal state machine; it is less prone to getting permanently stuck in the same way, though a physically stuck bus (a slave holding SDA low) still requires the same GPIO bit-bang recovery technique regardless of which peripheral generation is driving it — that part of the problem is a bus-level condition, not a peripheral-specific one.
- Can I avoid the BUSY-flag lockup entirely instead of just recovering from it?
- Not entirely, since the root cause is usually a downstream device or wiring fault outside the MCU's control — a slave losing power mid-transaction, an ESD event, or a connector unplugged while live. What you can do is reduce how often it happens (hot-pluggable connectors are the most common trigger) and make sure every I2C transaction path times out and calls the recovery sequence rather than blocking forever, so a fault degrades to a retried transaction instead of a hung application.
- Why does my I2C transaction return HAL_BUSY even though HAL_I2C_GetState() reports HAL_I2C_STATE_READY?
- These check two different things. HAL_I2C_GetState() reports the HAL driver's own software state machine, which can show READY even while the peripheral's hardware BUSY flag (I2C_SR2 bit 1, or ISR bit 15 on the newer peripheral) is stuck set from an earlier bus fault. HAL_I2C_Master_Transmit() and similar functions check the hardware flag directly before starting a transfer, so a stuck hardware flag produces HAL_BUSY regardless of what the driver's software state reports. Check the hardware BUSY bit directly in the debugger if the two seem to disagree.
References
Related Questions
What Is I2C (Inter-Integrated Circuit)?
I2C is a two-wire serial bus for addressing multiple peripherals over shared SDA/SCL lines. Learn how addressing, speed grades, and pull-up resistors work.
How Do You Configure STM32 HAL DMA for UART, SPI, and ADC?
Configure STM32 HAL DMA for UART, SPI, and ADC — normal vs circular mode, interrupt callbacks, double buffering, and cache coherency on STM32H7/F7.
How Does the STM32 Clock Tree Work?
The STM32 clock tree routes HSE or HSI through a PLL to generate SYSCLK, then divides it across AHB and APB buses. Learn how it works and how to configure it.
How Do You Configure STM32 Peripherals with HAL and CubeMX?
STM32CubeMX generates HAL initialisation code for UART, SPI, and I2C from a GUI. This guide explains key settings and how generated code maps to the hardware.
How Do You Configure STM32 NVIC Interrupt Priorities?
Learn how to configure STM32 NVIC interrupt priorities using HAL, priority grouping, and the FreeRTOS configMAX_SYSCALL_INTERRUPT_PRIORITY constraint.
How Do You Shift Logic Levels Between 3.3V and 5V, and When Do You Need a Level Shifter?
How to shift logic levels between 3.3V and 5V: when a direct connection is safe, resistor dividers, and bidirectional vs unidirectional level shifter ICs.
Related Forum Discussions
STM32L4 never wakes from Stop mode — button EXTI interrupt just doesn't fire
Working on a battery-powered sensor node, STM32L476RG (Nucleo board for now). Idea is: device sits in Stop 2 mode most of the time, wakes wh
STM32F401 UART printing garbage after switching to 84 MHz PLL — same 115200 baud in CubeMX and PuTTY
Got a WeAct Black Pill (STM32F401CCU6) project that's been running happily on the default HSI clock at 16 MHz. Using USART1 on PA9/PA10 thro
STM32H743 HAL_UART_Receive_DMA fires error callback immediately — TEIF1 set, RxCplt never fires
Upgrading a project from STM32F4 to STM32H743. UART DMA receive worked on the F4 without any issues — standard CubeMX setup, call HAL_UART_R
STM32 GPIO interrupt configured but ISR never fires — what am I missing?
Trying to use a button on PA0 to trigger an interrupt on an STM32F411 Nucleo board. Using HAL, generated the init code with CubeMX. The GPIO