3. Linux I2C 子系统架构:三层结构与关键数据结构
各位同学,今天我们来聊聊 Linux I2C 子系统的核心架构。说实话,我刚接触 I2C 驱动时,也被那三层结构绕得有点晕。但后来我明白了——搞懂这三层,你就掌握了 I2C 驱动的命脉。
Linux I2C 子系统设计得非常巧妙。它把整个框架拆成了三层:I2C 核心层、I2C 总线驱动、I2C 设备驱动。每一层各司其职,互不干扰。我当年在全志 T113 上调一个触摸屏驱动时,就是因为没搞清楚这三层的分工,折腾了两天才找到问题。嗯,咱们今天就把这事彻底说透。
3.1 三层架构总览
先看一张整体结构图,我习惯用图来理解复杂系统:
这张图我画了好几次才满意。你从上往下看:应用层通过设备节点访问外设,设备驱动处理具体业务逻辑,核心层提供通用管理接口,总线驱动直接操作硬件寄存器。每一层只跟相邻层打交道,耦合度很低。
3.2 三个关键数据结构
搞懂这三层,你只需要记住三个数据结构:i2c_adapter、i2c_client、i2c_driver。说白了,它们就是这三层的身份证。
3.2.1 i2c_adapter —— 总线驱动层的身份证
i2c_adapter 代表一个 I2C 控制器。在全志 T113 上,每个 I2C 控制器(比如 I2C0、I2C1)都对应一个 i2c_adapter 实例。它的核心任务是提供 master_xfer 函数,这个函数负责在物理总线上产生实际的 SCL/SDA 时序。
struct i2c_adapter {
struct module *owner;
unsigned int class; // 适配器支持的设备类
const struct i2c_algorithm *algo; // 算法接口,核心!
struct device dev; // 内核设备模型
int nr; // 适配器编号
char name[48]; // 适配器名称
struct list_head userspace_clients;
// ... 其他字段
};
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);
};
关键点: master_xfer 是总线驱动的灵魂。全志 T113 的 BSP 代码里,这个函数会操作 I2C 控制器的寄存器(如 TWI_CNTR、TWI_DATA、TWI_CR),完成起始信号、发送地址、读写数据、停止信号等时序。我在调试 T113 的 I2C 时钟频率时,就是在这个函数里加打印,发现分频系数算错了。
3.2.2 i2c_client —— 设备驱动的身份证
i2c_client 代表一个挂在 I2C 总线上的具体设备。每个外设芯片(比如 AT24C02 EEPROM、MPU6050 传感器)在驱动中就是一个 i2c_client。它记录了设备的地址、适配器归属等信息。
struct i2c_client {
unsigned short addr; // 7位或10位从机地址
char name[I2C_NAME_SIZE]; // 设备名称
struct i2c_adapter *adapter; // 所属适配器
struct device dev; // 内核设备模型
int irq; // 中断号(如果有)
struct list_head detected; // 用于设备检测链表
};
避坑指南: 我曾经在 T113 上挂载一个 I2C 温度传感器,死活读不到数据。后来发现是 addr 填错了——数据手册上写的是 8 位地址,而内核用的是 7 位地址。记住:i2c_client 的 addr 是 7 位地址,左移一位才是 8 位地址。
3.2.3 i2c_driver —— 设备驱动层的身份证
i2c_driver 是设备驱动的核心结构体。它定义了驱动支持的设备列表、探测函数、移除函数等。当内核发现一个 i2c_client 与 i2c_driver 的 id_table 匹配时,就会调用 probe 函数。
struct i2c_driver {
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
void (*shutdown)(struct i2c_client *client);
struct device_driver driver;
const struct i2c_device_id *id_table; // 支持的设备列表
// ... 其他字段
};
// 设备 ID 表示例
static const struct i2c_device_id myeeprom_id[] = {
{ "at24c02", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, myeeprom_id);
3.3 三层之间的协作流程
这三层是怎么配合工作的?我画个流程图你就明白了:
这个流程我解释一下:
- 注册 i2c_driver:驱动模块加载时,调用
i2c_add_driver()注册。 - 注册 i2c_adapter:总线驱动初始化时,调用
i2c_add_adapter()注册。 - 总线枚举:核心层扫描总线上的设备,根据设备树或静态信息创建
i2c_client。 - 匹配:核心层遍历已注册的
i2c_driver,比对id_table或设备树compatible属性。 - 调用 probe:匹配成功,调用驱动的
probe函数,驱动开始初始化外设。
注意: 在全志 T113 上,I2C 设备通常通过设备树描述。你需要在设备树中指定 compatible 属性,驱动才能正确匹配。我曾经见过有人把 compatible 写成了 compatiable,结果驱动死活不 probe——内核不会告诉你拼写错了,只会静默忽略。
3.4 三层结构对比总结
最后,我用一张表帮你理清这三层的区别:
| 层次 | 核心结构体 | 主要职责 | 全志 T113 示例 |
|---|---|---|---|
| I2C 总线驱动 | i2c_adapter |
操作硬件寄存器,产生 I2C 时序 | drivers/i2c/busses/i2c-sunxi.c |
| I2C 核心层 | 无特定结构体(框架代码) | 管理适配器、驱动注册/注销、提供通用接口 | drivers/i2c/i2c-core.c |
| I2C 设备驱动 | i2c_driver + i2c_client |
实现具体外设的读写逻辑 | drivers/misc/eeprom/at24.c |