The I2C bus is often considered a simple and reliable interface for connecting chips, but in reality, it comes with a few hidden pitfalls. One of the most common and frustrating issues is the I2C bus lockup.
This article explains the fundamentals of I2C, how lockups occur, and shares practical methods to prevent and recover from I2C lockups in embedded system design.
What is I2C? #
I2C (Inter-Integrated Circuit) is a two-wire serial communication protocol developed by Philips (now NXP) more than 40 years ago. It is widely used for connecting low- to medium-speed peripherals such as EEPROMs, temperature sensors, and ADCs to microcontrollers and SoCs.
The bus has two signal lines:
- SCL (clock line)
- SDA (data line)
Both are open-drain and require pull-up resistors to achieve wired-AND logic. This design allows multiple devices to share the same bus without conflict.
Here’s an example of a typical I2C multi-byte read transaction:
Before communication starts, the master must ensure the bus is idle (both SCL and SDA high). If either line is held low, the bus is considered busy and no new transaction can begin.
How Do I2C Lockups Happen? #
Despite its simplicity, I2C can fall into a permanent busy state—a lockup—where no new communication is possible. Common causes include:
- Noise or interference
- A missing or extra clock edge may cause the slave to hold SDA low indefinitely.
- Power-up glitches
- MCU I/O pins may glitch before initialization, leading the slave to misinterpret signals.
- Software crashes or resets
- Breaking execution during a transfer (e.g., at a debugger breakpoint) can leave the slave thinking communication is still ongoing.
The result:
- The slave holds SDA low, waiting for more clocks.
- The master thinks the transfer has ended and sends no further clocks.
- The bus is locked up.
Preventing I2C Lockups #
Good design starts with prevention. Here are proven practices:
-
Hardware measures
- Use stronger pull-up resistors to improve SCL/SDA rise times.
- Ensure I2C master pins default to high after reset.
-
Software measures
- Configure I/O carefully during initialization to avoid false transitions.
- Run a recovery clock sequence at system startup to clear any latent lockups.
Detecting and Recovering from I2C Lockups #
Even with precautions, lockups can still occur. A robust system must support detection and recovery:
-
Detection
- Always set timeouts when waiting for I2C events (bus idle, transfer complete). Never wait forever.
-
Recovery
- Reset the slave device (if hardware allows).
- Force clock pulses: Toggle the SCL line for at least 10 cycles to make the slave release SDA.
Why 10?
- 9 clocks for a byte, plus 1 for the ACK/NACK bit.
If the microcontroller’s I2C peripheral can’t generate these manually, reconfigure SCL as a GPIO output to bit-bang the pulses.
Example: Software Recovery Routine #
The following example is based on the NXP KL17 microcontroller. Since its I2C controller cannot directly force clocks, SCL must be temporarily reconfigured as a GPIO:
#define I2C_RECOVER_NUM_CLOCKS 10U /* Number of recovery clocks */
#define I2C_RECOVER_CLOCK_FREQ 50000U /* Recovery frequency */
#define I2C_RECOVER_CLOCK_DELAY_US (1000000U / (2U * I2C_RECOVER_CLOCK_FREQ))
void i2cLockupRecover (void)
{
/* Configure SCL as GPIO */
PORT_SetPinMux(I2C_SCL_PORT, I2C_SCL_GPIO_PIN, kPORT_MuxAsGpio);
const gpio_pin_config_t pinConfig = {
.pinDirection = kGPIO_DigitalOutput,
.outputLogic = 1U,
};
GPIO_PinInit(I2C_SCL_GPIO_PORT, I2C_SCL_GPIO_PIN, &pinConfig);
/* Generate recovery clock pulses */
for (unsigned int i = 0U; i < I2C_RECOVER_NUM_CLOCKS; ++i) {
delayUs(I2C_RECOVER_CLOCK_DELAY_US);
GPIO_PinWrite(I2C_SCL_GPIO_PORT, I2C_SCL_GPIO_PIN, 0U);
delayUs(I2C_RECOVER_CLOCK_DELAY_US);
GPIO_PinWrite(I2C_SCL_GPIO_PORT, I2C_SCL_GPIO_PIN, 1U);
}
/* Reconfigure as I2C SCL */
PORT_SetPinMux(I2C_SCL_PORT, I2C_SCL_GPIO_PIN, kPORT_MuxAlt4);
}
Tip: Always run a recovery clock sequence at system startup. This not only prevents power-up glitches from causing lockups but also saves debugging time when restarting after resets.
The figure below shows recovery pulses generated at startup, immediately followed by the first valid I2C transaction:
Conclusion #
I2C lockups are a real and frequent issue in embedded systems. However, with sound hardware design and software safeguards, they can be prevented, detected, and recovered effectively.
Key practices include:
- Stronger pull-ups and safe default I/O states
- Careful initialization and timeout enforcement
- Slave resets or manual clock pulses for recovery
Bottom line: Always design with a lockup recovery mechanism in mind. It not only improves reliability but also simplifies debugging and saves development time.