4. Linux I2C 核心框架:数据结构与核心 API
Linux I2C 核心框架是连接 I2C 总线控制器硬件与上层设备驱动的中间层。它通过三个核心数据结构——i2c_adapter、i2c_algorithm 和 i2c_client——实现了硬件无关的抽象。理解这些结构及其 API 是编写正确、高效的 I2C 从设备驱动的基石。
核心概念: 在 Linux 内核中,
i2c_adapter 代表一个 I2C 总线控制器(主设备端),i2c_algorithm 是控制器的通信方法集,而 i2c_client 则代表一个挂载在该总线上的从设备。驱动开发者主要与 i2c_client 和 i2c_msg 打交道。
4.1 i2c_adapter:总线适配器
i2c_adapter 是 I2C 总线控制器的内核抽象。每个物理 I2C 控制器(如 SoC 中的 I2C 外设)在系统中对应一个 i2c_adapter 实例。它负责管理总线编号、关联的算法、设备链表以及电源管理。
| 成员 | 类型 | 描述 |
|---|---|---|
nr |
int |
总线编号,在系统中唯一,如 /dev/i2c-1 中的 1 |
name |
char[48] |
适配器名称,用于调试和用户空间识别 |
algo |
struct i2c_algorithm * |
指向该适配器使用的通信算法 |
algo_data |
void * |
算法私有数据,通常指向控制器硬件信息 |
dev |
struct device |
内核设备模型结构体 |
userspace_clients |
struct list_head |
从用户空间创建的客户端列表 |
bus_lock |
struct mutex |
总线访问互斥锁 |
timeout |
int |
通信超时时间(单位:jiffies) |
retries |
int |
通信失败时的重试次数 |
关键点: 适配器的
nr 字段决定了其在用户空间中的设备节点编号。在 Android 系统中,I2C 控制器通常从 0 开始编号,例如 /dev/i2c-0、/dev/i2c-1 等。BSP 工程师在设备树中通过 reg 属性指定适配器编号。
4.2 i2c_algorithm:通信算法
i2c_algorithm 定义了适配器与 I2C 从设备进行通信的具体方法。它是硬件控制器驱动需要实现的核心接口。该结构体包含两组关键函数指针:主传输函数和从模式函数(较少用)。
struct i2c_algorithm {
/* 主传输函数:发送 i2c_msg 数组 */
int (*master_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs,
int num);
/* 主接收函数(已废弃,使用 master_xfer) */
int (*master_recv)(struct i2c_adapter *adap,
struct i2c_msg *msg);
/* 从模式传输函数 */
int (*slave_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs,
int num);
/* 功能支持查询 */
u32 (*functionality)(struct i2c_adapter *adap);
};
| 函数指针 | 用途 | 实现要求 |
|---|---|---|
master_xfer |
核心传输入口,处理一次 I2C 事务(可能包含多个消息) | 必须实现;返回成功传输的消息数,负值表示错误 |
functionality |
返回适配器支持的功能掩码 | 必须实现;如 I2C_FUNC_I2C、I2C_FUNC_SMBUS_* |
slave_xfer |
从模式传输,适配器作为从设备时使用 | 可选,Android 中极少使用 |
注意事项:
master_xfer 是 I2C 核心框架中最关键的底层函数。它接收一个 i2c_msg 数组,每个消息包含从设备地址、读写标志、数据缓冲区和长度。控制器驱动必须按顺序处理这些消息,并正确处理 START、STOP 和重复起始条件(RESTART)。
4.3 i2c_client:从设备客户端
i2c_client 是 I2C 从设备在内核中的代表。每个物理 I2C 从芯片在驱动探测成功后都会生成一个 i2c_client 实例。驱动开发者通过该结构体与设备进行所有交互。
struct i2c_client {
unsigned short flags; /* 标志位,如 I2C_CLIENT_TEN */
unsigned short addr; /* 从设备地址(7位或10位) */
char name[I2C_NAME_SIZE]; /* 设备名称 */
struct i2c_adapter *adapter; /* 所属适配器 */
struct device dev; /* 内核设备模型 */
int irq; /* 中断号(若使用中断) */
struct list_head detected; /* 检测链表 */
int (*detect)(struct i2c_client *, struct i2c_board_info *);
};
| 成员 | 说明 | 驱动开发关注点 |
|---|---|---|
addr |
从设备地址,7 位模式范围为 0x03~0x77 | 必须与硬件原理图一致,设备树中通过 reg 属性指定 |
adapter |
指向设备所在总线的适配器 | 用于调用 i2c_transfer() 时传入 |
irq |
设备中断号,-1 表示不使用中断 | 在 probe() 中通过 client->irq 获取并注册中断处理函数 |
flags |
设备标志,如 10 位地址标志 | 通常为 0,特殊场景设置 I2C_CLIENT_TEN |
dev |
内核设备结构体,用于电源管理和 sysfs | 通过 &client->dev 传递给其他内核 API |
最佳实践: 在驱动
probe() 函数中,应始终检查 client->irq 是否大于 0 再注册中断。对于不支持中断的从设备,应使用轮询方式或通过其他 GPIO 中断线间接获取状态变化。
4.4 核心 API 函数
Linux I2C 核心框架提供了一套丰富的 API,用于设备注册、消息传输和系统级管理。以下是最常用的核心 API:
4.4.1 消息传输 API
/* 核心传输函数:发送一组 I2C 消息 */
int i2c_transfer(struct i2c_adapter *adap,
struct i2c_msg *msgs,
int num);
/* 便捷函数:发送单条消息 */
int i2c_master_send(const struct i2c_client *client,
const char *buf, int count);
int i2c_master_recv(const struct i2c_client *client,
char *buf, int count);
/* SMBus 兼容函数 */
s32 i2c_smbus_read_byte_data(const struct i2c_client *client,
u8 command);
s32 i2c_smbus_write_byte_data(const struct i2c_client *client,
u8 command, u8 value);
| API 函数 | 适用场景 | 返回值 |
|---|---|---|
i2c_transfer() |
需要组合消息(如先写寄存器地址再读数据) | 成功传输的消息数,负数为错误 |
i2c_master_send() |
向设备写入连续数据 | 成功写入的字节数,负数为错误 |
i2c_master_recv() |
从设备读取连续数据 | 成功读取的字节数,负数为错误 |
i2c_smbus_read_byte_data() |
读取指定寄存器的单字节值 | 读取到的值(正数),负数为错误 |
i2c_smbus_write_byte_data() |
向指定寄存器写入单字节值 | 0 成功,负数为错误 |
4.4.2 设备注册与注销 API
/* 驱动注册/注销 */
int i2c_add_driver(struct i2c_driver *driver);
void i2c_del_driver(struct i2c_driver *driver);
/* 设备节点注册(通常由设备树或板级代码调用) */
struct i2c_client *i2c_new_client_device(struct i2c_adapter *adap,
struct i2c_board_info const *info);
void i2c_unregister_device(struct i2c_client *client);
/* 设备检测与地址扫描 */
int i2c_register_board_info(int busnum,
struct i2c_board_info const *info,
unsigned len);
驱动开发流程: 在 Android 内核中,I2C 从设备驱动通常遵循以下步骤:
1. 定义
2. 在
3. 使用
4. 若设备支持中断,通过
5. 调用
1. 定义
i2c_driver 结构体,填充 probe、remove、id_table 等回调。2. 在
probe() 中通过 client->adapter 和 client->addr 确认设备存在。3. 使用
i2c_master_send/recv 或 i2c_transfer 进行数据读写。4. 若设备支持中断,通过
client->irq 注册中断处理函数。5. 调用
i2c_add_driver() 注册驱动。
4.5 i2c_msg:消息传递单元
i2c_msg 是 I2C 通信的最小数据单元。一次 I2C 事务可以包含多个 i2c_msg,核心框架会按顺序处理它们,并在消息之间自动插入重复起始条件(RESTART)。
struct i2c_msg {
__u16 addr; /* 从设备地址 */
__u16 flags; /* 标志位:I2C_M_RD 表示读,0 表示写 */
__u16 len; /* 消息数据长度 */
__u8 *buf; /* 数据缓冲区指针 */
};
| 标志位 | 值 | 含义 |
|---|---|---|
I2C_M_RD |
0x0001 | 读消息,数据从设备传输到主机 |
I2C_M_TEN |
0x0010 | 使用 10 位地址 |
I2C_M_NOSTART |
0x4000 | 不发送起始条件,用于组合消息中的连续片段 |
I2C_M_STOP |
0x8000 | 强制在消息后发送停止条件 |
典型用法: 读取 I2C 从设备寄存器的标准模式是组合两个消息:
消息 0:写操作,发送寄存器地址(flags = 0)。
消息 1:读操作,读取寄存器值(flags = I2C_M_RD)。
核心框架会在两个消息之间自动插入 RESTART 条件,无需驱动开发者手动处理。
消息 0:写操作,发送寄存器地址(flags = 0)。
消息 1:读操作,读取寄存器值(flags = I2C_M_RD)。
核心框架会在两个消息之间自动插入 RESTART 条件,无需驱动开发者手动处理。
/* 示例:读取从设备 0x48 的寄存器 0x05 */
struct i2c_msg msgs[2];
u8 reg_addr = 0x05;
u8 reg_value;
msgs[0].addr = 0x48;
msgs[0].flags = 0; /* 写 */
msgs[0].len = 1;
msgs[0].buf = ®_addr;
msgs[1].addr = 0x48;
msgs[1].flags = I2C_M_RD; /* 读 */
msgs[1].len = 1;
msgs[1].buf = ®_value;
ret = i2c_transfer(client->adapter, msgs, 2);
if (ret != 2) {
dev_err(&client->dev, "I2C read failed: %d\n", ret);
return -EIO;
}
/* reg_value 现在包含寄存器 0x05 的值 */
常见错误: 在组合消息中,所有消息的
addr 字段必须相同,因为 I2C 总线在 RESTART 后不会改变从设备选择。如果地址不同,必须拆分为独立的 i2c_transfer() 调用,每次调用之间总线会释放。
4.6 数据结构关系总览
以下表格总结了三个核心数据结构在 I2C 框架中的角色和关系:
| 数据结构 | 角色 | 创建者 | 使用者 | 生命周期 |
|---|---|---|---|---|
i2c_adapter |
总线控制器抽象 | SoC 平台驱动 | 核心框架、所有从设备驱动 | 系统启动时创建,关机时销毁 |
i2c_algorithm |
通信方法实现 | 控制器驱动 | i2c_adapter 内部使用 |
与适配器相同 |
i2c_client |
从设备实例 | 设备树/板级代码/用户空间 | 从设备驱动 | 设备探测时创建,移除时销毁 |
i2c_msg |
数据传输单元 | 驱动开发者 | i2c_transfer() |
栈上分配,传输完成后失效 |
学习建议: 在阅读或编写 I2C 从设备驱动时,始终牢记
i2c_client 是驱动与硬件之间的桥梁。通过 client->adapter 可以访问总线,通过 client->addr 确定设备地址,通过 client->irq 处理中断。这三个字段覆盖了 I2C 从设备驱动 90% 以上的交互需求。