4. I2C适配器注册流程:i2c_add_adapter()与i2c_add_numbered_adapter()函数调用链、适配器编号分配策略
I2C适配器(Adapter)是Linux I2C子系统的核心总线控制器抽象。每个物理I2C总线控制器(如S3C2440、OMAP、i.MX的I2C接口)在驱动加载时,都需要向内核注册一个struct i2c_adapter实例。内核提供两个注册入口:i2c_add_adapter()(动态编号)和i2c_add_numbered_adapter()(静态编号)。理解它们的调用链与编号分配策略,是编写稳定I2C适配器驱动的关键。
4.1 核心数据结构:struct i2c_adapter
注册前,驱动必须填充适配器结构体。关键字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
owner |
struct module * |
模块所有者,通常设为THIS_MODULE |
class |
unsigned int |
适配器类型,如I2C_CLASS_HWMON |
algo |
const struct i2c_algorithm * |
总线访问算法(master_xfer / smbus_xfer) |
dev.parent |
struct device * |
父设备(通常是platform_device) |
nr |
int |
适配器编号(总线号),i2c_add_adapter自动分配,i2c_add_numbered_adapter使用预设值 |
name |
char[48] |
适配器名称,显示在/sys/bus/i2c/devices中 |
timeout |
int |
传输超时时间(单位:jiffies) |
retries |
int |
传输失败重试次数 |
关键点:
nr字段在注册前由驱动决定或留空(-1),注册后变为最终总线编号。用户空间通过/dev/i2c-N访问对应适配器,N即nr值。
4.2 注册入口对比
| 函数 | 原型 | 编号来源 | 典型场景 |
|---|---|---|---|
i2c_add_adapter() |
int i2c_add_adapter(struct i2c_adapter *adap) |
内核动态分配(从0开始递增) | 设备树未指定总线号、热插拔总线、通用驱动 |
i2c_add_numbered_adapter() |
int i2c_add_numbered_adapter(struct i2c_adapter *adap) |
使用adap->nr预设值 |
硬件固定总线号、与用户空间约定、多适配器板级 |
4.3 调用链深度分析
两个函数最终都汇聚到核心函数i2c_register_adapter(),但编号处理路径不同。下图展示了完整的调用链与分支:
4.4 编号分配策略详解
4.4.1 动态编号:i2c_add_adapter()
内核使用位图(bitmap)管理已分配的总线编号。分配流程如下:
- 清空nr:将
adap->nr设为-1,表示需要动态分配。 - 调用
__i2c_add_numbered_adapter():检测到nr == -1,进入动态分支。 - 位图扫描:调用
i2c_adapter_idr_alloc(),从0开始查找第一个未使用的编号。 - 编号锁定:将找到的编号标记为已用,写入
adap->nr。 - 注册:继续执行
i2c_register_adapter()。
注意:动态编号从0开始递增,但不会回收已卸载适配器的编号(除非显式释放)。如果系统频繁热插拔I2C总线,编号可能持续增长。内核社区在5.x版本中引入了
ida机制改善编号复用,但默认行为仍是递增。
4.4.2 静态编号:i2c_add_numbered_adapter()
驱动预先指定总线号,适用于硬件设计固定的场景:
- 预设nr:驱动在注册前设置
adap->nr = 1(或其他值)。 - 范围检查:内核检查
nr是否在[0, MAX_ID_MASK]范围内(通常最大支持0x3fffffff)。 - 冲突检测:调用
i2c_adapter_idr_alloc_fixed(),尝试分配指定编号。如果已被占用,返回-EBUSY。 - 注册:冲突检测通过后,进入
i2c_register_adapter()。
避坑指南:静态编号必须保证唯一性。在多适配器系统中,建议在设备树中通过
reg属性或板级代码中通过宏定义统一管理编号。不要硬编码nr=0,因为i2c-0通常被系统保留或由其他驱动占用。
4.5 编号分配策略对比
| 特性 | 动态编号 | 静态编号 |
|---|---|---|
| 编号来源 | 内核自动分配 | 驱动预设 |
| 编号确定性 | 依赖加载顺序,不可预测 | 固定,可预测 |
| 冲突风险 | 低(内核保证不重复) | 高(需手动保证唯一) |
| 热插拔友好 | 是 | 否(编号可能冲突) |
| 用户空间依赖 | 需动态查找(如通过i2cdetect -l) |
可直接使用固定/dev/i2c-N |
| 典型场景 | 设备树未指定、通用驱动 | 硬件设计固定、与用户空间约定 |
4.6 注册后自动扫描:从设备发现机制
i2c_register_adapter()在设备注册完成后,会触发从设备扫描:
- 设备树扫描:如果系统使用设备树,
of_i2c_register_devices()会遍历适配器节点下的所有子节点,为每个子节点创建struct i2c_client并调用i2c_new_device()。 - 板级信息扫描:对于非设备树系统,内核会检查
board_info列表,注册预定义的从设备。 - sysfs接口:用户可以通过
/sys/bus/i2c/devices/i2c-N/new_device手动添加从设备。
4.7 典型注册代码模板
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/platform_device.h>
static struct i2c_adapter my_adap = {
.owner = THIS_MODULE,
.class = I2C_CLASS_HWMON,
.algo = &my_algo,
.dev.parent = &pdev->dev,
.name = "my_i2c_adapter",
.timeout = 100, /* 100 jiffies */
.retries = 3,
};
static int my_probe(struct platform_device *pdev)
{
int ret;
/* 方式1:动态编号 */
ret = i2c_add_adapter(&my_adap);
if (ret)
dev_err(&pdev->dev, "动态注册失败: %d\n", ret);
/* 方式2:静态编号(假设硬件固定为总线1) */
// my_adap.nr = 1;
// ret = i2c_add_numbered_adapter(&my_adap);
// if (ret)
// dev_err(&pdev->dev, "静态注册失败: %d\n", ret);
return ret;
}
static int my_remove(struct platform_device *pdev)
{
i2c_del_adapter(&my_adap);
return 0;
}
module_platform_driver(my_driver);
最佳实践:
- 优先使用
i2c_add_adapter()动态编号,除非有明确的硬件编号约定。 - 静态编号时,在设备树中通过
reg = <1>;指定总线号,驱动从设备树读取并设置nr。 - 注册前确保
algo指针非空,且master_xfer回调已实现。 - 在
remove中必须调用i2c_del_adapter(),否则会导致编号泄漏和后续注册失败。
4.8 编号分配内部机制:IDR与位图
内核使用IDR(Integer ID Radix Tree)管理适配器编号。IDR是一种将整数ID映射到指针的基数树结构,支持高效的分配、查找和释放。关键函数:
| 函数 | 作用 |
|---|---|
idr_alloc() |
动态分配一个未使用的ID(对应动态编号) |
idr_alloc_cyclic() |
循环分配,避免总是从0开始 |
idr_remove() |
释放指定ID(适配器卸载时调用) |
idr_find() |
根据ID查找对应的适配器指针 |
IDR相比早期的位图实现有以下优势:
- 支持稀疏ID分配,不会因为卸载一个高编号适配器而浪费位图空间。
- 分配和查找时间复杂度为O(1)(基数树层数固定)。
- 天然支持动态和静态两种分配模式。
4.9 常见注册失败原因排查
| 错误码 | 含义 | 排查方向 |
|---|---|---|
-EBUSY |
静态编号冲突 | 检查nr是否被其他适配器占用,使用i2cdetect -l查看已注册总线 |
-EINVAL |
参数无效 | 检查algo是否为NULL,owner是否设置,dev.parent是否有效 |
-ENOMEM |
内存不足 | 检查系统内存,IDR分配失败 |
-ENODEV |
父设备未注册 | 确保dev.parent指向的platform_device已成功probe |