4. I2C设备注册流程:probe函数触发机制、设备与驱动的匹配过程

在Linux I2C子系统中,设备与驱动的匹配是驱动加载的核心环节。当I2C设备节点在设备树中被解析、或者通过板级代码静态注册时,内核会遍历已注册的I2C驱动列表,通过匹配表(of_match_table / id_table)找到对应的驱动,并触发其probe函数。本节将深入剖析这一匹配机制与probe触发流程。

4.1 设备注册的两种途径

I2C设备的注册主要分为以下两种方式:

途径 数据来源 注册时机 典型场景
设备树(Device Tree) .dts 文件中的I2C子节点 内核启动时,I2C控制器驱动解析子节点 ARM/ARM64嵌入式平台
板级代码(Board Code) i2c_board_info 结构体数组 模块初始化或arch_initcall阶段 旧版内核或非设备树平台

无论哪种途径,最终都会在I2C核心层生成一个 struct i2c_client 实例,并挂入I2C适配器的设备链表。

4.2 匹配过程详解

当一个新的 i2c_client 被添加到总线时,I2C核心会调用 i2c_device_match() 函数,按照以下优先级进行匹配:

  1. 设备树风格匹配(OF匹配):比较设备节点的 compatible 属性与驱动 of_match_table 中的字符串。
  2. ACPI风格匹配:在支持ACPI的平台上,比较ACPI ID。
  3. 传统ID表匹配:比较设备名称与驱动 id_table 中的 name 字段。
  4. 名称直接匹配:比较设备名称与驱动名称。
关键点: 在嵌入式Linux中,设备树匹配(OF匹配)是最常用的方式。驱动开发者只需在 of_match_table 中声明与设备树节点一致的 compatible 字符串即可。

4.3 probe函数触发流程

匹配成功后,内核调用 i2c_device_probe() 函数,该函数执行以下步骤:

  • 检查驱动与设备是否已经绑定,避免重复probe。
  • 调用驱动中定义的 probe 回调函数,并传入匹配到的 i2c_client 指针。
  • 在probe函数内部,驱动开发者完成:
    - 从设备树或平台数据中解析配置(如中断号、寄存器地址、时钟频率)。
    - 注册中断处理函数(request_irq / devm_request_irq)。
    - 初始化设备私有数据结构。
    - 注册字符设备、输入设备或IIO设备等。
  • probe成功返回0,失败返回负错误码。
最佳实践: 推荐使用 devm_ 系列API(如 devm_kzalloc、devm_request_irq),这些API会在设备注销或驱动卸载时自动释放资源,避免内存泄漏。

4.4 匹配与probe流程总览

下图展示了从设备树节点解析到probe函数执行完毕的完整流程:

I2C设备匹配与probe触发流程 I2C设备匹配与probe触发流程 设备树解析 创建 i2c_client 实例 i2c_device_match() 匹配成功 i2c_device_probe() 驱动 probe() 回调执行 设备初始化完成

4.5 匹配表结构示例

驱动中用于匹配的关键数据结构如下:

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

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

// 驱动结构体
static struct i2c_driver myi2c_driver = {
    .driver = {
        .name = "my_i2c_driver",
        .of_match_table = myi2c_of_match,
    },
    .probe    = myi2c_probe,
    .remove   = myi2c_remove,
    .id_table = myi2c_id,
};
注意: MODULE_DEVICE_TABLE 宏用于导出设备表,使得内核在模块加载时能够自动识别该驱动支持的设备。如果遗漏此宏,设备树匹配可能失效。

4.6 probe函数典型骨架

以下是一个标准的I2C驱动probe函数实现模板:

static int myi2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct my_device_data *data;
    int ret;

    // 1. 分配私有数据结构
    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    // 2. 解析设备树属性(如中断、时钟、GPIO)
    data->irq = irq_of_parse_and_map(client->dev.of_node, 0);
    if (data->irq < 0) {
        dev_err(&client->dev, "failed to get IRQ\n");
        return data->irq;
    }

    // 3. 注册中断
    ret = devm_request_irq(&client->dev, data->irq, myi2c_irq_handler,
                           IRQF_TRIGGER_RISING, "myi2c", data);
    if (ret) {
        dev_err(&client->dev, "failed to request IRQ\n");
        return ret;
    }

    // 4. 初始化硬件(如复位、配置寄存器)
    // ...

    // 5. 注册上层设备(如输入设备、IIO设备、字符设备)
    // ...

    i2c_set_clientdata(client, data);
    dev_info(&client->dev, "probe success\n");
    return 0;
}

4.7 匹配与probe的常见问题

问题现象 可能原因 排查方法
probe函数未被调用 compatible字符串不匹配 / 驱动未编译进内核 检查 /sys/bus/i2c/devices/ 下是否有设备节点;核对dts与驱动中的compatible值
probe返回-ENODEV 设备不存在或硬件地址错误 使用 i2cdetect 工具扫描总线地址
中断不触发 中断号解析错误 / 中断类型配置错误 检查 /proc/interrupts;确认设备树中interrupts属性格式
资源泄漏 未使用devm_系列API 检查probe中是否手动调用 kzalloc 但未在remove中释放
调试技巧: 在probe函数入口处添加 dev_dbg(&client->dev, "probe enter\n");,并确保内核开启了 CONFIG_DYNAMIC_DEBUG 或 DEBUG_DRIVER,通过 echo 'file myi2c_driver.c +p' > /sys/kernel/debug/dynamic_debug/control 动态开启调试信息。