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(),但编号处理路径不同。下图展示了完整的调用链与分支:

I2C适配器注册调用链 I2C适配器注册调用链 i2c_add_adapter() i2c_add_numbered_adapter() 动态编号:adap->nr = -1 静态编号:adap->nr = 预设值 __i2c_add_numbered_adapter() 编号有效性检查 + 冲突检测 i2c_register_adapter() 设备注册 | sysfs创建 | 总线扫描 device_register() 注册为linux设备 i2c_sysfs_new_device() 创建sysfs属性文件 of_i2c_register_devices() 扫描设备树子节点 适配器注册完成 /sys/bus/i2c/devices/i2c-N 可用

4.4 编号分配策略详解

4.4.1 动态编号:i2c_add_adapter()

内核使用位图(bitmap)管理已分配的总线编号。分配流程如下:

  1. 清空nr:将adap->nr设为-1,表示需要动态分配。
  2. 调用__i2c_add_numbered_adapter():检测到nr == -1,进入动态分支。
  3. 位图扫描:调用i2c_adapter_idr_alloc(),从0开始查找第一个未使用的编号。
  4. 编号锁定:将找到的编号标记为已用,写入adap->nr。
  5. 注册:继续执行i2c_register_adapter()。
注意:动态编号从0开始递增,但不会回收已卸载适配器的编号(除非显式释放)。如果系统频繁热插拔I2C总线,编号可能持续增长。内核社区在5.x版本中引入了ida机制改善编号复用,但默认行为仍是递增。

4.4.2 静态编号:i2c_add_numbered_adapter()

驱动预先指定总线号,适用于硬件设计固定的场景:

  1. 预设nr:驱动在注册前设置adap->nr = 1(或其他值)。
  2. 范围检查:内核检查nr是否在[0, MAX_ID_MASK]范围内(通常最大支持0x3fffffff)。
  3. 冲突检测:调用i2c_adapter_idr_alloc_fixed(),尝试分配指定编号。如果已被占用,返回-EBUSY。
  4. 注册:冲突检测通过后,进入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);
最佳实践:
  1. 优先使用i2c_add_adapter()动态编号,除非有明确的硬件编号约定。
  2. 静态编号时,在设备树中通过reg = <1>;指定总线号,驱动从设备树读取并设置nr。
  3. 注册前确保algo指针非空,且master_xfer回调已实现。
  4. 在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