前面几篇文章,我们已经简单聊过 I2C 通信机制、I2C 为什么要上拉,以及 I2C 和 SMBus 的关系。
到这里,如果只是理解 I2C 协议本身,基本已经够入门了。
但真正进入 Linux 驱动开发时,又会遇到一个新问题:
Linux 内核里的 I2C 子系统到底是怎么组织的?
比如我们经常会看到这些东西:
/dev/i2c-2i2c_transfer()i2c_smbus_read_byte_data()structi2c_clientstructi2c_driverstructi2c_adapterdrivers/i2c/busses/i2c-rk3x.cdrivers/i2c/i2c-dev.c
刚开始看时,确实容易一头雾水。
这些东西到底谁管谁?
应用层怎么访问 I2C?
设备驱动怎么和设备树匹配?
最后又是谁真正去控制 SDA / SCL 产生时序?
这篇文章就以 RK3576 平台为例,从整体架构角度,先把 Linux I2C 子系统的大框架捋一遍。
不追求一上来逐行啃源码,先建立索引。后面真正看驱动时,知道每一层大概在干什么,就不会迷路。
1. 先说结论
Linux I2C 子系统可以简单理解成四层:

如果从一次访问链路来看,大概就是:
APP/ i2ctools↓i2c-dev.c 或具体 I2C 设备驱动↓i2c-core↓I2C 控制器驱动↓RK3576 I2C 控制器↓SDA / SCL↓外设
数据返回时,再沿着这条链路反向返回。
所以 Linux I2C 子系统的核心设计思想,其实就是一句话:
把“控制器硬件操作”和“外设功能逻辑”拆开,让不同平台、不同设备都能复用同一套框架。
这也是 Linux 驱动子系统非常典型的设计方式。
2. 第1层:I2C 控制器驱动层
先从最底层说起。
I2C 控制器驱动层,负责对接芯片内部真实存在的 I2C 控制器。
以 RK3576 为例,SoC 内部会有多路 I2C 控制器,比如 I2C0、I2C1、I2C2 等。
这些控制器最终负责控制 SDA / SCL,在总线上产生 Start、Stop、ACK、地址、数据等时序。
RK 平台相关驱动路径一般类似:
kernel-6.1/drivers/i2c/busses/i2c-rk3x.c

