4. I2C驱动框架(Linux内核)

好,咱们今天来聊聊Linux内核里的I2C驱动框架。说实话,刚接触内核驱动的时候,我也被那一堆结构体搞得晕头转向。但后来我发现,只要抓住三个核心角色——适配器、设备、驱动——整个框架就清晰了。

你可以把I2C子系统想象成一个总线管理系统。适配器就是总线控制器,负责物理信号的收发;设备就是挂在总线上的那些传感器、EEPROM;驱动就是操作设备的软件逻辑。这三者通过内核的注册机制绑定在一起。

核心思想:Linux I2C子系统将硬件操作抽象为三层——i2c_adapter(适配器层)、i2c_client(设备层)、i2c_driver(驱动层)。驱动开发者主要关注后两者。

4.1 子系统架构概览

先看一张整体架构图。我习惯用这种图来理解代码的层次关系。

Linux I2C 子系统架构 用户空间 (User Space) 内核空间 I2C 设备驱动层 (i2c_driver) I2C 核心层 (i2c-core) I2C 适配器层 (i2c_adapter) 硬件层 (I2C Controller / GPIO模拟) 说明: 驱动开发者主要编写 i2c_driver 和 i2c_client 的匹配逻辑,适配器层通常由 SoC 厂商提供。

从这张图你能看到,数据流是从用户空间往下,经过驱动层、核心层、适配器层,最终到达硬件。反过来,硬件中断或数据到达时,也是沿着这条路往上走。

4.2 核心数据结构详解

这三个结构体是I2C驱动的基石。我当年第一次看源码时,花了整整一个下午才理清它们的关系。咱们一个一个来拆解。

4.2.1 i2c_adapter —— 总线控制器

i2c_adapter 代表一个I2C总线控制器。它负责物理层的时序生成、起始/停止信号、ACK/NACK检测等。说白了,它就是那个在硬件上“拉高拉低SDA/SCL”的家伙。

struct i2c_adapter {
    struct module *owner;
    unsigned int class;               // 适配器支持的设备类型
    const struct i2c_algorithm *algo;  // 算法:收发消息的具体实现
    void *algo_data;
    struct device dev;                 // 内核设备模型
    int nr;                            // 总线编号
    struct list_head userspace_clients;
    // ... 其他字段
};

关键字段是 algo,它指向 i2c_algorithm 结构体,里面定义了 master_xfer() 函数指针。这个函数就是实际发送和接收I2C数据的地方。

避坑指南:我曾经在一个项目里遇到适配器注册失败的问题,折腾了两天才发现是 class 字段没设置。如果你想让用户空间通过 /dev/i2c-N 访问设备,记得把 class 设为 I2C_CLASS_HWMON 或 I2C_CLASS_SPD。

4.2.2 i2c_client —— 从设备

i2c_client 代表挂在总线上的一个具体设备。每个I2C从设备都有一个唯一的7位或10位地址,这个地址就存在 addr 字段里。

struct i2c_client {
    unsigned short flags;       // 标志位,如 10 位地址
    unsigned short addr;        // 从设备地址(7位或10位)
    char name[I2C_NAME_SIZE];   // 设备名称
    struct i2c_adapter *adapter;// 所属适配器
    struct device dev;          // 内核设备模型
    int irq;                    // 中断号(如果有)
    struct list_head detected;  // 用于设备检测链表
};

这个结构体通常在设备树或板级文件中静态定义,或者在驱动探测时动态创建。我个人习惯在设备树里声明,这样代码更干净。

4.2.3 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);
    struct device_driver driver;             // 内核驱动模型
    const struct i2c_device_id *id_table;    // 设备ID匹配表
    const struct of_device_id *of_match_table; // 设备树匹配表
    // ... 其他回调
};

你想想看,内核怎么知道你的驱动该驱动哪个设备?就是通过 id_table 或 of_match_table 来匹配的。匹配成功后,probe 函数被调用,你在里面做初始化、注册字符设备、创建sysfs文件等操作。

