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 核心完成设备注册的完整流程,以及动态编号与固定编号的分支路径。
4.6 车载系统中的选择策略
在车载嵌入式项目中,推荐遵循以下策略:
- 主控 SoC 内置 I2C 控制器:使用
i2c_add_numbered_adapter,编号与设备树reg属性一致。例如,i2c@3 对应 nr=3。 - 外部 I2C 桥接芯片(如 PCA954x):使用
i2c_add_adapter,因为桥接芯片的子总线数量可能动态变化。 - USB 转 I2C 适配器(调试用):使用
i2c_add_adapter,避免与板上固定总线冲突。 - 虚拟 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 实现同步)。