这一层可以理解成:
I2C子系统和 RK3576 I2C 硬件之间的“翻译官”。上层只会告诉它:我要给 0x38 这个设备发一段数据;我要从 0x5d 这个设备读几个字节;我要完成一组 i2c_msg 传输。
而控制器驱动要做的,就是把这些抽象请求,转换成硬件寄存器操作,最终在 SDA / SCL 上产生真实波形。
这一层主要负责:
[ ]初始化 I2C 控制器;[ ]配置时钟和通信速率;[ ]处理中断或 DMA;[ ]实现 master_xfer 传输函数;[ ]处理 ACK 失败、超时、仲裁丢失等异常;[ ]操作硬件寄存器产生 I2C 总线时序。
比如在 RK3576 上,最终真正和硬件寄存器打交道的,就是这一层。
3. 第2层:I2C 核心层 i2c-core
控制器驱动之上,就是 I2C 子系统的核心层。
源码路径一般在:
kernel-6.1/drivers/i2c/i2c-core.ckernel-6.1/drivers/i2c/i2c-core-base.c
这一层可以理解为 I2C 子系统的“调度中心”。
它不直接关心你下面是 RK3576、全志 T527、STM32,还是其他平台。
它关心的是:
系统里有哪些 I2C 控制器?有哪些 I2C 设备?有哪些 I2C 驱动?设备和驱动怎么匹配?上层要传输数据时,应该调用哪个控制器?
所以 i2c-core 的作用非常关键。
它向上提供统一接口,向下调用具体控制器驱动。
常见接口包括:
i2c_transfer()i2c_smbus_xfer()i2c_master_send()i2c_master_recv()
对于设备驱动来说,不需要知道底层 I2C 控制器寄存器怎么配置,只需要调用这些通用接口即可。
比如:
ret= i2c_transfer(client->adapter, msgs, num);
至于这个adapter最后对应 RK3576 的哪个 I2C 控制器,就由 i2c-core 和控制器驱动去处理。
4. I2C 子系统里的三个关键结构体
看 I2C 子系统,绕不开三个结构体。它们就是整个框架的骨架。
(1)struct i2c_adapter
i2c_adapter表示一个 I2C 控制器。
比如 RK3576 的 I2C2 控制器,在内核里就会注册成一个i2c_adapter。
可以简单理解为:
一个 i2c_adapter = 一条 I2C 总线 / 一个 I2C 控制器
它里面会包含:
控制器编号;控制器名称;支持的功能;传输函数;所属设备信息。
最关键的是传输函数,比如master_xfer。
上层最终调用i2c_transfer()时,最后会走到这个 adapter 对应的master_xfer,再进入具体平台的控制器驱动。
所以,i2c_adapter代表的是“谁来发起 I2C 传输”。
(2)struct i2c_client
i2c_client表示一个 I2C 从设备。
比如 I2C2 总线上挂了一个地址为0x5d的 GT911 触摸芯片,那么这个 GT911 在内核里就会对应一个i2c_client。
可以简单理解为:
一个 i2c_client = 一个挂在 I2C 总线上的外设
它里面通常包含:
设备地址;所属 i2c_adapter;设备名称;设备节点信息;驱动私有数据。
驱动里经常会看到:
staticintxxx_probe(structi2c_client *client)
这个client就代表当前匹配到的 I2C 设备。
驱动后续读写寄存器时,也通常是通过这个client找到设备地址和所属 adapter。
(3)struct i2c_driver
i2c_driver表示一个 I2C 设备驱动。
比如 GT911 触摸驱动、RTC 驱动、温度传感器驱动,都可以注册为一个i2c_driver。
它里面通常包含:
probe 函数;remove函数;设备匹配表;驱动名称;电源管理回调。
当设备和驱动匹配成功后,内核就会调用驱动的probe()函数。
所以这三个结构体可以这样理解:
i2c_adapter:代表 I2C 控制器i2c_client :代表 I2C 外设i2c_driver :代表 I2C 外设驱动
这三个概念搞清楚,I2C 子系统的大框架基本就立起来了。
5. 第三层:I2C 设备驱动层
设备驱动层,就是针对具体 I2C 外设写的驱动。
比如:
触摸芯片驱动:gt911RTC 驱动:rtc-pcf8563.c温湿度传感器驱动:aht20电源管理芯片驱动:pmic
这一层的职责不是直接控制 SDA / SCL,而是实现具体设备功能。
比如一个触摸驱动,它关心的是:
怎么读取触摸坐标;怎么初始化芯片;怎么处理中断;怎么上报input事件;怎么管理电源。
它不需要关心 RK3576 的 I2C 控制器寄存器怎么配置。
它只需要通过 i2c-core 提供的接口访问设备寄存器,比如:
i2c_smbus_read_byte_data()i2c_smbus_write_byte_data()i2c_transfer()
这样带来的好处很明显:
同一个 I2C 外设驱动,可以尽量复用在不同 SoC 平台上。
比如某个 I2C 触摸芯片驱动,在 RK 平台能用,在其他 ARM 平台上也可能能用。只要底层 I2C 控制器驱动正常,设备树配置正确,上层设备驱动通常不用关心底层硬件差异。
这就是 Linux 子系统分层带来的好处。
6. 第四层:用户空间接口层 i2c-dev
除了写内核设备驱动,Linux 还提供了一个用户空间访问 I2C 的方式。
对应文件是:
kernel-6.1/drivers/i2c/i2c-dev.c
这个文件实现了一个字符设备驱动,会为每个 I2C adapter 创建对应的设备节点:
/dev/i2c-0/dev/i2c-1/dev/i2c-2...
比如 RK3576 的 I2C2,用户空间可能就能看到:
/dev/i2c-2
有了这个节点,用户空间程序或者 i2c-tools 就可以直接访问 I2C 总线。
常用工具包括:
i2cdetecti2cgeti2cseti2cdump
比如扫描 I2C2 总线:
i2cdetect-y2
读取某个设备寄存器:
i2cget-y20x380x00
这里的调用链路大概是:
i2cget↓/dev/i2c-2↓i2c-dev.c↓i2c-core↓i2c-rk3x.c↓RK3576 I2C2 控制器↓I2C 外设
所以i2c-dev.c的作用可以理解为:
给用户空间开了一个访问 I2C 总线的调试入口。
它非常适合调试阶段验证硬件。
比如我们想确认设备地址对不对、外设有没有 ACK、某个寄存器能不能读到,就可以先用 i2c-tools 测一下。
但如果是正式产品中的复杂设备功能,通常还是建议写内核驱动,而不是长期依赖用户空间直接操作 I2C。
7. 设备和驱动是怎么匹配的?
理解 I2C 子系统,除了看数据传输,还要理解设备和驱动怎么绑定。
在设备树里,我们可能会这样写:

