← Back to Zephyr

I2C — Inter-Integrated Circuit

What Is I2C?

I2C is a two-wire, multi-master, multi-slave serial protocol invented by Philips in 1982. “Inter-Integrated Circuit” — designed for chips communicating on the same PCB.

Used for: IMUs, temperature sensors, magnetometers, barometers, OLEDs, EEPROMs, real-time clocks — anything that doesn’t need high speed but benefits from simplicity (only 2 wires).


Physical Wires — Just 2

Master (STM32)                     Slaves
──────────────────────────────────────────────────────────
SDA ─────┬─────────────────────────  SDA  SDA  SDA   (data)
         │ pull-up to VCC (4.7kΩ)   [A]  [B]  [C]
SCL ─────┴─────────────────────────  SCL  SCL  SCL   (clock)
         │ pull-up to VCC (4.7kΩ)

SDA: bidirectional data line — both master and slaves share this wire SCL: clock, driven by master

The pull-up resistors are essential. Both lines are open-drain — devices can only pull line LOW. The resistors pull the line HIGH when nobody is pulling it low. Without pull-ups, nothing works.

Typical pull-up: 4.7 kΩ at 100kHz/400kHz, 2.2 kΩ at 1MHz (Fast-mode+).


Addressing — How Slaves Are Identified

Unlike SPI (which uses a dedicated CS wire per slave), I2C identifies slaves by address.

Each slave has a 7-bit address (0x00-0x7F) — up to 127 slaves on one bus. Some devices use 10-bit addresses for up to 1023 slaves.

Common I2C addresses:
ICM-42688 IMU:         0x68 (AD0 low)  or 0x69 (AD0 high)
MPU-6050 IMU:          0x68 or 0x69
BMP280 pressure:       0x76 or 0x77
ADS1115 ADC:           0x48, 0x49, 0x4A, or 0x4B
OLED SSD1306:          0x3C or 0x3D
AT24C EEPROM:          0x50-0x57

Many devices have 1-2 address pins (ADDRx) so you can configure the address and put 2-4 of the same chip on one bus.


How a Transfer Works

Start condition

Master pulls SDA LOW while SCL is HIGH (special "start" signal)
  SCL: ─────┐         ...
  SDA: ──┐  └─ ...
         └── transition while SCL high = START

Data frame: 8 bits + 1 ACK bit

Master sends 7-bit address + R/W bit:

SCL:  ─┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌────
       └─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└
SDA:  ─A6─A5─A4─A3─A2─A1─A0─R/W─ACK
                               ↑          ↑
                      0=Write  slave pulls SDA LOW
                      1=Read   to acknowledge

If slave present and ready: pulls SDA LOW (ACK = 0)
If slave absent or busy:    SDA stays HIGH (NACK = 1 → master gets error)

Write sequence (master writes register to slave)

[START] [ADDR+W] [ACK] [REG] [ACK] [DATA0] [ACK] [DATA1] [ACK] [STOP]
   ↑        ↑      ↑     ↑     ↑      ↑      ↑              ↑      ↑
master   master  slave master slave  master slave  ...     slave  master

Read sequence (master reads register from slave)

[START] [ADDR+W] [ACK] [REG] [ACK] [RESTART] [ADDR+R] [ACK] [DATA0] [NACK] [STOP]
                                        ↑          ↑      ↑      ↑      ↑
                                   master sets     master slave  slave  master NACK
                                   read direction          drives SDA   = "I'm done reading"

The repeated start (restart) is key — master switches from write (sending register address) to read (receiving data) without releasing the bus.


I2C Speeds

Mode Speed Use
Standard 100 kHz Old sensors, EEPROMs
Fast 400 kHz Most modern sensors ← default for IMUs
Fast-mode+ 1 MHz High-speed sensors, displays
High-speed 3.4 MHz Rare, needs special hardware

At 400kHz: 1 byte ≈ 25µs. Reading a 12-byte IMU frame = ~300µs. That’s 3% of our 10ms 100Hz budget — acceptable.


I2C in Zephyr

Devicetree

/* app.overlay */
&i2c1 {
    status = "okay";
    pinctrl-0 = <&i2c1_default>;
    pinctrl-names = "default";
    clock-frequency = <I2C_BITRATE_FAST>;  /* 400kHz */

    /* IMU at address 0x68 */
    imu: icm42688@68 {
        compatible = "invensense,icm42688p";
        reg = <0x68>;
        int-gpios = <&gpiob 3 GPIO_ACTIVE_HIGH>;  /* data-ready interrupt */
    };
};

Basic read/write API

#include <zephyr/drivers/i2c.h>

static const struct device *i2c = DEVICE_DT_GET(DT_NODELABEL(i2c1));
#define IMU_ADDR 0x68

/* Write a single register */
int imu_write_reg(uint8_t reg, uint8_t val)
{
    uint8_t buf[2] = { reg, val };
    return i2c_write(i2c, buf, 2, IMU_ADDR);
}

/* Read a single register */
int imu_read_reg(uint8_t reg, uint8_t *out)
{
    return i2c_write_read(i2c, IMU_ADDR, &reg, 1, out, 1);
    /* i2c_write_read: writes reg addr then issues RESTART + read */
}

/* Read multiple consecutive registers (burst read — most efficient) */
int imu_burst_read(uint8_t start_reg, uint8_t *out, size_t len)
{
    return i2c_write_read(i2c, IMU_ADDR, &start_reg, 1, out, len);
}

