STM32 I2C Communication Masterclass: Complete Guide with HAL Code Examples

Key Takeaway: The STM32 I2C peripheral is a powerful multi-master, multi-slave serial communication interface that uses just two wires (SCL and SDA). Mastering STM32 I2C communication with the HAL library enables reliable data exchange with sensors, displays, and external memory — a fundamental skill for embedded systems development in 2026.

STM32 I2C Communication Masterclass - Bus Architecture, Protocol Frame, and HAL Functions
STM32 I2C bus architecture, protocol frame structure, CubeMX configuration, and core HAL functions covered in this guide

1. I2C Protocol Overview

The Inter-Integrated Circuit (I2C) protocol, developed by Philips Semiconductor (now NXP), has been a cornerstone of embedded communication for over three decades. In 2026, it remains one of the most widely used serial communication protocols for connecting microcontrollers to peripheral devices like sensors, ADCs, DACs, EEPROMs, and OLED displays.

Key advantages of I2C:

  • Two-wire interface: Only SDA (data) and SCL (clock) lines are needed, reducing PCB routing complexity
  • Multi-master support: Multiple STM32 microcontrollers can share the same bus
  • Multi-slave capability: Up to 127 devices on a single bus using 7-bit addressing
  • Built-in arbitration: Collision detection is handled at the hardware level
  • Acknowledge mechanism: Each byte transfer is verified with an ACK/NACK bit

The I2C bus operates at standard data rates of 100 kbit/s (Standard Mode), 400 kbit/s (Fast Mode), and up to 1 Mbit/s (Fast Mode Plus) on modern STM32 microcontrollers. The STM32F4 and STM32G4 series support all three modes natively.

2. STM32 I2C Hardware Architecture

The STM32 I2C peripheral includes several hardware features that simplify implementation:

Clock Generator: The SCL clock is generated by the master and can be configured from 10 kHz to 1 MHz depending on the STM32 series. The clock is derived from the APB peripheral clock (typically 42-84 MHz on STM32F4) and divided down using the TIMINGR register in newer STM32 MCUs, or CCR register in legacy models.

Data Register (DR): An 8-bit shift register handles both transmit and receive operations. In interrupt mode, the TXE (transmit empty) and RXNE (receive not empty) flags trigger interrupts when the register is ready for the next byte.

Own Address Registers (OAR1, OAR2): In slave mode, the STM32 can be configured with up to two own addresses, enabling dual-address recognition. This is useful for implementing custom protocols on top of I2C.

Status Flags: The I2C peripheral provides comprehensive status flags including SB (start bit), ADDR (address matched), BTF (byte transfer finished), TXE, RXNE, AF (acknowledge failure), and STOPF (stop condition).

3. STM32CubeMX Configuration

Setting up I2C in STM32CubeMX is straightforward. Follow these steps:

  1. Select I2C peripheral: In the Pinout & Configuration tab, navigate to Connectivity > I2C1 (or I2C2/I2C3 depending on your target)
  2. Set I2C mode: Choose “I2C” as the mode. For most applications, Standard Mode (100 kHz) or Fast Mode (400 kHz) is sufficient
  3. Configure timing: In newer STM32 families (F0, G0, G4, L4, H7), set the TIMINGR value based on your APB clock frequency. For STM32F1/F4 legacy peripherals, set the clock control register (CCR) directly
  4. Configure GPIO: Verify that the I2C pins (typically PB6 for SCL, PB7 for SDA on I2C1) are configured as Alternate Function Open-Drain with appropriate pull-up resistors (4.7kΩ for Standard Mode, 2.2kΩ for Fast Mode)
  5. Optional settings: Enable general call, clock stretching, or DMA requests if needed for your application
  6. Generate code: Click “Generate Code” to produce the HAL-based project for your preferred IDE (CubeIDE, Keil, or IAR)

4. STM32 HAL I2C Functions

The STM32 HAL provides three levels of I2C API: blocking (polling), interrupt, and DMA. Here are the essential functions you’ll use:

Blocking (Polling) Functions

// Master Transmit - sends data to slave device
HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// Master Receive - reads data from slave device
HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// Memory Write - writes to a specific register on the slave device
HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);

// Memory Read - reads from a specific register on the slave device
HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);

Interrupt-Based Functions

// Non-blocking versions with IT suffix
HAL_I2C_Master_Transmit_IT(...);
HAL_I2C_Master_Receive_IT(...);
HAL_I2C_Mem_Write_IT(...);
HAL_I2C_Mem_Read_IT(...);

// Complete callback - called when transfer finishes
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c);
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c);
void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c);
void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c);

// Error callback
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c);

5. I2C Master Mode Implementation

Here’s a complete example of reading temperature and humidity from a BME280 sensor over I2C using the STM32 HAL:

#include "stm32f4xx_hal.h"

I2C_HandleTypeDef hi2c1;

#define BME280_ADDR      (0x76 << 1)  // 7-bit address shifted for HAL
#define BME280_ID_REG    0xD0         // Chip ID register
#define BME280_CTRL_REG  0xF4         // Control measurement register
#define BME280_DATA_REG  0xF7         // Pressure data start

// Initialize I2C peripheral
void MX_I2C1_Init(void)
{
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 400000;        // 400 kHz Fast Mode
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
    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);
}

// Read sensor ID to verify communication
uint8_t BME280_ReadID(void)
{
    uint8_t id = 0;
    HAL_I2C_Mem_Read(&hi2c1, BME280_ADDR, BME280_ID_REG,
                     I2C_MEMADD_SIZE_8BIT, &id, 1, 100);
    return id;  // Should return 0x60 for BME280
}