4.3 数据结构关系图

为了让你更直观地理解这三个结构体怎么协作,我画了一张关系图。

I2C 核心数据结构关系 i2c_adapter 总线编号: nr 算法: algo 设备列表 i2c_client 地址: addr 名称: name 所属适配器 i2c_driver probe / remove 回调 id_table / of_match_table 包含 匹配 匹配 匹配成功后,内核调用 i2c_driver->probe(client),将 client 与 driver 绑定。

4.4 驱动注册与注销流程

理解了数据结构,接下来就是怎么用它们。驱动注册和注销的流程其实很固定,我总结了一个步骤表。

步骤 操作 关键函数/宏 说明
1 定义 i2c_driver 结构体 struct i2c_driver 填充 probe、remove、id_table
2 定义设备ID表 i2c_device_id 用于传统匹配方式
3 定义设备树匹配表 of_device_id 用于设备树匹配(推荐)
4 注册驱动 i2c_add_driver() 宏展开为 i2c_register_driver()
5 注销驱动 i2c_del_driver() 在模块卸载时调用

嗯,这里要注意:i2c_add_driver() 是一个宏,它帮你做了模块所有权设置和注册两步操作。我建议你直接用这个宏,少写几行代码,也少犯错误。

4.5 一个完整的驱动骨架

光说不练假把式。我给你写一个最简单的I2C驱动骨架,你可以直接拿去改。

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>

// 设备树匹配表
static const struct of_device_id my_sensor_of_match[] = {
    { .compatible = "mycompany,my-sensor" },
    { }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);

// 传统ID匹配表
static const struct i2c_device_id my_sensor_id[] = {
    { "my-sensor", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, my_sensor_id);

// probe 函数:设备匹配成功后调用
static int my_sensor_probe(struct i2c_client *client,
                           const struct i2c_device_id *id)
{
    dev_info(&client->dev, "My sensor probed, addr=0x%02x\n", client->addr);
    // 在这里做:分配私有数据、初始化硬件、注册设备等
    return 0;
}

// remove 函数:设备移除时调用
static int my_sensor_remove(struct i2c_client *client)
{
    dev_info(&client->dev, "My sensor removed\n");
    // 在这里做:释放资源、注销设备等
    return 0;
}

// 定义 i2c_driver
static struct i2c_driver my_sensor_driver = {
    .driver = {
        .name = "my-sensor",
        .of_match_table = my_sensor_of_match,
    },
    .probe    = my_sensor_probe,
    .remove   = my_sensor_remove,
    .id_table = my_sensor_id,
};

// 模块入口/出口
module_i2c_driver(my_sensor_driver);

MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My I2C sensor driver");
MODULE_LICENSE("GPL");

注意:这个骨架里 module_i2c_driver() 宏同时完成了 module_init 和 module_exit 的定义。如果你需要额外的初始化逻辑(比如注册多个驱动),可以手动写 module_init 和 module_exit 函数。

4.6 匹配机制:驱动怎么找到设备?

内核通过两种方式匹配驱动和设备:

  • 设备树匹配:内核遍历 of_match_table,与设备树节点中的 compatible 属性比较。这是目前最主流的方式。
  • ID表匹配:内核遍历 id_table,与板级文件或ACPI中声明的设备名称比较。这种方式在旧内核或非设备树平台中常见。

匹配顺序是:先查设备树,再查ID表。如果都没匹配上,驱动就不会被调用。

个人经验:我曾经调试过一个驱动,死活不进入 probe。最后发现是设备树里 compatible 字符串写错了——多了一个空格。内核匹配是精确字符串比较,多一个空格都不行。所以,检查设备树时,建议用 cat /proc/device-tree/.../compatible 看看实际内容。

4.7 小结

这一章我们拆解了Linux I2C子系统的三个核心数据结构。记住:i2c_adapter 是总线控制器,i2c_client 是从设备,i2c_driver 是驱动逻辑。驱动开发的核心就是写好 i2c_driver,填好匹配表,实现 probe 和 remove。