Reading IMU at 100Hz

#define ACCEL_XOUT_H 0x1F   /* ICM-42688 register map */
#define GYRO_XOUT_H  0x25

void imu_thread_fn(void *a, void *b, void *c)
{
    const struct device *i2c = DEVICE_DT_GET(DT_NODELABEL(i2c1));
    uint8_t raw[12];
    struct imu_data msg;

    /* One-time: wake IMU from sleep */
    imu_write_reg(0x4E /* PWR_MGMT0 */, 0x0F);  /* accel+gyro = low noise mode */
    k_msleep(10);  /* wait for startup */

    while (1) {
        /* Burst read: 6 bytes accel + 6 bytes gyro in one I2C transaction */
        int rc = imu_burst_read(ACCEL_XOUT_H, raw, 12);
        if (rc != 0) {
            LOG_ERR("I2C read failed: %d", rc);
            k_msleep(10);
            continue;
        }

        /* Parse big-endian int16 → float */
        msg.accel_x = (int16_t)((raw[0] << 8) | raw[1]) / 2048.0f;
        msg.accel_y = (int16_t)((raw[2] << 8) | raw[3]) / 2048.0f;
        msg.accel_z = (int16_t)((raw[4] << 8) | raw[5]) / 2048.0f;
        msg.gyro_x  = (int16_t)((raw[6] << 8) | raw[7])  / 16.4f;
        msg.gyro_y  = (int16_t)((raw[8] << 8) | raw[9])  / 16.4f;
        msg.gyro_z  = (int16_t)((raw[10]<< 8) | raw[11]) / 16.4f;
        msg.timestamp_us = k_ticks_to_us_near64(k_uptime_ticks());

        zbus_chan_pub(&imu_chan, &msg, K_NO_WAIT);

        k_msleep(10);  /* 100Hz */
    }
}

Interrupt-driven (more efficient than polling)

Many sensors have a data-ready (DRDY) interrupt pin. Instead of polling every 10ms, the IMU tells you “new data ready”:

#include <zephyr/drivers/gpio.h>

static const struct gpio_dt_spec drdy_gpio =
    GPIO_DT_SPEC_GET(DT_NODELABEL(imu), int_gpios);

static struct gpio_callback drdy_cb_data;
static K_SEM_DEFINE(imu_drdy_sem, 0, 1);

static void drdy_isr(const struct device *dev,
                     struct gpio_callback *cb, uint32_t pins)
{
    k_sem_give(&imu_drdy_sem);  /* wake up reader thread */
}

void imu_thread_fn(void *a, void *b, void *c)
{
    /* Setup DRDY interrupt */
    gpio_pin_configure_dt(&drdy_gpio, GPIO_INPUT);
    gpio_init_callback(&drdy_cb_data, drdy_isr, BIT(drdy_gpio.pin));
    gpio_add_callback(drdy_gpio.port, &drdy_cb_data);
    gpio_pin_interrupt_configure_dt(&drdy_gpio, GPIO_INT_EDGE_RISING);

    /* Configure IMU to assert INT1 when data ready */
    imu_write_reg(0x14 /* INT_CONFIG */, 0x02);  /* INT1 = data-ready */

    while (1) {
        /* Block until IMU says "new data" */
        k_sem_take(&imu_drdy_sem, K_FOREVER);

        /* Read immediately — data is guaranteed fresh */
        imu_burst_read(ACCEL_XOUT_H, raw, 12);
        /* ... parse and publish ... */
    }
}

Why interrupt-driven is better: sleep exactly until data is ready → lower CPU load, more precise timing, no polling jitter.


I2C Bus Errors and Recovery

I2C can get stuck if a transfer is interrupted (power glitch, software reset mid-transfer):

Symptom: slave is holding SDA LOW (it thinks a transfer is in progress)
         master cannot send START condition
         all subsequent reads return -EIO

Fix: send 9 clock pulses on SCL (enough to complete any stuck byte + ACK)
     then send STOP condition

Zephyr handles this automatically with i2c_recover_bus():

int rc = i2c_write_read(i2c, IMU_ADDR, &reg, 1, out, 1);
if (rc == -EIO) {
    LOG_WRN("I2C stuck, recovering...");
    i2c_recover_bus(i2c);
    /* retry once */
    rc = i2c_write_read(i2c, IMU_ADDR, &reg, 1, out, 1);
}

I2C vs SPI Summary

I2C SPI
Wires 2 (shared, pulled high) 4+ (one CS per slave)
Speed Up to 3.4 MHz Up to 50+ MHz
Slaves Up to 127 (by address) Unlimited (by CS lines)
Full duplex No Yes
Error detection ACK/NACK None (need CRC in application)
Cable length <30cm <100cm
Use for IMU Yes (convenient) Yes (faster, better for high-rate)

For our 100Hz IMU: I2C works fine. If you need 1000Hz+, switch to SPI.


Common Mistakes

Mistake Symptom Fix
Missing pull-up resistors Nothing works, all reads fail Add 4.7kΩ to VCC on SDA and SCL
Wrong I2C address -ENXIO error Scan with i2c_scan or check AD0/AD1 pin
Too many slaves → pull-up too weak Works at low speed, fails at fast Use lower pull-up value (1kΩ), use buffer/mux
Polling too slowly Miss first data, stale reads Use DRDY interrupt
Not waiting for sensor startup First read returns zeros Add k_msleep(20) after power-on