3. I2C核心数据结构与API:驱动开发的基石
说实话,I2C子系统在Linux内核里设计得非常精巧。你想想看,一个总线协议要支持成百上千种不同设备,还要让驱动开发者写得顺手,这背后全靠那几套核心数据结构撑着。
我刚开始接触I2C驱动时,对着i2c_adapter、i2c_client、i2c_driver这三个结构体看了整整两天。后来在项目里调了三个月的I2C传感器,才真正理解它们为什么这么设计。今天我就把这几块硬骨头给你拆开讲透。
3.1 三个核心结构体的角色定位
先给你一张总览图,把这三个结构体的关系理清楚:
3.2 i2c_adapter:总线控制器的抽象
i2c_adapter 说白了就是 I2C 总线控制器的软件代表。每个 I2C 控制器硬件(比如 SoC 里的 I2C 外设)在内核里都对应一个 adapter 实例。
它的核心定义在 include/linux/i2c.h 里,我挑几个关键字段给你看:
struct i2c_adapter {
struct module *owner; // 所属模块
unsigned int class; // 设备类,用于匹配
const struct i2c_algorithm *algo; // 算法,实现具体传输
void *algo_data; // 算法私有数据
struct device dev; // 内核设备模型
int nr; // adapter 编号
char name[48]; // 名称
struct list_head userspace_clients; // 用户空间 client 列表
struct mutex bus_lock; // 总线锁
};
这里最重要的就是 i2c_algorithm,它定义了 adapter 怎么干活:
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 是 adapter 的核心函数。你在驱动里调用的所有 I2C 读写操作,最终都会走到这里。我在移植一个老款加速度传感器时,发现 adapter 的 master_xfer 实现有 bug,导致连续读写时丢数据,查了整整两天才定位到。
3.3 i2c_client:从设备的软件化身
每个挂在 I2C 总线上的物理设备,在内核里都对应一个 i2c_client。它记录了设备的地址、所属 adapter、设备 ID 等信息。
struct i2c_client {
unsigned short flags; // 标志位
unsigned short addr; // 7位或10位从设备地址
char name[I2C_NAME_SIZE]; // 设备名称
struct i2c_adapter *adapter; // 所属 adapter
struct device dev; // 内核设备模型
int irq; // 中断号(如果有)
struct list_head detected; // 检测链表
};
这里有个坑我要提醒你:设备地址是 7 位还是 10 位,由 flags 里的 I2C_CLIENT_TEN 标志位决定。大部分传感器用 7 位地址,但有些特殊设备会用 10 位。
我曾经踩过的坑:在调试一个温湿度传感器时,数据手册上写的地址是 0x5C,我直接写进代码里,结果死活读不到数据。后来才发现,手册给的是 8 位地址(左移了一位),实际 7 位地址应该是 0x2E。记住:i2c_client 里的 addr 是 7 位原始地址,不是左移后的。
3.4 i2c_driver:驱动逻辑的载体
i2c_driver 就是我们写驱动时要实现的核心结构体。它定义了驱动怎么和设备匹配、怎么初始化、怎么卸载。
struct i2c_driver {
unsigned int class; // 匹配的设备类
int (*probe)(struct i2c_client *client,
const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
void (*shutdown)(struct i2c_client *client);
int (*suspend)(struct i2c_client *client, pm_message_t mesg);
int (*resume)(struct i2c_client *client);
const struct i2c_device_id *id_table; // 设备 ID 表
struct device_driver driver; // 内核驱动模型
};
probe 函数是驱动入口。当内核发现一个 client 和你的 driver 匹配时,就会调用 probe。你在 probe 里做设备初始化、注册字符设备、创建 sysfs 文件等操作。
我的习惯:在 probe 函数里,我通常会先读取设备的 ID 寄存器,确认硬件确实存在。这能避免设备地址冲突或硬件故障导致的诡异问题。有一次客户反馈说传感器数据全是 0xFF,我一查,是 probe 里没做 ID 校验,驱动绑到了错误的设备上。
3.5 核心 API 函数详解
理解了数据结构,我们来看看怎么用它们。I2C 核心提供了几组 API,我按使用频率给你列出来:
| API 函数 | 功能 | 使用场景 |
|---|---|---|
i2c_master_send() |
向设备发送数据 | 写寄存器、发送命令 |
i2c_master_recv() |
从设备接收数据 | 读取传感器数据 |
i2c_transfer() |
执行一组消息传输 | 复合操作(先写地址再读数据) |
i2c_smbus_read_byte_data() |
SMBus 读一个字节 | 读取单个寄存器 |
i2c_smbus_write_byte_data() |
SMBus 写一个字节 | 配置寄存器 |
i2c_smbus_read_word_data() |
SMBus 读两个字节 | 读取 16 位数据 |
i2c_smbus_read_i2c_block_data() |
SMBus 读一块数据 | 批量读取数据 |
实际项目中,我最常用的是 i2c_smbus_read_byte_data 和 i2c_smbus_write_byte_data。它们封装了地址发送和数据读写的完整流程,用起来特别方便。
来看一个实际例子,读取传感器 ID 寄存器:
static int sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct sensor_data *data;
int ret;
// 分配私有数据结构
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 读取设备 ID 寄存器(假设地址为 0x0F)
ret = i2c_smbus_read_byte_data(client, 0x0F);
if (ret < 0) {
dev_err(&client->dev, "failed to read device ID\n");
return ret;
}
// 校验 ID
if (ret != SENSOR_EXPECTED_ID) {
dev_err(&client->dev, "unexpected device ID: 0x%02x\n", ret);
return -ENODEV;
}
dev_info(&client->dev, "sensor detected, ID = 0x%02x\n", ret);
// 保存 client 指针
data->client = client;
i2c_set_clientdata(client, data);
// 其他初始化...
return 0;
}
注意:i2c_smbus_read_byte_data 返回的是 int 类型,负数表示错误。很多新手直接把它当 u8 用,导致错误码被截断。我见过一个案例,就是因为没检查返回值,驱动一直用 0xFF 的数据往下跑,系统都跑飞了。
3.6 驱动注册与注销流程
写好了 driver 结构体,怎么把它注册进内核?看这个标准流程:
static const struct i2c_device_id sensor_id[] = {
{ "my_sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, sensor_id);
static struct i2c_driver sensor_driver = {
.driver = {
.name = "my_sensor",
.of_match_table = of_match_ptr(sensor_of_match),
},
.probe = sensor_probe,
.remove = sensor_remove,
.id_table = sensor_id,
};
module_i2c_driver(sensor_driver);
module_i2c_driver 是一个宏,它展开后就是标准的模块加载/卸载函数。你不需要自己写 module_init 和 module_exit,这个宏全帮你搞定了。
我的建议:如果你的设备支持设备树,一定要加上 of_match_table。这样内核可以通过设备树节点自动匹配你的驱动,省去手动注册 client 的麻烦。我在项目里一直用设备树方式,从来没手动注册过 client。
3.7 消息传输:i2c_msg 与 i2c_transfer
有些传感器需要先写寄存器地址,再读数据。这种复合操作要用 i2c_msg 和 i2c_transfer:
static int sensor_read_reg(struct i2c_client *client,
u8 reg, u8 *value)
{
struct i2c_msg msg[2];
int ret;
// 第一条消息:写寄存器地址
msg[0].addr = client->addr;
msg[0].flags = 0; // 写操作
msg[0].len = 1;
msg[0].buf = ®
// 第二条消息:读数据
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读操作
msg[1].len = 1;
msg[1].buf = value;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret != 2) {
dev_err(&client->dev, "i2c_transfer failed: %d\n", ret);
return ret < 0 ? ret : -EIO;
}
return 0;
}
这里有个细节:两条消息之间,I2C 总线会发送一个重复起始条件(Repeated Start),而不是先发停止再发起始。这是 I2C 协议的标准行为,能保证总线操作的原子性。
我曾经遇到的坑:有些 adapter 硬件不支持 Repeated Start,遇到两条消息的传输会直接返回错误。这种情况下,你得用 SMBus 的 i2c_smbus_read_byte_data 函数,它内部会处理兼容性问题。所以,如果你的驱动要支持多种硬件平台,优先用 SMBus API。
3.8 本章小结
这三个结构体和配套的 API,就是 I2C 驱动开发的全部家当。你只要记住:
- adapter 管总线,client 管设备,driver 管逻辑
- probe 函数里做初始化,remove 函数里做清理
- 简单读写用 SMBus API,复合操作用 i2c_transfer
- 设备地址是 7 位,别被数据手册的 8 位写法坑了