3. Linux I2C 子系统架构:三层结构与关键数据结构

各位同学,今天我们来聊聊 Linux I2C 子系统的核心架构。说实话,我刚接触 I2C 驱动时,也被那三层结构绕得有点晕。但后来我明白了——搞懂这三层,你就掌握了 I2C 驱动的命脉。

Linux I2C 子系统设计得非常巧妙。它把整个框架拆成了三层:I2C 核心层、I2C 总线驱动、I2C 设备驱动。每一层各司其职,互不干扰。我当年在全志 T113 上调一个触摸屏驱动时,就是因为没搞清楚这三层的分工,折腾了两天才找到问题。嗯,咱们今天就把这事彻底说透。

3.1 三层架构总览

先看一张整体结构图,我习惯用图来理解复杂系统:

Linux I2C 子系统三层架构 用户空间应用程序 (APP) I2C 设备驱动 (i2c_driver) 实现具体外设的读写逻辑 (如 EEPROM、传感器、触摸屏) I2C 核心层 (I2C Core) 提供统一接口:i2c_add_adapter / i2c_add_driver / i2c_transfer I2C 总线驱动 (I2C Adapter / Algorithm) 操作硬件寄存器,产生 SCL/SDA 时序 (全志 T113 I2C 控制器) I2C 物理总线 (SCL / SDA) 分层职责 设备驱动: 业务逻辑 核心层: 通用管理 总线驱动: 硬件适配

这张图我画了好几次才满意。你从上往下看:应用层通过设备节点访问外设,设备驱动处理具体业务逻辑,核心层提供通用管理接口,总线驱动直接操作硬件寄存器。每一层只跟相邻层打交道,耦合度很低。

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 设备驱动加载与匹配流程 1. 注册 i2c_driver 2. 注册 i2c_adapter 3. 总线枚举 4. 创建 i2c_client 5. 匹配 id_table 6. 调用 probe() 7. 设备驱动开始工作 (读写外设) 匹配方式 • 设备树 compatible 匹配 • i2c_board_info 静态注册 • 用户空间 i2c-dev 动态创建

这个流程我解释一下:

  1. 注册 i2c_driver:驱动模块加载时,调用 i2c_add_driver() 注册。
  2. 注册 i2c_adapter:总线驱动初始化时,调用 i2c_add_adapter() 注册。
  3. 总线枚举:核心层扫描总线上的设备,根据设备树或静态信息创建 i2c_client。
  4. 匹配:核心层遍历已注册的 i2c_driver,比对 id_table 或设备树 compatible 属性。
  5. 调用 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