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() 函数,按照以下优先级进行匹配:
- 设备树风格匹配(OF匹配):比较设备节点的
compatible属性与驱动of_match_table中的字符串。 - ACPI风格匹配:在支持ACPI的平台上,比较ACPI ID。
- 传统ID表匹配:比较设备名称与驱动
id_table中的name字段。 - 名称直接匹配:比较设备名称与驱动名称。
关键点: 在嵌入式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函数执行完毕的完整流程:
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 动态开启调试信息。