// Configure sensor
void BME280_Init(void)
{
    uint8_t data[2];
    // Set normal mode, temp oversampling x2, pressure x16
    data[0] = 0xF4;
    data[1] = 0xB7;
    HAL_I2C_Master_Transmit(&hi2c1, BME280_ADDR, data, 2, 100);
}

// Read raw temperature value
int32_t BME280_ReadTemperature(void)
{
    uint8_t buf[3];
    HAL_I2C_Mem_Read(&hi2c1, BME280_ADDR, BME280_DATA_REG + 3,
                     I2C_MEMADD_SIZE_8BIT, buf, 3, 100);
    return ((int32_t)buf[0] << 12) | ((int32_t)buf[1] <> 4);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_I2C1_Init();

    if (BME280_ReadID() != 0x60) {
        Error_Handler();  // Sensor not detected
    }
    BME280_Init();

    while (1)
    {
        int32_t raw_temp = BME280_ReadTemperature();
        // Apply compensation formula
        HAL_Delay(1000);
    }
}

6. Multiple Slave Communication

One of the strengths of I2C is multi-slave communication. Here’s how to communicate with multiple devices on the same bus:

#define BME280_ADDR   (0x76 << 1)   // Environmental sensor
#define OLED_ADDR     (0x3C << 1)   // SSD1306 OLED display
#define EEPROM_ADDR   (0x50 << 1)   // 24LC256 EEPROM

// Read from each slave sequentially
void ReadAllSensors(void)
{
    uint8_t temp_data[3];
    uint8_t oled_cmd[2];
    uint8_t eeprom_buf[16];

    // Read BME280
    HAL_I2C_Mem_Read(&hi2c1, BME280_ADDR, 0xF7,
                     I2C_MEMADD_SIZE_8BIT, temp_data, 3, 100);

    // Send command to OLED
    oled_cmd[0] = 0x00;  // Command mode
    oled_cmd[1] = 0xAF;  // Display ON
    HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, oled_cmd, 2, 100);

    // Read from EEPROM
    HAL_I2C_Mem_Read(&hi2c1, EEPROM_ADDR, 0x0000,
                     I2C_MEMADD_SIZE_16BIT, eeprom_buf, 16, 100);
}

Important: Each slave must have a unique I2C address. Address conflicts are the most common issue in multi-slave I2C systems. Many sensors like the BME280 allow address changes via the SDO pin (0x76 or 0x77).

7. Common Issues and Debugging

Based on extensive experience with STM32 I2C debugging, here are the most common problems and their solutions:

Bus Hang / SDA Line Stuck Low: This occurs when a slave device fails to release the SDA line. Solution: cycle power to the slave device, or manually toggle SCL to free the bus (clock stretching timeout).

ACK Failure: If HAL_I2C_Master_Transmit returns HAL_ERROR with HAL_I2C_ERROR_AF, the addressed device did not acknowledge. Check: correct address (shifted left by 1 for HAL), proper power to the slave, correct wiring, and no address conflicts.

Incorrect Data: Valid I2C frames but wrong register values. This usually indicates incorrect register addressing or misconfigured oversampling settings. Use a logic analyzer to verify the I2C bus traffic.

Busy Flag Set (BUSYF): The I2C bus may remain busy if a previous transfer was interrupted. The HAL_I2C_Reset() function clears the bus state, but if this fails, the I2C peripheral may need to be reinitialized.

Pull-up Resistor Issues: Incorrect pull-up resistor values cause signal integrity problems. For Standard Mode (100 kHz) with short cables, 4.7kΩ resistors work well. For Fast Mode (400 kHz) or longer cables, use 2.2kΩ or even 1kΩ resistors. Total bus capacitance should stay under 400 pF.

8. Frequently Asked Questions

What is the difference between STM32 I2C legacy and new HAL?

The legacy HAL (STM32F1/F4) uses the CCR register to set clock speed. The new HAL (STM32F0/G0/G4/L4/H7) uses the TIMINGR register, which provides more precise timing control. Check your STM32 reference manual to determine which version applies to your MCU.

Can I use multiple STM32 I2C peripherals simultaneously?

Yes. Most STM32 MCUs have 2-4 I2C peripherals. STM32F4 has 3 (I2C1, I2C2, I2C3), STM32H7 has up to 4. Each peripheral operates independently on its own GPIO pins and can be configured for different speeds.

What is the maximum cable length for STM32 I2C?

For Standard Mode (100 kHz), cables up to 1 meter are reliable with proper pull-up resistors. For longer distances, consider using an I2C buffer/extender like the PCA9600 or switching to RS-485 or CAN bus for factory-floor communications.

Why does my STM32 I2C bus hang after an error?

The I2C bus enters a bus-locked state when the slave pulls SDA low and the master generates a STOP condition. The fix is to: 1) Reset the I2C peripheral with HAL_I2C_DeInit(), 2) Generate 9 clock pulses on SCL using GPIO bit-banging, and 3) Reinitialize the I2C peripheral. This releases the bus without power cycling.

How do I choose between I2C and SPI for my STM32 project?

Use I2C when: you need fewer pins (2 vs 4+), connecting multiple slaves, or using devices that only support I2C (many sensors). Use SPI when: you need higher speed (>10 Mbps), streaming data (audio, video), or communicating with SD cards and displays.

Sources

  1. STMicroelectronics – AN4031: I2C Communication with STM32 Microcontrollers
  2. Controllerstech – STM32 I2C Tutorial with HAL
  3. NXP – I2C Bus Specification and User Manual
  4. DeepBlue Embedded – STM32 I2C HAL Library Examples

Disclosure: As an Amazon Associate, we earn from qualifying purchases. Some links on this page are affiliate links that help support our content at no extra cost to you.

You are currently viewing STM32 I2C Communication Masterclass: Complete Guide with HAL Code Examples

Leave a Reply