这里表示:
AHT20挂在 I2C2 总线上;设备地址是 0x38;compatible 是"aosong,aht20"。
内核启动时,会解析设备树,在 I2C2 这个 adapter 下创建一个对应的i2c_client。
如果内核中有一个 I2C 驱动的匹配表里包含:
{ .compatible="aosong,aht20"}
那么设备和驱动就能匹配成功,随后调用驱动的probe()函数。
简化流程如下:
设备树描述 I2C 外设↓内核创建 i2c_client↓I2C 驱动注册 i2c_driver↓i2c-core 完成匹配↓调用驱动 probe()↓驱动初始化外设
这就是 Linux “总线-设备-驱动”模型在 I2C 子系统里的体现。
8. 一次完整读取链路
下面以 RK3576 通过 I2C2 读取 AHT20 温湿度传感器为例,简单看一次完整链路。
假设用户空间执行:
i2cget-y20x380x00
大致流程如下:
1.用户空间执行 i2cget;2.i2cget 打开 /dev/i2c-2;3.通过 ioctl 设置从设备地址 0x38;4.通过 read/write 或 I2C_RDWR 发起访问;5. i2c-dev.c 接收用户请求;6. i2c-dev.c 调用 i2c-core 的传输接口;7. i2c-core 封装 i2c_msg,调用 i2c_transfer;8. i2c_transfer 找到 I2C2 对应的 i2c_adapter;9. 调用 adapter 的 master_xfer;10.进入 RK 平台 i2c-rk3x.c;11.rk3x_i2c_xfer 操作硬件寄存器;12.RK3576 I2C2 控制器在 SDA/SCL 上产生时序;13.AHT20 返回数据;14.数据沿原路径返回给 i2cget。
这条链路看起来长,但理解之后就很清晰。
一句话总结就是:
用户层负责发起请求,i2c-dev 负责接入用户空间,i2c-core 负责调度,控制器驱动负责干硬件活,外设负责响应数据。
9. 总结
Linux I2C 子系统看起来复杂,但拆开后其实就是几层分工。
i2c_adapter:代表 I2C 控制器;i2c_client :代表 I2C 外设;i2c_driver :代表 I2C 设备驱动;i2c-core :负责把它们组织起来;i2c-dev :负责给用户空间提供调试入口。
刚开始学习时,不建议直接扎进源码里逐行看。
更建议先建立这条主线:
设备树描述外设↓内核创建 i2c_client↓驱动注册 i2c_driver↓i2c-core 完成匹配并调用 probe↓设备驱动调用 i2c_transfer / smbus API↓控制器驱动 master_xfer 产生硬件时序
理解了这条线,再去看i2c-rk3x.c、i2c-core-base.c、i2c-dev.c,就不会像刚开始那样满屏函数乱飞了。
当然,本文只是先建立 I2C 子系统的整体认知,具体源码细节后面再慢慢拆。
(完)
下期可以继续聊:硬件 I2C 和软件 I2C 谁更坑?
本人专注Linux 嵌入式全栈开发,有项目合作 / 技术支持 / 交个朋友,欢迎后台私信。
-
I2C
+关注
关注
28文章
1570浏览量
132199 -
I2C协议
+关注
关注
0文章
31浏览量
9375
发布评论请先 登录
linux I2C子系统的相关资料分享
需要了解Linux驱动子系统之一的I2C
一文理清EMI的传播过程资料下载
linux I2C子系统(及相关程序设计MPU6050)
嵌入式内核及驱动开发-09IIC子系统框架使用(I2C协议和时序,I2C驱动框架,I2C从设备驱动开发,MPU6050硬件连接
驱动之路#43:一文理清I2C子系统架构
评论