| name | zephyr-i2c |
| description | Comprehensive Zephyr I2C subsystem expertise for Inter-Integrated Circuit bus communication. Use when: (1) Configuring I2C controllers or devices via devicetree or Kconfig, (2) Implementing synchronous I2C transfers with i2c_transfer/i2c_read/i2c_write, (3) Using register access helpers like i2c_reg_read_byte/i2c_reg_write_byte, (4) Implementing asynchronous I2C with i2c_transfer_cb or RTIO, (5) Setting up I2C speed modes (standard 100kHz, fast 400kHz, fast-plus 1MHz), (6) Implementing I2C target/slave mode with callbacks, (7) Using SMBus protocol functions, (8) Troubleshooting I2C communication issues (NACK, bus stuck, clock stretching). |
Zephyr I2C
Expert guidance for Zephyr's I2C driver subsystem covering synchronous and asynchronous transfer APIs, target/slave mode, devicetree configuration, and common usage patterns.
Table of Contents
- API Selection
- Getting Device Reference
- Common Workflows
- Configuration
- Target Mode
- Error Handling
- Troubleshooting
API Selection
Zephyr provides multiple I2C access methods. Choose based on requirements:
| API | Kconfig | Use Case | Blocking? |
|---|
| Synchronous | (default) | Simple transfers, blocking until complete | Yes |
| Async Callback | CONFIG_I2C_CALLBACK | Non-blocking with completion callback | No |
| RTIO | CONFIG_I2C_RTIO | Real-time I/O subsystem integration | No |
| Target Mode | CONFIG_I2C_TARGET | Act as I2C peripheral/slave device | N/A |
Decision Tree
Simple blocking transfer? -> Synchronous (i2c_transfer, i2c_write_read)
Register read/write? -> Helpers (i2c_reg_read_byte, i2c_reg_write_byte)
Need non-blocking? -> Async callback (i2c_transfer_cb)
RTIO integration? -> RTIO API (i2c_rtio_copy)
Respond to controller? -> Target mode (i2c_target_register)
SMBus protocol? -> SMBus API (smbus_byte_data_read, etc.)
Getting Device Reference
From Devicetree (Preferred - for I2C Devices)
Use I2C_DT_SPEC_GET to get a complete I2C specification including bus and address:
#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#define MY_I2C_DEVICE DT_NODELABEL(my_sensor)
static const struct i2c_dt_spec i2c_dev = I2C_DT_SPEC_GET(MY_I2C_DEVICE);
if (!i2c_is_ready_dt(&i2c_dev)) {
printk("I2C device not ready\n");
return -ENODEV;
}
Direct Controller Access
const struct device *i2c = DEVICE_DT_GET(DT_NODELABEL(i2c0));
if (!device_is_ready(i2c)) {
return -ENODEV;
}
#define SENSOR_ADDR 0x48
ret = i2c_read(i2c, buf, sizeof(buf), SENSOR_ADDR);
From Device Node on Bus
#define CODEC_NODE DT_NODELABEL(wm8731)
const struct device *i2c_bus = DEVICE_DT_GET(DT_BUS(CODEC_NODE));
uint16_t addr = DT_REG_ADDR(CODEC_NODE);
Common Workflows
1. Simple Write
#include <zephyr/drivers/i2c.h>
uint8_t data[] = {0x01, 0x02, 0x03};
int ret = i2c_write_dt(&i2c_dev, data, sizeof(data));
if (ret < 0) {
printk("I2C write error: %d\n", ret);
}
ret = i2c_write(i2c, data, sizeof(data), SENSOR_ADDR);
2. Simple Read
uint8_t rx_buf[4];
int ret = i2c_read_dt(&i2c_dev, rx_buf, sizeof(rx_buf));
if (ret < 0) {
printk("I2C read error: %d\n", ret);
}
3. Write-Then-Read (Most Common Pattern)
Combined transaction with RESTART condition - typical for register access:
uint8_t reg_addr = 0x00;
uint8_t rx_data[2];
int ret = i2c_write_read_dt(&i2c_dev, ®_addr, 1, rx_data, sizeof(rx_data));
if (ret < 0) {
printk("I2C write-read error: %d\n", ret);
}
4. Register Access Helpers
Convenient functions for single-byte register operations:
uint8_t value;
ret = i2c_reg_read_byte_dt(&i2c_dev, 0x0F, &value);
ret = i2c_reg_write_byte_dt(&i2c_dev, 0x20, 0x47);
ret = i2c_reg_update_byte_dt(&i2c_dev, 0x20, 0x07, 0x05);
5. Burst Read/Write (Multi-byte Register Access)
uint8_t data[6];
ret = i2c_burst_read_dt(&i2c_dev, 0x28, data, sizeof(data));
uint8_t config[] = {0x47, 0x00, 0x00, 0x88};
ret = i2c_burst_write_dt(&i2c_dev, 0x20, config, sizeof(config));
6. Multi-Message Transfer (Scatter-Gather)
For complex transactions requiring multiple operations in one bus lock:
struct i2c_msg msgs[2];
uint8_t cmd = 0x9F;
uint8_t rx_buf[4];
msgs[0].buf = &cmd;
msgs[0].len = 1;
msgs[0].flags = I2C_MSG_WRITE;
msgs[1].buf = rx_buf;
msgs[1].len = sizeof(rx_buf);
msgs[1].flags = I2C_MSG_RESTART | I2C_MSG_READ | I2C_MSG_STOP;
int ret = i2c_transfer_dt(&i2c_dev, msgs, ARRAY_SIZE(msgs));
if (ret < 0) {
printk("I2C transfer error: %d\n", ret);
}
7. Async Transfer with Callback
Requires CONFIG_I2C_CALLBACK=y:
#include <zephyr/drivers/i2c.h>
static volatile bool transfer_done;
static int transfer_result;
void i2c_callback(const struct device *dev, int result, void *userdata)
{
transfer_result = result;
transfer_done = true;
}
int async_transfer(void)
{
struct i2c_msg msgs[1];
uint8_t data[] = {0x01, 0x02};
msgs[0].buf = data;
msgs[0].len = sizeof(data);
msgs[0].flags = I2C_MSG_WRITE | I2C_MSG_STOP;
transfer_done = false;
int ret = i2c_transfer_cb(i2c_dev.bus, msgs, 1, i2c_dev.addr,
i2c_callback, NULL);
if (ret < 0) {
return ret;
}
while (!transfer_done) {
k_yield();
}
return transfer_result;
}
Configuration
Kconfig Essentials
CONFIG_I2C=y # Enable I2C driver subsystem
CONFIG_I2C_CALLBACK=y # Enable async callback API
CONFIG_I2C_RTIO=y # Enable RTIO API (experimental)
CONFIG_I2C_TARGET=y # Enable target/slave mode
CONFIG_I2C_SHELL=y # Enable I2C shell for debugging
CONFIG_I2C_DUMP_MESSAGES=y # Log all I2C transactions (debug)
Devicetree Essentials
I2C Controller
&i2c0 {
status = "okay";
pinctrl-0 = <&i2c0_default>;
pinctrl-names = "default";
clock-frequency = <I2C_BITRATE_FAST>; /* 400 kHz */
my_sensor: sensor@48 {
compatible = "vendor,sensor";
reg = <0x48>; /* 7-bit I2C address */
};
};
Speed Constants (clock-frequency)
| Constant | Speed |
|---|
I2C_BITRATE_STANDARD | 100 kHz |
I2C_BITRATE_FAST | 400 kHz |
I2C_BITRATE_FAST_PLUS | 1 MHz |
I2C_BITRATE_HIGH | 3.4 MHz |
I2C_BITRATE_ULTRA | 5 MHz |
Message Flags
Common flags for i2c_msg.flags:
| Flag | Description |
|---|
I2C_MSG_WRITE | Write operation (0) |
I2C_MSG_READ | Read operation |
I2C_MSG_STOP | Send STOP after this message |
I2C_MSG_RESTART | Send RESTART before this message |
I2C_MSG_ADDR_10_BITS | Use 10-bit addressing |
Target Mode
I2C target (slave) mode allows the device to respond to an external controller.
Target Callbacks Setup
#include <zephyr/drivers/i2c.h>
static uint8_t rx_buffer[32];
static uint8_t tx_buffer[32];
static size_t rx_index;
static int target_write_requested(struct i2c_target_config *cfg)
{
rx_index = 0;
return 0;
}
static int target_write_received(struct i2c_target_config *cfg, uint8_t val)
{
if (rx_index < sizeof(rx_buffer)) {
rx_buffer[rx_index++] = val;
}
return 0;
}
static int target_read_requested(struct i2c_target_config *cfg, uint8_t *val)
{
*val = tx_buffer[0];
return 0;
}
static int target_read_processed(struct i2c_target_config *cfg, uint8_t *val)
{
*val = tx_buffer[1];
return 0;
}
static int target_stop(struct i2c_target_config *cfg)
{
return 0;
}
static struct i2c_target_callbacks target_callbacks = {
.write_requested = target_write_requested,
.write_received = target_write_received,
.read_requested = target_read_requested,
.read_processed = target_read_processed,
.stop = target_stop,
};
static struct i2c_target_config target_cfg = {
.address = 0x60,
.callbacks = &target_callbacks,
};
const struct device *i2c_bus = DEVICE_DT_GET(DT_NODELABEL(i2c0));
if (i2c_target_register(i2c_bus, &target_cfg) < 0) {
printk("Failed to register I2C target\n");
}
Using EEPROM Target Driver
Zephyr provides a virtual EEPROM target driver:
&i2c0 {
eeprom0: eeprom@52 {
compatible = "zephyr,i2c-target-eeprom";
reg = <0x52>;
size = <256>;
};
};
const struct device *eeprom = DEVICE_DT_GET(DT_NODELABEL(eeprom0));
if (i2c_target_driver_register(eeprom) < 0) {
printk("Failed to register EEPROM target\n");
}
Error Handling
Return Values
| Value | Meaning |
|---|
0 | Success |
-ENOTSUP | Operation not supported |
-EINVAL | Invalid parameter |
-EIO | I/O error (NACK, bus error) |
-EBUSY | Bus busy |
-ETIMEDOUT | Transfer timeout |
Robust Error Handling Pattern
int read_sensor_data(const struct i2c_dt_spec *dev, uint8_t reg, uint8_t *data, size_t len)
{
int ret;
int retries = 3;
while (retries--) {
ret = i2c_burst_read_dt(dev, reg, data, len);
if (ret == 0) {
return 0;
}
if (ret == -ETIMEDOUT || ret == -EIO) {
i2c_recover_bus(dev->bus);
k_msleep(10);
continue;
}
return ret;
}
return -EIO;
}
Bus Recovery
int ret = i2c_recover_bus(i2c_dev.bus);
if (ret < 0) {
printk("Bus recovery failed: %d\n", ret);
}
Troubleshooting
Quick Reference
| Issue | Likely Cause | Fix |
|---|
| No ACK / -EIO | Wrong address, device not powered | Verify address (7-bit), check power |
| Bus stuck | Device holding SDA low | Call i2c_recover_bus() |
| Timeout | Clock stretching too long | Increase timeout config |
| Garbled data | Speed too high | Reduce clock-frequency |
| Device not found | Wrong controller | Verify DT node hierarchy |
-ENOTSUP | Missing driver feature | Check Kconfig options |
Debug Checklist
- Device ready? Check
i2c_is_ready_dt(&spec) or device_is_ready(dev)
- Address correct? I2C uses 7-bit addresses (0x00-0x7F)
- Pins configured? Verify pinctrl in devicetree
- Pull-ups present? I2C requires pull-up resistors on SDA/SCL
- Speed valid? Start with
I2C_BITRATE_STANDARD (100kHz)
- Power sequence? Some devices need power-on delay
I2C Shell Commands
Enable with CONFIG_I2C_SHELL=y:
# Scan for devices on bus
i2c scan i2c@40003000
# Read register
i2c read_byte i2c@40003000 0x48 0x00
# Write register
i2c write_byte i2c@40003000 0x48 0x20 0x47
# Recover bus
i2c recover i2c@40003000
References
Source Locations
Key files in Zephyr source tree:
include/zephyr/drivers/i2c.h - Public API header
include/zephyr/drivers/smbus.h - SMBus API header
drivers/i2c/ - Driver implementations
dts/bindings/i2c/i2c-controller.yaml - Controller DTS binding
dts/bindings/i2c/i2c-device.yaml - Device DTS binding
samples/drivers/i2c/ - Sample applications