3. Linux I2C 子系统架构:三层架构详解
Linux I2C 子系统采用经典的分层设计思想,将复杂的 I2C 通信控制抽象为三个职责清晰的层次:I2C 核心层、I2C 总线驱动层和I2C 设备驱动层。这种分层架构使得驱动开发者无需关心底层硬件寄存器操作,只需专注于各自层次的接口实现,极大地提高了代码的可移植性和复用性。
3.1 I2C 核心层 (I2C Core)
I2C 核心层是 Linux I2C 子系统的中枢神经,位于 drivers/i2c/ 目录下,主要由 i2c-core.c、i2c-dev.c 和 i2c-boardinfo.c 等文件实现。它不直接操作硬件,而是提供一套统一的框架和 API,向上对接设备驱动,向下适配总线驱动。
- 设备与驱动管理:维护 i2c_bus_type 总线类型,负责 i2c_driver 和 i2c_client 的注册、注销与匹配。
- 通信抽象:提供 i2c_transfer()、i2c_master_send()、i2c_master_recv() 等高层 API,屏蔽底层硬件差异。
- SMBus 兼容:实现 SMBus 协议栈,提供 i2c_smbus_read_byte()、i2c_smbus_write_word() 等接口。
- 用户空间接口:通过 i2c-dev.c 创建 /dev/i2c-N 设备节点,允许用户空间直接访问 I2C 总线。
| 文件名 | 核心功能 | 关键 API / 数据结构 |
|---|---|---|
| i2c-core.c | 核心框架:总线注册、驱动匹配、设备生命周期 | i2c_add_driver(), i2c_new_device(), i2c_transfer() |
| i2c-dev.c | 用户空间字符设备接口 | i2cdev_read(), i2cdev_write(), i2cdev_ioctl() |
| i2c-boardinfo.c | 板级设备信息注册(非设备树平台) | i2c_register_board_info() |
| i2c-smbus.c | SMBus 协议实现 | i2c_smbus_xfer(), i2c_smbus_read_byte_data() |
3.2 I2C 总线驱动层 (I2C Bus Driver / Adapter)
总线驱动层是 I2C 子系统中最接近硬件的层次,直接操作 SoC 内部 I2C 控制器的寄存器。它负责实现 I2C 总线的时序控制、起始/停止条件生成、数据收发、时钟同步等底层协议细节。
每个 I2C 控制器对应一个 struct i2c_adapter 实例,适配器内部封装了 struct i2c_algorithm,其中包含两个关键函数指针:
struct i2c_algorithm {
/* 标准 I2C 传输函数 */
int (*master_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs,
int num);
/* SMBus 传输函数(可选) */
int (*smbus_xfer)(struct i2c_adapter *adap,
u16 addr,
unsigned short flags,
char read_write,
u8 command,
int size,
union i2c_smbus_data *data);
/* 功能支持位掩码 */
u32 (*functionality)(struct i2c_adapter *adap);
};
3.3 I2C 设备驱动层 (I2C Device Driver)
设备驱动层是开发者最常接触的层次,它负责驱动具体的 I2C 外设芯片,如 EEPROM (AT24Cxx)、温度传感器 (LM75)、加速度计 (MPU6050) 等。设备驱动通过核心层提供的 API 与总线驱动层交互,无需关心底层时序细节。
一个标准的 I2C 设备驱动需要实现以下核心结构:
/* I2C 设备驱动结构体 */
static struct i2c_driver my_sensor_driver = {
.driver = {
.name = "my_sensor",
.of_match_table = my_sensor_of_match, /* 设备树匹配表 */
},
.probe = my_sensor_probe, /* 设备探测函数 */
.remove = my_sensor_remove, /* 设备移除函数 */
.id_table = my_sensor_id, /* 传统 ID 匹配表(备用) */
};
/* 设备树匹配表 */
static const struct of_device_id my_sensor_of_match[] = {
{ .compatible = "vendor,my-sensor", },
{ /* sentinel */ }
};
/* probe 函数:设备被发现时调用 */
static int my_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct my_sensor_data *data;
struct device *dev = &client->dev;
/* 1. 分配私有数据结构 */
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
/* 2. 初始化 I2C 通信 */
data->client = client;
i2c_set_clientdata(client, data);
/* 3. 读取设备 ID 确认通信正常 */
ret = i2c_smbus_read_byte_data(client, REG_DEVICE_ID);
if (ret < 0) {
dev_err(dev, "failed to read device ID\n");
return ret;
}
/* 4. 注册设备到子系统(如 IIO、HWMON、Input) */
return my_sensor_register_device(dev, data);
}
3.4 三层架构的协作流程
当用户空间发起一次 I2C 读操作时,数据流在三层架构中的完整路径如下:
- 用户空间调用:应用程序通过 /dev/i2c-N 设备节点发起 read() 系统调用,或通过 i2c-tools 的 i2cget 命令。
- 核心层转发:i2c-dev.c 中的 i2cdev_read() 将用户请求封装为 struct i2c_msg,调用 i2c_transfer() 进入核心层。
- 核心层调度:i2c-core.c 根据 adapter 找到对应的总线驱动,调用 adapter->algo->master_xfer()。
- 总线驱动执行:master_xfer() 操作 I2C 控制器寄存器,在 SCL/SDA 线上产生时序,完成数据收发。
- 数据返回:数据沿原路径返回:总线驱动 → 核心层 → 用户空间。
| 层次 | 主要文件 | 核心数据结构 | 开发者关注点 |
|---|---|---|---|
| 设备驱动层 | drivers/i2c/chips/ 或 drivers/misc/ | struct i2c_driver, struct i2c_client | 外设协议、probe/remove、用户接口 |
| 核心层 | drivers/i2c/i2c-core.c | struct i2c_bus_type | API 使用、设备树绑定、驱动匹配规则 |
| 总线驱动层 | drivers/i2c/busses/ | struct i2c_adapter, struct i2c_algorithm | 硬件寄存器、时钟配置、中断处理 |
1. 确认总线驱动层已正确注册 adapter(查看 /sys/class/i2c-adapter/)。
2. 使用 i2cdetect -l 列出所有 I2C 总线,确认硬件地址映射正确。
3. 使用 i2cdetect -y <bus> 扫描设备,验证外设是否应答。
4. 最后才调试设备驱动层的 probe 函数和通信逻辑。
3.5 设备树与三层架构的关联
设备树(Device Tree)是连接三层架构的关键纽带。在设备树中,I2C 控制器节点描述总线驱动层的硬件信息,而子节点描述设备驱动层的外设信息。核心层在系统启动时解析设备树,自动完成 i2c_adapter 和 i2c_client 的创建与匹配。
/* 设备树中的 I2C 节点示例 */
&i2c1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
clock-frequency = <100000>; /* 100kHz 标准模式 */
/* 子节点:I2C 设备 */
temperature-sensor@48 {
compatible = "national,lm75";
reg = <0x48>; /* 7 位 I2C 地址 */
status = "okay";
};
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
status = "okay";
};
};
设备树中的 compatible 属性与设备驱动中的 of_match_table 进行匹配,匹配成功后核心层调用驱动的 probe() 函数,完成设备初始化。这种机制使得驱动代码与硬件平台解耦,同一份驱动代码可以支持多个硬件平台。