3. Linux I2C 子系统架构:三层模型详解
Linux I2C 子系统采用经典的三层软件架构设计,将硬件控制、总线管理与设备交互解耦。理解这一分层模型是进行 I2C 控制器驱动移植与中断绑定的基础。三层分别为:I2C 核心层、I2C 总线驱动层(即控制器驱动层)与 I2C 设备驱动层。
核心要点: 三层模型的设计哲学是“分离关注点”。总线驱动层负责与硬件寄存器打交道,设备驱动层负责与具体外设通信,核心层则提供统一的注册、注销、数据传输与适配器管理接口。
3.1 I2C 核心层 (I2C Core)
核心层是 I2C 子系统的中枢,位于 drivers/i2c/ 目录下,主要文件包括 i2c-core.c、i2c-dev.c 和 i2c-boardinfo.c。它不直接操作硬件,而是提供一套统一的框架和 API,供上下两层调用。
| 核心功能 | 关键 API / 数据结构 | 说明 |
|---|---|---|
| 适配器管理 | i2c_add_adapter() / i2c_del_adapter() |
注册/注销 I2C 总线控制器 |
| 驱动管理 | i2c_register_driver() / i2c_del_driver() |
注册/注销 I2C 设备驱动 |
| 数据传输 | i2c_transfer() / i2c_master_send() / i2c_master_recv() |
发起 I2C 总线读写事务 |
| 设备枚举 | i2c_new_device() / i2c_new_probed_device() |
动态创建 I2C 设备节点 |
| 用户空间接口 | /dev/i2c-N (通过 i2c-dev) |
提供字符设备访问方式 |
移植提示: 核心层代码通常不需要修改。移植工作主要围绕“如何让核心层找到并正确调用你的控制器驱动”展开,即正确实现
i2c_adapter 和 i2c_algorithm。
3.2 I2C 总线驱动层 (I2C Bus/Adapter Driver)
总线驱动层直接面向硬件,是驱动移植的核心战场。它负责实现 I2C 控制器的初始化、时钟配置、中断处理、以及底层时序的生成。该层主要实现两个关键数据结构:struct i2c_adapter 和 struct i2c_algorithm。
总线驱动层需要完成的核心任务清单:
- 硬件初始化: 使能 I2C 控制器时钟、配置引脚复用 (Pinmux)、设置波特率。
- 中断注册: 申请 IRQ 并绑定中断处理函数,用于处理传输完成、仲裁丢失、错误等事件。
- 实现 master_xfer: 这是最关键的函数,负责将上层下发的
struct i2c_msg数组转换为硬件时序。 - 实现 functionality: 返回控制器支持的操作类型(如 I2C_FUNC_I2C、I2C_FUNC_SMBUS_EMUL)。
- 注册适配器: 调用
i2c_add_adapter()将适配器加入核心层管理。
常见陷阱: 在 master_xfer 中如果使用轮询方式等待传输完成,会阻塞调用进程并浪费 CPU。正确的做法是使用中断 + 完成量 (completion) 机制,在中断处理函数中唤醒等待的传输线程。这是中断绑定的核心价值所在。
3.3 I2C 设备驱动层 (I2C Device Driver)
设备驱动层面向具体的 I2C 从设备(如 EEPROM、温度传感器、音频编解码器)。它通过核心层提供的 API 与总线驱动层交互,无需关心底层硬件时序细节。
设备驱动层的关键数据结构与流程:
| 组件 | 描述 |
|---|---|
struct i2c_driver |
驱动对象,包含 probe()、remove()、id_table 等成员 |
struct i2c_client |
设备对象,由核心层根据板级信息或设备树创建 |
struct i2c_device_id |
用于传统板级匹配的设备 ID 表 |
of_match_table |
用于设备树匹配的 compatible 字符串表 |
一个典型的 I2C 设备驱动 probe 函数模板如下:
static int my_sensor_probe(struct i2c_client *client)
{
struct my_sensor_data *data;
int ret;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
/* 读取设备 ID 确认连接 */
ret = i2c_smbus_read_byte_data(client, REG_DEVICE_ID);
if (ret < 0) {
dev_err(&client->dev, "failed to read device id\n");
return ret;
}
/* 注册输入设备 / 硬件监控设备 / 音频设备等 */
return 0;
}
static const struct i2c_device_id my_sensor_id[] = {
{ "my-sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_sensor_id);
static const struct of_device_id my_sensor_of_match[] = {
{ .compatible = "vendor,my-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);
static struct i2c_driver my_sensor_driver = {
.driver = {
.name = "my_sensor",
.of_match_table = my_sensor_of_match,
},
.probe = my_sensor_probe,
.id_table = my_sensor_id,
};
module_i2c_driver(my_sensor_driver);
3.4 三层之间的数据流与协作
当用户空间或内核其他子系统发起一次 I2C 读取操作时,数据在三层之间的流动路径如下:
3.5 三层模型在驱动移植中的指导意义
理解三层模型后,驱动移植的边界变得非常清晰:
- 不需要修改: I2C 核心层代码。它由内核社区维护,与具体硬件无关。
- 需要重写或适配: I2C 总线驱动层。这是移植工作的主体,包括实现
i2c_algorithm、处理中断、配置时钟。 - 通常不需要修改: 现有的 I2C 设备驱动。只要从设备兼容,设备驱动可以直接复用。但需要确保设备树或板级文件中正确描述了设备连接关系。
移植工作清单(基于三层模型):
1. 在总线驱动层实现
2. 在总线驱动层调用
3. 在设备树中描述 I2C 控制器节点(地址、中断、时钟、DMA)。
4. 在设备树中描述挂载在该总线上的从设备节点(compatible、reg)。
5. 确保从设备的
1. 在总线驱动层实现
i2c_algorithm->master_xfer(),并绑定中断。2. 在总线驱动层调用
i2c_add_adapter() 注册适配器。3. 在设备树中描述 I2C 控制器节点(地址、中断、时钟、DMA)。
4. 在设备树中描述挂载在该总线上的从设备节点(compatible、reg)。
5. 确保从设备的
i2c_driver 能够匹配设备树中的 compatible 字符串。