在 Linux 内核中,I2C 子系统承担着处理与各类 I2C 外设交互的角色。它不仅是设备模型中的重要一环,也体现了总线驱动模型、设备树机制和字符设备接口的协同设计。本篇博文旨在全面系统地梳理 I2C 子系统的架构、注册流程、驱动模型、字符接口集成与调试方式,帮助你打通从设备树到驱动、从用户空间到内核空间的 I2C 工作机制理解闭环。
一、I2C 子系统的架构概览
Linux 的 I2C 子系统由以下三部分组成:
角色 | 对应结构体 | 描述 |
---|---|---|
I2C 主控 | struct i2c_adapter | 表示一个 I2C 控制器,通常由 SoC 提供并注册 |
I2C 从设备 | struct i2c_client | 表示挂载在 I2C 总线上的一个外设 |
I2C 驱动 | struct i2c_driver | 对某类外设进行管理的驱动,实现 probe/remove |
核心流程图:
Device Tree (.dts)
└──▶ of_i2c_register_devices()
└──▶ i2c_new_device()
└──▶ 匹配 i2c_driver(.of_match_table)
└──▶ 调用 probe()
这些流程涉及了 Linux 内核的设备模型(device model)中的总线 - 设备 - 驱动三元组概念。其中 i2c_client
是设备(device),i2c_driver
是驱动(driver),i2c_adapter
是总线(bus),三者之间的关联由 i2c-core
统一管理。
二、设备注册过程详解(以 Device Tree 为例)
在现代设备树体系中,I2C 外设的注册通常由如下方式实现:
&i2c1 {
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <16>;
};
};
上面配置中的 eeprom@50
节点会在 I2C 控制器驱动中被解析,过程如下:
- I2C 控制器驱动初始化时,调用
of_i2c_register_devices()
- 遍历子节点(如
eeprom@50
),构建i2c_client
实例 - 调用
i2c_new_device()
,注册到i2c-core
i2c-core
会对所有已注册的i2c_driver
进行匹配- 匹配成功后,调用
probe()
或probe_new()
完成初始化
此流程关键在于设备树中 compatible
字段的匹配。
三、I2C 驱动模型特性
3.1 基本结构
static struct i2c_driver at24_driver = {
.driver = {
.name = "at24",
.of_match_table = at24_of_match,
},
.probe_new = at24_probe,
.remove = at24_remove,
};
驱动通过 i2c_add_driver()
注册,在模块加载时由 i2c-core
自动匹配并调用 probe。
3.2 probe_new vs probe
.probe()
是老接口,参数需手动转换为struct i2c_client *
.probe_new()
是新接口,更类型安全,推荐使用
3.3 匹配方式
of_match_table
:基于设备树的 compatible 字符串id_table
:适用于非 DT 场景,如平台代码中i2c_register_board_info()
四、字符设备接口的集成
某些 I2C 设备,如 EEPROM,需要被用户空间直接访问,这时可以结合字符设备(cdev
)提供接口。
static struct file_operations at24_fops = {
.owner = THIS_MODULE,
.read = at24_read,
.write = at24_write,
.llseek = default_llseek,
};
注册流程如下:
alloc_chrdev_region()
分配主设备号cdev_init()
+cdev_add()
注册设备class_create()
+device_create()
生成/dev/at24char
结合 i2c_smbus_*
或 i2c_transfer
在 read/write 中读写 EEPROM 内容,实现完整的数据通路。
五、I2C 与字符设备的区别与协作
对比项 | I2C 子系统 | 字符设备接口 |
---|---|---|
本质 | 设备模型中的“设备” | 用户接口模型中的“文件” |
注册方式 | i2c_client + i2c_driver | register_chrdev + cdev_add |
面向对象 | 内核驱动(probe),与总线模型紧密集成 | 面向用户空间,提供 read/write API |
用户使用 | 通过 sysfs/nvmem/自定义 ioctl 等接口 | 直接通过 /dev/xxx 文件操作 |
典型应用 | 温度传感器、电源芯片、EEPROM | SPI Flash、摄像头等字节访问型设备 |
小结: I2C 子系统提供“设备管理”,字符设备提供“用户入口”,两者结合实现从内核到底层外设的完整交互。
六、调试与验证流程
6.1 内核侧调试
-
查看 I2C 总线设备列表:
ls /sys/class/i2c-adapter/
-
检查设备注册过程:
dmesg | grep i2c
-
检查驱动匹配过程:
dmesg | grep probe
-
查看字符设备节点:
ls -l /dev/at24char
6.2 用户态工具验证
-
总线扫描:
i2cdetect -y 2
-
写入数据:
i2cset -y 2 0x50 0x00 0x12
-
读取数据:
i2cget -y 2 0x50 0x00
-
多字节读写建议结合小工具或字符驱动封装测试
七、十大关键问题深度剖析
1. 什么是 i2c_adapter
和 i2c_client
?
i2c_adapter
表示主控接口,i2c_client
表示一个挂载在主控下的具体设备(如 EEPROM)。一个适配器可管理多个 client。
2. 设备如何从设备树注册进内核?
通过 of_i2c_register_devices()
递归解析设备树节点,将其转换为 i2c_client
,随后与 i2c_driver
进行匹配。
3. I2C 驱动如何匹配设备?
依据设备的 compatible
字段,与驱动中 of_device_id
表中的字符串匹配,成功后调用 probe_new()
。
4. probe/probe_new 有何区别?
probe_new()
类型更严格,不需要手动转换为i2c_client *
probe()
更兼容老内核,但已不推荐使用
5. 所有 I2C 设备都需要字符设备吗?
不是。部分设备通过 sysfs 或专有框架(如 PMIC、RTC)间接提供访问,不需要字符接口。
6. 如何在驱动中读写 I2C 外设?
常用 API:
i2c_smbus_read_byte_data()
/i2c_smbus_write_byte_data()
i2c_transfer()
可组合多个i2c_msg
进行复杂交互
7. 驱动中如何保存上下文?
使用 dev_set_drvdata(&client->dev, priv)
设置,后续可通过 dev_get_drvdata()
获取私有结构。
8. I2C 驱动与 platform 驱动的区别?
- 前者由
i2c-core
驱动管理,面向挂载在 I2C 总线的设备 - 后者由
platform_bus
管理,面向片上资源
9. probe 匹配失败怎么查?
- 查看
dmesg
- 确认设备树 compatible 是否写错
- 驱动是否注册成功,of_match_table 是否生效
10. 字符设备如何对接 I2C 外设?
在 probe
中注册字符设备,read/write
中通过 i2c_smbus_read/write
与 I2C 外设交互,即可实现用户态读写
八、实践建议与总结
掌握 I2C 子系统的本质,是理解 Linux 驱动模型不可或缺的一环。建议从如下角度出发深入:
- 动手写一个 I2C + 字符设备混合驱动(如 at24)
- 调试一个真实硬件 I2C 外设,比如使用
i2c-tools
验证写入后再读取 - 通过
of_i2c_register_devices()
源码追踪理解 client 注册机制 - 对比 SPI/Platform 子系统,理解总线驱动模型的一致性
最终,你应能清晰回答:“设备是怎么被识别的?驱动是怎么匹配的?用户程序是怎么访问的?”
✅ 附加资源
如有需要,我可以继续补充以下内容:
- 思维导图:概览 I2C 子系统的组成和注册流程
- at24.c 驱动解析:逐函数讲解字符接口集成流程
- FAQ 问答:归纳常见 I2C 调试陷阱