1. 问题背景与现象描述
最近在基于正点原子imx6ull开发板进行I2C驱动开发时,遇到了一个颇为棘手的问题:当仅使用设备树compatible匹配方式时,驱动无法成功执行probe函数,而添加了传统的id_table匹配方式后却能正常工作。这与platform驱动的行为模式形成了鲜明对比——在platform驱动中,仅使用compatible匹配是完全可行的。
这个现象引起了我的强烈好奇:为什么同样基于设备树的匹配机制,在I2C驱动和platform驱动中会有如此差异?为了彻底搞清这个问题,我决定深入内核源码一探究竟。
2. 初步排查与验证
2.1 设备物理存在性验证
首先,我们需要确认设备是否真实存在于系统中。即使设备树中配置了节点,实际硬件可能并未焊接相应器件。
通过以下命令查看I2C总线下的设备:
bash复制ls /sys/bus/i2c/devices/
在我的测试中,系统显示存在两条I2C物理总线(i2c-0和i2c-1)以及挂载的设备。我的目标设备ap3216c显示为0-001e。
进一步使用i2cdetect扫描总线:
bash复制i2cdetect -y 0
结果显示1e(ap3216c)设备被识别到,但状态显示为"1e"而非"UU",这意味着设备已被识别但尚未匹配驱动。
关键解读:
- "1e":设备被识别但未匹配驱动
- "UU":设备已识别且匹配了驱动
- "--":设备未被识别
2.2 设备树配置检查
接下来验证设备树中的compatible属性是否与驱动中的定义一致。经检查,两者完全匹配:
dts复制ap3216c@1e {
compatible = "alientek,ap3216c";
reg = <0x1e>;
};
驱动中的匹配表:
c复制static const struct of_device_id ap3216c_of_match[] = {
{ .compatible = "alientek,ap3216c" },
{}
};
2.3 驱动冲突排查
检查设备是否被其他驱动占用:
bash复制ls -l /sys/bus/i2c/devices/0-001e/driver
结果显示"没有那个文件或目录",说明设备未被其他驱动占用。这一点也被后续添加id_table后能成功probe所证实。
3. 深入内核源码分析
3.1 I2C设备匹配机制
I2C驱动的匹配过程由i2c_device_match()函数控制,其核心逻辑如下:
c复制static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
struct i2c_client *client = i2c_verify_client(dev);
struct i2c_driver *driver;
if (!client) return 0;
/* 设备树匹配方式 */
if (of_driver_match_device(dev, drv)) {
printk("iic通过compatibel匹配\n");
return 1;
}
/* ACPI匹配方式 */
if (acpi_driver_match_device(dev, drv))
return 1;
driver = to_i2c_driver(drv);
/* ID表匹配方式 */
if (driver->id_table) {
printk("iic通过id_table匹配\n");
return i2c_match_id(driver->id_table, client) != NULL;
}
return 0;
}
3.2 设备树匹配的关键细节
of_driver_match_device()的实现涉及一个重要条件编译:
c复制#ifdef CONFIG_OF
static inline int of_driver_match_device(struct device *dev,
const struct device_driver *drv)
{
return of_match_device(drv->of_match_table, dev) != NULL;
}
#else
static inline int of_driver_match_device(struct device *dev,
struct device_driver *drv)
{
return 0;
}
#endif
通过编译命令验证:
bash复制make -j12 V=1 zImage | grep "CONFIG_OF"
确认CONFIG_OF=y,说明内核确实支持设备树匹配方式。
3.3 匹配成功但probe失败的根源
虽然匹配成功了,但probe函数仍未执行。问题出在i2c_device_probe()函数:
c复制static int i2c_device_probe(struct device *dev)
{
struct i2c_client *client = i2c_verify_client(dev);
struct i2c_driver *driver;
if (!client) return 0;
driver = to_i2c_driver(dev->driver);
/* 关键检查点 */
if (!driver->probe || !driver->id_table)
return -ENODEV;
status = driver->probe(client, i2c_match_id(driver->id_table, client));
return status;
}
这个检查意味着:
- 即使通过设备树匹配成功
- 如果id_table为空,仍然会返回-ENODEV
- 导致probe函数无法执行
4. 问题本质与解决方案
4.1 I2C与Platform驱动的差异
虽然两者都支持设备树匹配,但I2C驱动在probe前有额外的id_table检查,这是设计上的差异:
| 特性 | I2C驱动 | Platform驱动 |
|---|---|---|
| 匹配方式 | 支持设备树和id_table | 主要支持设备树 |
| Probe前检查 | 强制检查id_table存在性 | 无特殊检查 |
| 参数传递 | probe需要id_table参数 | probe不需要id参数 |
4.2 必须提供id_table的原因
内核强制要求I2C驱动提供id_table的主要原因包括:
- 历史兼容性:I2C驱动早于设备树出现,需要保持向后兼容
- 参数传递:I2C probe函数需要接收id_table参数
- 非设备树场景:某些旧系统可能仍使用传统匹配方式
4.3 最终解决方案
为确保驱动可靠工作,必须同时提供:
- 设备树匹配表(of_device_id)
- 传统ID表(i2c_device_id)
示例实现:
c复制static const struct i2c_device_id ap3216c_id[] = {
{"ap3216c", 0},
{}
};
static const struct of_device_id ap3216c_of_match[] = {
{ .compatible = "alientek,ap3216c" },
{}
};
MODULE_DEVICE_TABLE(i2c, ap3216c_id);
MODULE_DEVICE_TABLE(of, ap3216c_of_match);
struct i2c_driver ap3216c_driver = {
.driver = {
.name = "ap3216c",
.of_match_table = ap3216c_of_match,
},
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.id_table = ap3216c_id,
};
5. 经验总结与避坑指南
5.1 I2C驱动开发检查清单
- 双重匹配表:始终同时提供of_match_table和id_table
- 设备存在性验证:使用i2cdetect确认设备响应
- 驱动冲突检查:查看/sys/bus/i2c/devices/xx-xx/driver
- 内核配置确认:确保CONFIG_OF=y支持设备树
- 调试技巧:添加printk跟踪匹配流程
5.2 常见问题排查流程
当I2C驱动无法probe时,建议按以下步骤排查:
-
硬件层:
- 确认设备已正确焊接
- 检查I2C总线是否启用
- 测量I2C信号是否正常
-
设备树层:
- 确认compatible字符串完全匹配
- 检查reg地址是否正确
- 验证节点是否在正确总线之下
-
驱动层:
- 检查是否提供了id_table
- 确认probe函数原型正确
- 查看内核日志是否有错误信息
-
系统层:
- 确认没有其他驱动占用设备
- 检查内核配置选项
- 验证模块依赖关系
5.3 高级调试技巧
- 动态打印:
c复制# 启用I2C核心调试
echo 8 > /sys/module/i2c_core/parameters/debug
- 设备树反查:
bash复制# 查看设备树节点信息
cat /proc/device-tree/ap3216c@1e/compatible
- 事件追踪:
bash复制# 跟踪I2C总线事件
echo 1 > /sys/kernel/debug/tracing/events/i2c/enable
cat /sys/kernel/debug/tracing/trace_pipe
在实际项目中,我发现这个设计特点虽然增加了开发者的工作量,但也带来了更好的兼容性和灵活性。特别是在维护既有系统时,能够平滑过渡到设备树而不破坏原有功能。