4. I2C 适配器驱动注册:i2c_add_adapter 与 i2c_add_numbered_adapter 接口详解

在车载 Linux 系统中,I2C 适配器(Adapter)是连接 SoC 控制器与外围传感器(如温度、压力、摄像头)的桥梁。适配器驱动的注册是整个 I2C 子系统初始化的核心步骤。Linux 内核提供了两个关键注册接口:i2c_add_adapter 和 i2c_add_numbered_adapter。理解它们的差异与适用场景,是编写稳定、可移植的车载 I2C 驱动的必备技能。

4.1 核心数据结构:struct i2c_adapter

在深入注册接口之前,必须了解适配器对象的核心成员。该结构体定义在 include/linux/i2c.h 中,驱动开发者需要填充关键字段后调用注册函数。

成员 类型 说明
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 或 pci_dev)
nr int 适配器编号(总线号),i2c_add_adapter 动态分配时为 -1
name char[48] 适配器名称,用于调试和 sysfs 显示
timeout int 传输超时时间(单位:jiffies)
retries int 传输失败时的重试次数
车载实践提示: 在车载项目中,timeout 和 retries 需要根据具体传感器数据手册调整。例如,某些温度传感器在转换期间需要较长等待时间,建议将 timeout 设置为 100ms 以上,避免在高速 CAN 通信中频繁超时。

4.2 i2c_add_adapter:动态编号注册

i2c_add_adapter 是使用最广泛的注册接口。它由内核动态分配一个空闲的总线编号(nr),并自动创建对应的 /dev/i2c-N 设备节点。该接口适用于以下场景:

  • 适配器数量不固定,或总线号对上层应用透明。
  • 设备树中未显式指定 reg 对应的总线号。
  • 热插拔式 I2C 控制器(如 USB 转 I2C 适配器)。
// 典型调用示例
struct i2c_adapter *adap;
int ret;

adap = kzalloc(sizeof(*adap), GFP_KERNEL);
adap->owner = THIS_MODULE;
adap->class = I2C_CLASS_HWMON;
adap->algo = &my_i2c_algo;
adap->dev.parent = &pdev->dev;
strscpy(adap->name, "my_i2c_adapter", sizeof(adap->name));

ret = i2c_add_adapter(adap);
if (ret) {
    dev_err(&pdev->dev, "Failed to add i2c adapter: %d\n", ret);
    kfree(adap);
    return ret;
}
关键行为: 调用 i2c_add_adapter 后,内核会在 /sys/class/i2c-adapter/ 下创建目录,并通过 i2cdev 模块生成 /dev/i2c-N 节点。总线号 N 由内核在 [0, MAX_ID_MASK] 范围内动态分配,不可预测。

4.3 i2c_add_numbered_adapter:固定编号注册

i2c_add_numbered_adapter 允许驱动开发者指定一个固定的总线编号。该接口在车载系统中尤为重要,因为:

  • 车载网络拓扑通常固定,传感器挂载在特定 I2C 总线上。
  • 用户空间诊断工具(如 i2cdetect)需要固定的总线号进行扫描。
  • 设备树中通过 i2c-bus 属性引用特定适配器。
// 固定编号注册示例
struct i2c_adapter *adap;
int ret;

adap = kzalloc(sizeof(*adap), GFP_KERNEL);
adap->owner = THIS_MODULE;
adap->class = I2C_CLASS_HWMON;
adap->algo = &my_i2c_algo;
adap->dev.parent = &pdev->dev;
strscpy(adap->name, "my_i2c_adapter", sizeof(adap->name));

// 指定总线号为 3(对应设备树中的 i2c@3)
adap->nr = 3;

ret = i2c_add_numbered_adapter(adap);
if (ret) {
    dev_err(&pdev->dev, "Failed to add numbered i2c adapter: %d\n", ret);
    kfree(adap);
    return ret;
}
避坑指南: 如果指定的 nr 已经被其他适配器占用,i2c_add_numbered_adapter 会返回 -EBUSY。在车载多控制器系统中,务必通过设备树或板级文件确保总线号唯一,避免因硬件复用导致注册失败。

4.4 接口对比与选择策略

对比维度 i2c_add_adapter i2c_add_numbered_adapter
总线号来源 内核动态分配 驱动指定
适用场景 通用、热插拔、动态拓扑 固定拓扑、车载、设备树绑定
设备树兼容性 需通过别名或 i2c-parent 间接引用 直接匹配 reg 属性
调试可预测性 总线号每次启动可能不同 总线号固定,便于脚本和工具
注册失败风险 低(内核自动选择空闲号) 中(需手动管理冲突)
车载推荐度 不推荐(除非 USB 转 I2C) 强烈推荐

4.5 注册流程与内核内部机制

下图展示了从驱动调用注册接口到 I2C 核心完成设备注册的完整流程,以及动态编号与固定编号的分支路径。

I2C 适配器注册流程:动态 vs 固定编号 I2C 适配器注册流程:动态 vs 固定编号 驱动调用注册接口 检查 adap->nr 是否有效 nr < 0 i2c_add_adapter nr >= 0 i2c_add_numbered_adapter ida_simple_get 分配 ID 设置 adap->nr = 分配值 检查 nr 是否已被占用 占用则返回 -EBUSY i2c_register_adapter 核心注册 创建 sysfs / dev 节点 说明:动态编号通过 IDA 分配器获取空闲号;固定编号需手动确保 nr 唯一,否则注册失败。

4.6 车载系统中的选择策略

在车载嵌入式项目中,推荐遵循以下策略:

  1. 主控 SoC 内置 I2C 控制器:使用 i2c_add_numbered_adapter,编号与设备树 reg 属性一致。例如,i2c@3 对应 nr=3。
  2. 外部 I2C 桥接芯片(如 PCA954x):使用 i2c_add_adapter,因为桥接芯片的子总线数量可能动态变化。
  3. USB 转 I2C 适配器(调试用):使用 i2c_add_adapter,避免与板上固定总线冲突。
  4. 虚拟 I2C 适配器(用于模拟):使用 i2c_add_numbered_adapter 并指定一个高位编号(如 100+),避免与物理总线重叠。
车载案例: 某车载信息娱乐系统使用 i.MX8QM 处理器,板上有 4 个 I2C 总线:i2c@1(摄像头)、i2c@2(触摸屏)、i2c@3(音频编解码器)、i2c@4(环境传感器)。驱动中分别调用 i2c_add_numbered_adapter 并指定 nr=1,2,3,4,确保每次启动后总线号固定,用户空间诊断脚本可直接使用 i2cdetect -y 3 检测音频编解码器。

4.7 注销与资源释放

与注册对应的注销接口为 i2c_del_adapter,在驱动卸载或设备移除时调用:

// 注销适配器
i2c_del_adapter(adap);
kfree(adap);

无论使用哪种注册方式,注销接口完全相同。内核会自动清理 sysfs 条目、设备节点,并通知所有挂载在该适配器上的客户端驱动进行解绑。

注意: 在车载系统中,如果适配器正在处理中断或 DMA 传输,直接调用 i2c_del_adapter 可能导致内核崩溃。建议在注销前先确保所有挂载设备已移除,并等待正在进行的传输完成(可通过 i2c_lock_bus 实现同步)。