3. I2C驱动框架:总线驱动、设备驱动、核心层与设备树匹配

好,咱们今天聊聊I2C驱动框架。触摸屏绝大多数都挂在I2C总线上,所以搞懂这个框架,后面移植驱动才能顺手。我刚开始接触嵌入式Linux时,也被这层那层绕得头晕,后来自己动手调了几个驱动,才慢慢理清楚。

说白了,Linux的I2C子系统就干三件事:管总线怎么发数据、管设备怎么被识别、管驱动怎么和设备配对。这三件事分别对应:I2C总线驱动、I2C设备驱动、I2C核心层。再加上设备树这个“媒人”,整个框架就完整了。

核心理解:I2C总线驱动负责硬件时序,I2C设备驱动负责业务逻辑,核心层负责调度和注册,设备树负责描述硬件连接关系。四者缺一不可。

3.1 I2C核心层——框架的“交通警察”

核心层在drivers/i2c/i2c-core.c里。它不直接操作硬件,而是提供一套标准接口。我习惯把它想象成交通警察——它不自己开车,但指挥所有车怎么走。

核心层主要干这几件事:

  • 注册/注销总线:管理系统中所有的I2C总线适配器。
  • 注册/注销设备与驱动:维护设备链表和驱动链表,负责匹配。
  • 提供通信API:比如i2c_transfer()、i2c_smbus_read_byte_data(),这些是设备驱动调用的底层接口。
  • 解析设备树:从设备树中提取I2C子节点,自动创建i2c_client结构体。

嗯,这里要注意:核心层里有个关键结构体叫i2c_adapter,它代表一个I2C控制器。还有个i2c_client,代表挂在总线上的一个设备。驱动开发时,你打交道最多的就是这两个结构体。

3.2 I2C总线驱动——管好“路”

总线驱动就是I2C控制器的驱动。在嵌入式平台里,它通常由芯片厂商(比如NXP、TI、Allwinner)提供。我当年在调试一款电容触摸屏时,发现I2C时钟老是不对,最后追到总线驱动里一个分频系数配错了——这种坑,你懂的。

总线驱动的核心是struct i2c_algorithm:

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就是最底层的收发函数。它直接操作I2C控制器的寄存器,产生START、STOP、ACK信号。总线驱动注册后,核心层就能通过它跟设备通信了。

个人经验:移植触摸屏驱动时,如果I2C通信失败,先别急着怀疑设备驱动。用i2c-tools工具(i2cdetect、i2cget、i2cset)测一下总线能不能正常读写。如果工具都读不到设备地址,那八成是总线驱动或硬件连接的问题。

3.3 I2C设备驱动——管好“车”

设备驱动才是我们写触摸屏驱动时主要动手的地方。它负责和具体的触摸屏芯片打交道——初始化、配置寄存器、读取触摸坐标、上报事件。

设备驱动的核心是struct i2c_driver:

struct i2c_driver {
    int (*probe)(struct i2c_client *client, 
                 const struct i2c_device_id *id);
    int (*remove)(struct i2c_client *client);
    struct device_driver driver;
    const struct i2c_device_id *id_table;
};

这里probe函数是关键。当设备树匹配成功后,内核会调用你的probe。我在probe里一般做这几件事:

  1. 读取设备树属性(比如中断引脚、复位GPIO)。
  2. 初始化触摸屏芯片(发一串初始化命令)。
  3. 注册中断处理函数。
  4. 注册输入设备(input subsystem)。

我曾经犯过一个低级错误:在probe里忘了检查id_table的兼容性,结果驱动被加载到了错误的I2C设备上,系统直接挂掉。从那以后,我每次都会仔细核对设备树里的compatible字符串和驱动里的of_match_table。

3.4 设备树匹配方式——驱动和设备怎么“相亲”

设备树匹配是现在的主流方式。内核通过设备树节点里的compatible属性,找到对应的驱动。匹配过程是这样的:

设备树里写:

&i2c1 {
    status = "okay";
    touchscreen@38 {
        compatible = "goodix,gt911";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>;
    };
};

驱动里写:

static const struct of_device_id gt911_of_match[] = {
    { .compatible = "goodix,gt911" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, gt911_of_match);

static struct i2c_driver gt911_driver = {
    .probe = gt911_probe,
    .remove = gt911_remove,
    .driver = {
        .name = "gt911",
        .of_match_table = gt911_of_match,
    },
};

匹配流程很简单:内核启动时,I2C核心层扫描设备树,为每个compatible匹配的节点创建i2c_client。然后遍历已注册的i2c_driver,比对of_match_table。匹配上了,就调用probe。

避坑指南:我曾经遇到一个情况,设备树里compatible写的是"goodix,gt911",但驱动里写的是"goodix,gt9xx"。就差一个数字,内核死活不匹配。调试了一下午才发现。所以,字符串必须一字不差,包括大小写和标点。

3.5 整体框架图

下面这张图把I2C驱动框架的四个层次和它们之间的关系画清楚了。你看一眼就能明白数据是怎么从应用层流到硬件寄存器的。

I2C驱动框架四层结构图 I2C驱动框架四层结构 应用层(触摸屏上报事件) I2C设备驱动(触摸屏驱动) probe / remove / id_table / of_match_table I2C核心层(i2c-core) 总线注册 / 设备驱动匹配 / 通信API I2C总线驱动(I2C控制器驱动) i2c_algorithm / master_xfer / 硬件寄存器 I2C硬件控制器 + 触摸屏芯片 设备树匹配 compatible ↓ 匹配 ↓ of_match_table ↓ 调用 ↓ probe() 创建i2c_client

3.6 匹配方式对比

除了设备树匹配,还有两种老方式:板级代码匹配和ACPI匹配。但在嵌入式Linux里,设备树是绝对主流。我整理了一个对比表:

匹配方式 数据来源 适用场景 灵活性 我个人的看法
设备树匹配 设备树.dts文件 ARM/ARM64嵌入式 高(改dts即可) 现在的主流,必须掌握
板级代码匹配 arch/arm/mach-xxx 老旧内核(3.x以前) 低(需重新编译内核) 建议尽快迁移到设备树
ACPI匹配 ACPI表 x86平台 中 触摸屏很少用,了解即可

3.7 驱动注册与注销的完整流程

最后,我把驱动注册到注销的完整流程走一遍。你写驱动时,这个流程就是你的“作战地图”:

  1. 模块加载:调用module_i2c_driver(gt911_driver),触发i2c_add_driver()。
  2. 核心层处理:将驱动加入驱动链表,遍历已存在的设备链表,尝试匹配。
  3. 设备树解析:核心层读取设备树,为每个compatible匹配的节点创建i2c_client。
  4. 匹配成功:调用驱动的probe函数,传入i2c_client指针。
  5. probe执行:初始化硬件、注册中断、注册输入设备。
  6. 正常运行:触摸屏上报数据,驱动通过input_report_key()等接口上报给上层。
  7. 模块卸载:调用i2c_del_driver(),触发remove,释放资源。

一个小技巧:调试时可以在probe函数开头加一句dev_info(&client->dev, "probe success\n");。如果内核启动日志里没看到这句话,说明驱动根本没被匹配上。这时候别急着查硬件,先检查compatible字符串对不对。

好了,I2C驱动框架的四个层次和匹配方式就讲到这里。你把这个框架理清了,后面写触摸屏驱动时,心里就有底了——知道代码该往哪层写,问题该往哪层查。