I2C子系统:从协议时序到设备驱动开发的完整实战
I2C是嵌入式里用得最多的总线之一,传感器、EEPROM、PMIC全挂在上面,但写I2C驱动不只是调几个API的事——协议时序没搞清楚,上拉电阻选不对,总线电容算错,出来的就是一堆NACK和超时。本文基于Linux 6.8内核,从I2C协议基础出发,讲清子系统三层架构、设备树配置、设备驱动开发、数据传输API、控制器驱动原理,最后给调试技巧和常见问题排查看
一、I2C协议基础
两线制主从模块
I2C只用两根线:SDA(数据)和SCL(时钟)。所有设备共享这两根线,通过地址区分从机,主机负责产生时钟和发起传输,从机被动响应。
| 信号线 | 方向 | 作用 |
|:-------|:-----|:-----|
| SDA | 双向 | 数据收发,开漏输出 |
| SCL | 主→从 | 时钟信号,主机驱动 |
两根线都是开漏输出,必须外接上拉电阻(典型4.7kΩ),空闲时均为高电平。
7/10位地址
标准I2C地址7位,有效范围0x03~0x77,0x00~0x02、0x78~0x7F保留)。总线上最多挂127个从机。10位地址扩展(1024个),但实际用得少,大部分传感器和EEPROM都是7位地址。
注意:设备手册上写的地址有时是8位格式(7位地址左移1位+ R/W位),比如BMP280手册0xEC/0xED,实际7位地址0x76。写设备树reg字段时用7位地址。
传输时序
一次完整的I2C传输:
START → 从机地址(7bit) + R/W(1bit) → ACK → 数据(8bit) → ACK → ... → STOP
关键信号:
| 信号 | 产生方 | 说明 |
|:-----|:-------|:-----|
| START | 主机 | SCL高电平期间,SDA由高拉低 |
| STOP | 主机 | SCL高电平期间,SDA由低拉高 |
| ACK | 接收方 | 1个时钟周期,SDA拉低表示应答 |
| NACK | 接收方 | 1个时钟周期,SDA保持高表示无应答 |
数据在SCL高电平期间必须稳定,SDA只在SCL低电平期间才能变化(START和STOP除外)。
四种传输速率
| 模式 | 速率 | 上拉电阻 | 典型应用 |
|---|---|---|---|
| 标准模式 | 100kbps | 4.7kΩ~10kΩ | 低速传感器、RTC |
| 快速模式 | 400kbps | 2.2kΩ~4.7kΩ | 大部分传感器、EEPROM |
| 高速模式 | 3.4Mbps | <1kΩ | 高速ADC、摄像头 |
| 超快模式 | 5Mbps | 单向传输 | 极少数场景 |
速率越高,上拉电阻要越小,但功耗也越大,实际项目中400kbps覆盖90%的场景:
二、I2C子系统架构
Linux I2C子系统分三层,各层职责明确,驱动开发只需要关注最上层。
各层职责
I2C设备驱动*(我们写的部分)。
- 实现i2c_driver的probe和remove
- 通过i2c_transfer、i2c_smbus_*等API与从机通信
- 通过of_device_id匹配设备树中的设备节。
I2C核心(内核提供)。
- 管理i2c_driver和i2c_client的注册与匹配
- 向上提供统一的i2c_transfer等数据传输API
- 向下调用控制器驱动的master_xfer
- SMBus协议模拟(控制器不支持SMBus时由软件模拟。
I2C控制器驱动层*(SoC厂商提供):
- 操作控制器硬件寄存器,产生START/STOP/ACK等信息- 实现
i2c_algorithm的master_xfer回调 - 处理中断、DMA、时钟拉伸等底层细节
驱动开发者绝大多数时候只写设备驱动层,控制器驱动由SoC厂商(NXP、Rockchip、Allwinner等)已经写好。
三、设备树配置
I2C设备要在设备树里声明,内核才能帮你创建i2c_client并匹配驱动。
I2C控制器节。
SoC的设备树通常已经定义好了I2C控制器节点:
/* SoC级设备树(厂商提供) */
i2c1: i2c@fe5a0000 {
compatible = "rockchip,rk3568-i2c";
reg = <0x0 0xfe5a0000 0x0 0x1000>;
clocks = <&cru CLK_I2C1>;
interrupts = <GIC_SPI 48 IRQ_TYPE_LEVEL_HIGH>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
};
I2C设备子节。
在板级设备树中使能控制器并添加从设备。
/* 板级设备*/
&i2c1 {
status = "okay";
clock-frequency = <400000>; /* 400kbps快速模块*/
/* BMP280气压传感器,7位地址0x76 */
bmp280@76 {
compatible = "bosch,bmp280";
reg = <0x76>;
};
/* AT24C02 EEPROM7位地址0x50 */
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
};
};
关键字段说明。
| 字段 | 说明 |
|:-----|:-----|
| compatible | 驱动匹配字符串,必须与驱动中of_device_id一致 |
| reg | 从机7位地址(不是8位),如0x76而非0xEC |
| clock-frequency | 总线时钟频率,不写则默认100kHz |
完整DTS示例
/* i2c-sensor-board.dtsi - IMU传感。+ EEPROM */
&i2c1 {
status = "okay";
clock-frequency = <400000>;
/* MPU6050六轴IMU,AD0接地时地址0x68 */
imu@68 {
compatible = "invensense,mpu6050";
reg = <0x68>;
interrupt-parent = <&gpio3>;
interrupts = <21 IRQ_TYPE_EDGE_FALLING>;
mount-matrix = "1", "0", "0",
"0", "1", "0",
"0", "0", "1";
};
/* AT24C08 EEPROM,A0/A1/A2接地时地址0x50 */
eeprom@50 {
compatible = "atmel,24c08";
reg = <0x50>;
pagesize = <16>;
read-only;
};
};
mount-matrix是IMU传感器的安装矩阵,描述芯片坐标系与设备坐标系的映射关系,驱动读取后用于数据校正。
四、I2C设备驱动开销
i2c_driver结构建
#include <linux/i2c.h>
#include <linux/module.h>
/* 设备私有数据 */
struct bmp280_data {
struct i2c_client *client;
struct mutex lock;
int calib_t1, calib_t2, calib_t3;
};
static int bmp280_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct bmp280_data *data;
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_BYTE_DATA))
return -EIO;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
mutex_init(&data->lock);
i2c_set_clientdata(client, data);
/* 读取芯片ID验证通信 */
int chip_id = i2c_smbus_read_byte_data(client, 0xD0);
if (chip_id != 0x58) {
dev_err(&client->dev, "wrong chip id: 0x%02x\n", chip_id);
return -ENODEV;
}
dev_info(&client->dev, "BMP280 detected, id=0x%02x\n", chip_id);
return 0;
}
static void bmp280_remove(struct i2c_client *client)
{
/* devm资源自动释放,无需手动清理 */
dev_info(&client->dev, "BMP280 removed\n");
}
ID表与设备树匹。
I2C驱动有两种匹配方式,都要写:
/* 方式1:i2c_device_id - 传统非设备树匹配 */
static const struct i2c_device_id bmp280_id[] = {
{ "bmp280", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, bmp280_id);
/* 方式2:of_device_id - 设备树匹配(优先*/
static const struct of_device_id bmp280_of_match[] = {
{ .compatible = "bosch,bmp280" },
{ }
};
MODULE_DEVICE_TABLE(of, bmp280_of_match);
MODULE_DEVICE_TABLE宏把匹配表导出到模块信息,modprobe根据它自动加载对应驱动。
完整驱动示例:BMP280气压传感。
// bmp280.c - BMP280气压传感器驱动(Linux 6.8。#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/delay.h>
#include <linux/mutex.h>
#include <linux/regmap.h>
#define BMP280_REG_ID 0xD0
#define BMP280_REG_RESET 0xE0
#define BMP280_REG_CTRL_MEAS 0xF4
#define BMP280_REG_CONFIG 0xF5
#define BMP280_REG_TEMP_MSB 0xFA
#define BMP280_ID_VAL 0x58
#define BMP280_RESET_CMD 0xB6
/* 过采样配置*/
#define BMP280_OSRS_T_X1 (1 << 5) /* 温度x1过采*/
#define BMP280_OSRS_P_X1 (1 << 2) /* 气压x1过采*/
#define BMP280_MODE_NORMAL (3 << 0) /* 正常工作模式 */
struct bmp280_data {
struct i2c_client *client;
struct mutex lock;
/* 校准参数(简化,实际有更多) */
u16 dig_t1;
s16 dig_t2, dig_t3;
};
static int bmp280_read_calibration(struct bmp280_data *data)
{
struct i2c_client *client = data->client;
int ret;
/* 读取温度校准参数 T1/T2/T3 */
ret = i2c_smbus_read_word_swapped(client, 0x88);
if (ret < 0)
return ret;
data->dig_t1 = ret;
ret = i2c_smbus_read_word_swapped(client, 0x8A);
if (ret < 0)
return ret;
data->dig_t2 = ret;
ret = i2c_smbus_read_word_swapped(client, 0x8C);
if (ret < 0)
return ret;
data->dig_t3 = ret;
return 0;
}
static s32 bmp280_compensate_temp(struct bmp280_data *data, s32 adc_temp)
{
s64 var1, var2;
var1 = ((s64)(adc_temp >> 3) - ((s64)data->dig_t1 << 1))
* ((s64)data->dig_t2 >> 11);
var2 = (((s64)(adc_temp >> 4) - (s64)data->dig_t1)
* ((s64)(adc_temp >> 4) - (s64)data->dig_t1) >> 12)
* ((s64)data->dig_t3 >> 14);
return (var1 + var2) * 5 + 128 >> 8; /* 单位0.01°C */
}
static int bmp280_read_raw(struct bmp280_data *data, int *temp_centi)
{
struct i2c_client *client = data->client;
u8 buf[3];
s32 adc_temp;
int ret;
mutex_lock(&data->lock);
/* 读取3字节温度数据 */
ret = i2c_smbus_read_i2c_block_data(client, BMP280_REG_TEMP_MSB, 3, buf);
if (ret < 0) {
mutex_unlock(&data->lock);
return ret;
}
adc_temp = (buf[0] << 12) | (buf[1] << 4) | (buf[2] >> 4);
*temp_centi = bmp280_compensate_temp(data, adc_temp);
mutex_unlock(&data->lock);
return 0;
}
static int bmp280_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct bmp280_data *data;
int chip_id, ret, temp;
/* 检查控制器是否支持所需功能 */
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_BYTE_DATA |
I2C_FUNC_SMBUS_READ_I2C_BLOCK))
return -EIO;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
mutex_init(&data->lock);
i2c_set_clientdata(client, data);
/* 读取芯片ID */
chip_id = i2c_smbus_read_byte_data(client, BMP280_REG_ID);
if (chip_id != BMP280_ID_VAL) {
dev_err(&client->dev, "chip id mismatch: 0x%02x\n", chip_id);
return -ENODEV;
}
/* 软复*/
ret = i2c_smbus_write_byte_data(client, BMP280_REG_RESET, BMP280_RESET_CMD);
if (ret < 0)
return ret;
msleep(10);
/* 读取校准参数 */
ret = bmp280_read_calibration(data);
if (ret < 0) {
dev_err(&client->dev, "failed to read calibration: %d\n", ret);
return ret;
}
/* 配置:温度x1过采。+ 气压x1过采。+ 正常模式 */
ret = i2c_smbus_write_byte_data(client, BMP280_REG_CTRL_MEAS,
BMP280_OSRS_T_X1 | BMP280_OSRS_P_X1 |
BMP280_MODE_NORMAL);
if (ret < 0)
return ret;
/* 读取一次温度验*/
ret = bmp280_read_raw(data, &temp);
if (ret < 0) {
dev_err(&client->dev, "failed to read temperature: %d\n", ret);
return ret;
}
dev_info(&client->dev, "BMP280: temp=%d.%02d C\n",
temp / 100, temp % 100);
return 0;
}
static void bmp280_remove(struct i2c_client *client)
{
dev_info(&client->dev, "BMP280 removed\n");
}
static const struct i2c_device_id bmp280_id[] = {
{ "bmp280", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, bmp280_id);
static const struct of_device_id bmp280_of_match[] = {
{ .compatible = "bosch,bmp280" },
{ }
};
MODULE_DEVICE_TABLE(of, bmp280_of_match);
static struct i2c_driver bmp280_driver = {
.driver = {
.name = "bmp280",
.of_match_table = bmp280_of_match,
},
.probe = bmp280_probe,
.remove = bmp280_remove,
.id_table = bmp280_id,
};
module_i2c_driver(bmp280_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("BMP280 pressure sensor driver");
module_i2c_driver宏展开后自动注册/注销驱动,不用手写module_init和module_exit。
五、I2C数据传输API
i2c_transfer:通用传输
最灵活的传输方式,支持组合消息(一次START条件发多段数据)。
/* 写寄存器:先发寄存器地址,再发数*/
static int sensor_write_reg(struct i2c_client *client, u8 reg, u8 val)
{
struct i2c_msg msg;
u8 buf[2] = { reg, val };
msg.addr = client->addr;
msg.flags = 0; /* 写操*/
msg.len = 2;
msg.buf = buf;
return i2c_transfer(client->adapter, &msg, 1);
}
/* 读寄存器:先写寄存器地址,再读数据(restart)/
static int sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
struct i2c_msg msgs[2];
u8 buf;
/* 第一段:写寄存器地址 */
msgs[0].addr = client->addr;
msgs[0].flags = 0; /* */
msgs[0].len = 1;
msgs[0].buf = ®
/* 第二段:读数*/
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD; /* */
msgs[1].len = 1;
msgs[1].buf = &buf;
int ret = i2c_transfer(client->adapter, msgs, 2);
if (ret < 0)
return ret;
*val = buf;
return 0;
}
i2c_transfer返回成功传输的消息数,出错返回负值。注意:返回值不直接表示成功,而是等于消息数才表示全部成功
i2c_master_send / i2c_master_recv
单次发送或接收的快捷函数:
/* 发送数*/
int ret = i2c_master_send(client, buf, len);
/* 接收数据 */
int ret = i2c_master_recv(client, buf, len);
内部就是封装了一个i2c_msg然后调i2c_transfer。适合简单的整块读写,不适合"先写地址再读数据"这种组合操作集
i2c_msg结构建
struct i2c_msg {
__u16 addr; /* 从机7位地址 */
__u16 flags; /* 传输标志 */
__u16 len; /* 数据长度 */
__u8 *buf; /* 数据缓冲*/
};
常用flags。
| 标志 | | 说明 |
|:-----|:---|:-----|
| 0 | 0 | 写操作(默认) |
| I2C_M_RD | 0x0001 | 读操作 |
| I2C_M_TEN | 0x0010 | 10位地址 |
| I2C_M_STOP | 0x8000 | 强制在消息后发STOP |
SMBus操作
SMBus是I2C的子集协议,定义了更严格的时序和标准化的读写协议。Linux内核提供了SMBus封装API,大多数传感器都支持。
/* 读单字节寄存*/
int val = i2c_smbus_read_byte_data(client, reg);
/* 写单字节寄存*/
int ret = i2c_smbus_write_byte_data(client, reg, val);
/* 。6位字(小端序,字节交换) */
int val = i2c_smbus_read_word_swapped(client, reg);
/* 。6位字 */
int ret = i2c_smbus_write_word_swapped(client, reg, val);
/* 读多字节*/
int ret = i2c_smbus_read_i2c_block_data(client, reg, len, buf);
/* 写多字节*/
int ret = i2c_smbus_write_i2c_block_data(client, reg, len, buf);
/* 读单字节(无寄存器地址*/
int val = i2c_smbus_read_byte(client);
/* 写单字节(无寄存器地址*/
int ret = i2c_smbus_write_byte(client, val);
SMBus API返回值:成功返回读取的数据(read)或0(write),失败返回负值
使用前用i2c_check_functionality检查控制器支持情况。
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA))
return -EIO;
传输方式对比
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
i2c_transfer |
组合读写、复杂时序 | 灵活,支持restart | 代码稍多 |
i2c_master_send/recv |
整块读写 | 简单 | 不支持组合操作 |
i2c_smbus_read_byte_data |
单字节寄存器读写 | 最简单 | 只能一字节 |
i2c_smbus_read_i2c_block_data |
多字节寄存器读写 | 简洁,支持块传递 | 控制器必须支持 |
i2c_smbus_read_word_swapped |
16位寄存器 | 自动字节交换 | 只能一字节 |
实际开发中优先用SMBus API,不够灵活时再退回i2c_transfer。
六、I2C控制器驱动
i2c_adapter + i2c_algorithm
控制器驱动注册一个i2c_adapter,描述一个I2C控制器实例,并绑定i2c_algorithm描述操作方法。
struct i2c_adapter {
struct module *owner;
unsigned int class;
const struct i2c_algorithm *algo; /* 操作方法 */
void *algo_data;
struct device dev; /* 内嵌device */
int nr; /* 总线编号 */
char name[48];
/* ... */
};
struct i2c_algorithm {
int (*master_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
int (*smbus_xfer)(struct i2c_adapter *adap,
u16 addr, unsigned short flags,
char read_write, u8 command,
int size, union i2c_smbus_data *data);
u32 (*functionality)(struct i2c_adapter *adap);
};
master_xfer是核心回调,接收一组i2c_msg,操作硬件完成传输。functionality返回控制器支持的功能掩码。
何时需要写控制器驱动
*绝大多数情况不需要 SoC厂商已经在主线内核或BSP里提供了控制器驱动:
| SoC | 控制器驱动文件 | compatible |
|---|---|---|
| RK3568 | drivers/i2c/busses/i2c-rk3x.c |
rockchip,rk3568-i2c |
| i.MX6 | drivers/i2c/busses/i2c-imx.c |
fsl,imx6q-i2c |
| STM32MP | drivers/i2c/busses/i2c-stm32f7.c |
st,stm32mp15-i2c |
| Allwinner | drivers/i2c/busses/i2c-sun6i-p2wi.c |
allwinner,sun6i-a31-p2wi |
需要自己写控制器驱动的场景:- 自研FPGA实现I2C控制。- 使用冷门SoC,主线内核没有支。- 需要优化控制器性能(如DMA传输。
七、调试技。
i2c-tools
最常用的I2C调试工具集,Ubuntu直接apt install i2c-tools。
# 列出系统所有I2C总线
i2cdetect -l
# 扫描总线0上的从机设备
i2cdetect -y 0
# 读取0x76设备0xD0寄存器(BMP280芯片ID)
i2cget -y 0 0x76 0xD0
# 写入0x50设备0x00寄存器(EEPROM)
i2cset -y 0 0x50 0x00 0xAA
# dump整个0x50设备的寄存器
i2cdump -y 0 0x50
i2cdetect扫描结果中--表示地址无应答,UU表示该地址已被驱动占用,数字表示检测到设备。
i2c-stub:虚拟I2C设备
不需要真实硬件就能测试I2C驱动。
# 加载i2c-stub模块,模拟地址0x76的设备
sudo modprobe i2c-stub chip_addr=0x76
# 查看虚拟总线编号
i2cdetect -l
# i2c-3 i2c Stub device ...
# 在虚拟设备上读写测试
i2cset -y 3 0x76 0xD0 0x58 # 模拟BMP280的ID寄存器
i2cget -y 3 0x76 0xD0 # 读回验证
开发驱动时先用stub验证逻辑,再上真机调试,效率高很多。
逻辑分析仪抓。
软件调试搞不定时,上逻辑分析仪看实际波形。
1. Saleae Logic / sigrok:抓取SDA/SCL信号,自动解码I2C协议
2. *关注。 - START/STOP是否正确产生
- 从机地址是否匹配(注意7位vs8位)
- ACK/NACK位置——NACK说明从机没响。 - 时钟频率是否在从机支持范围内
- 信号上升沿是否太缓(上拉电阻过大或总线电容过大。
八、常见问题
Q1:probe不被调用。
驱动加载了但probe没执行,最常见的原因是匹配失败。
1. 设备树compatible与驱动of_device_id不一致——逐字对比,大小写、逗号都要一致。2. 设备树reg地址与驱动id_table不匹配——检查7位地址是否正确
3. 控制器节点status = "disabled"——板级DTS里必须status = "okay"
4. I2C控制器驱动未加载——先确认i2cdetect -l能看到总线
排查步骤。
# 查看已注册的I2C设备
ls /sys/bus/i2c/devices/
# 查看驱动匹配置cat /sys/bus/i2c/drivers/bmp280/modalias
# 查看设备树节点是否生成
ls /proc/device-tree/i2c@fe5a0000/
Q2:i2c_transfer返回-ENXIO。
-ENXIO(No such device or address)表示从机无应答(NACK):
- 从机地址错误——对照数据手册确认7位地址,注意AD0/AD1等引脚配置2. 硬件连接问题——SDA/SCL是否接反、上拉电阻是否焊接、供电是否正常。3. **从机未就绪——某些传感器上电后需要几毫秒初始化时间,立即访问会NACK
- *总线被占——其他主机或从机拉低了SDA/SCL
```bash
用i2cdetect确认设备是否在线
i2cdetect -y 1
如果0x76位置显示"--",说明总线上确实没有这个地址的设```
Q3:I2C通信偶尔失败。
间歇性失败是最头疼的,常见原因。
| 原因 | 现象 | 解决 |
|:-----|:-----|:-----|
| 上拉电阻过大 | 上升沿缓慢,高速时采样错误 | 减小上拉电阻。>400kbps用1.2kΩ~4.7kΩ|
| 总线电容过大 | 信号畸变,长走线/多设备时明显 | 缩短走线,减少挂载设备数 |
| 时钟拉伸 | 从机拉低SCL请求等待,主机超时 | 增加超时时间,或换用支持时钟拉伸的控制器 |
| 中断延迟 | 实时性要求高的场景,中断响应 | 使用RT内核或线程化中断 |
| 电源噪声 | 传感器供电不稳导致通信异常 | 加去耦电容,检查LDO输出 |
用逻辑分析仪抓波形,看上升沿时间和信号完整性,基本能定位问题。
九、总结
I2C驱动开发速查看
| 环节 | 关键字 |
|---|---|
| 协议 | 两线开销上拉7位地址,START→地址+R/W→ACK→数据→STOP |
| 子系统架 | 设备驱动→核心层→控制器驱动,三层解 |
| 设备 | compatible匹配驱动,reg7位地址,clock-frequency设速率 |
| 驱动注册 | i2c_driver+of_device_id+i2c_device_id,module_i2c_driver |
| 数据传输 | 优先SMBus API,复杂时序用i2c_transfer |
| 控制器驱动 | SoC厂商提供,一般不需要自己写 |
| 调试 | i2c-tools扫描读写,i2c-stub虚拟设备,逻辑分析仪看波形 |
| 常见故障 | probe不调→匹配失败,ENXIO→地址/硬件错,间歇失败→上拉电容/时钟拉伸 |
本文首发于linuxros.cn,转载请注明出处。