3. I2C核心数据结构与API:驱动开发的基石

说实话,I2C子系统在Linux内核里设计得非常精巧。你想想看,一个总线协议要支持成百上千种不同设备,还要让驱动开发者写得顺手,这背后全靠那几套核心数据结构撑着。

我刚开始接触I2C驱动时,对着i2c_adapter、i2c_client、i2c_driver这三个结构体看了整整两天。后来在项目里调了三个月的I2C传感器,才真正理解它们为什么这么设计。今天我就把这几块硬骨头给你拆开讲透。

3.1 三个核心结构体的角色定位

先给你一张总览图,把这三个结构体的关系理清楚:

I2C 核心数据结构关系图 i2c_adapter I2C 总线控制器 管理硬件时序与传输 i2c_client I2C 从设备 代表一个物理传感器 i2c_driver 设备驱动 实现读写与控制逻辑 adapter 提供总线能力,client 代表设备实体,driver 实现设备操作 三者通过 I2C 核心框架自动匹配与绑定 注册 adapter 注册 driver 匹配 client 匹配成功后,driver 的 probe 函数被调用,完成设备初始化

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 = &reg;

    // 第二条消息:读数据
    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 位写法坑了