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

如果从一次访问链路来看,大概就是:
text
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 平台相关驱动路径一般类似:
text
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 子系统的核心层。
源码路径一般在:
text
kernel-6.1/drivers/i2c/i2c-core.c
kernel-6.1/drivers/i2c/i2c-core-base.c
这一层可以理解为 I2C 子系统的"调度中心"。
它不直接关心你下面是 RK3576、全志 T527、STM32,还是其他平台。
它关心的是:
- 系统里有哪些 I2C 控制器?
- 有哪些 I2C 设备?
- 有哪些 I2C 驱动?
- 设备和驱动怎么匹配?
- 上层要传输数据时,应该调用哪个控制器?
所以 i2c-core 的作用非常关键。
它向上提供统一接口,向下调用具体控制器驱动。
常见接口包括:
c
i2c_transfer()
i2c_smbus_xfer()
i2c_master_send()
i2c_master_recv()
对于设备驱动来说,不需要知道底层 I2C 控制器寄存器怎么配置,只需要调用这些通用接口即可。
比如:
c
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;
- 设备名称;
- 设备节点信息;
- 驱动私有数据。
驱动里经常会看到:
c
static int xxx_probe(struct i2c_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 外设写的驱动。
比如:
- 触摸芯片驱动:gt911
- RTC 驱动:rtc-pcf8563.c
- 温湿度传感器驱动:aht20
- 电源管理芯片驱动:pmic
这一层的职责不是直接控制 SDA / SCL,而是实现具体设备功能。
比如一个触摸驱动,它关心的是:
- 怎么读取触摸坐标;
- 怎么初始化芯片;
- 怎么处理中断;
- 怎么上报 input 事件;
- 怎么管理电源。
它不需要关心 RK3576 的 I2C 控制器寄存器怎么配置。
它只需要通过 i2c-core 提供的接口访问设备寄存器,比如:
c
i2c_smbus_read_byte_data()
i2c_smbus_write_byte_data()
i2c_transfer()
这样带来的好处很明显:
同一个 I2C 外设驱动,可以尽量复用在不同 SoC 平台上。
比如某个 I2C 触摸芯片驱动,在 RK 平台能用,在其他 ARM 平台上也可能能用。只要底层 I2C 控制器驱动正常,设备树配置正确,上层设备驱动通常不用关心底层硬件差异。
这就是 Linux 子系统分层带来的好处。
6. 第四层:用户空间接口层 i2c-dev
除了写内核设备驱动,Linux 还提供了一个用户空间访问 I2C 的方式。
对应文件是:
text
kernel-6.1/drivers/i2c/i2c-dev.c
这个文件实现了一个字符设备驱动,会为每个 I2C adapter 创建对应的设备节点:
bash
/dev/i2c-0
/dev/i2c-1
/dev/i2c-2
...
比如 RK3576 的 I2C2,用户空间可能就能看到:
bash
/dev/i2c-2
有了这个节点,用户空间程序或者 i2c-tools 就可以直接访问 I2C 总线。
常用工具包括:
text
i2cdetect
i2cget
i2cset
i2cdump
比如扫描 I2C2 总线:
bash
i2cdetect -y 2
读取某个设备寄存器:
bash
i2cget -y 2 0x38 0x00
这里的调用链路大概是:
text
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 驱动的匹配表里包含:
c
{ .compatible = "aosong,aht20" }
那么设备和驱动就能匹配成功,随后调用驱动的 probe() 函数。
简化流程如下:
text
设备树描述 I2C 外设
↓
内核创建 i2c_client
↓
I2C 驱动注册 i2c_driver
↓
i2c-core 完成匹配
↓
调用驱动 probe()
↓
驱动初始化外设
这就是 Linux "总线-设备-驱动"模型在 I2C 子系统里的体现。
8. 一次完整读取链路
下面以 RK3576 通过 I2C2 读取 AHT20 温湿度传感器为例,简单看一次完整链路。
假设用户空间执行:
bash
i2cget -y 2 0x38 0x00
大致流程如下:
- 用户空间执行 i2cget;
- i2cget 打开 /dev/i2c-2;
- 通过 ioctl 设置从设备地址 0x38;
- 通过 read/write 或 I2C_RDWR 发起访问;
- i2c-dev.c 接收用户请求;
- i2c-dev.c 调用 i2c-core 的传输接口;
- i2c-core 封装 i2c_msg,调用 i2c_transfer;
- i2c_transfer 找到 I2C2 对应的 i2c_adapter;
- 调用 adapter 的 master_xfer;
- 进入 RK 平台 i2c-rk3x.c;
- rk3x_i2c_xfer 操作硬件寄存器;
- RK3576 I2C2 控制器在 SDA/SCL 上产生时序;
- AHT20 返回数据;
- 数据沿原路径返回给 i2cget。
这条链路看起来长,但理解之后就很清晰。
一句话总结就是:
用户层负责发起请求,i2c-dev 负责接入用户空间,i2c-core 负责调度,控制器驱动负责干硬件活,外设负责响应数据。
9. 总结
Linux I2C 子系统看起来复杂,但拆开后其实就是几层分工。
i2c_adapter:代表 I2C 控制器;i2c_client:代表 I2C 外设;i2c_driver:代表 I2C 设备驱动;i2c-core:负责把它们组织起来;i2c-dev:负责给用户空间提供调试入口。
刚开始学习时,不建议直接扎进源码里逐行看。
更建议先建立这条主线:
text
设备树描述外设
↓
内核创建 i2c_client
↓
驱动注册 i2c_driver
↓
i2c-core 完成匹配并调用 probe
↓
设备驱动调用 i2c_transfer / smbus API
↓
控制器驱动 master_xfer 产生硬件时序
理解了这条线,再去看 i2c-rk3x.c、i2c-core-base.c、i2c-dev.c,就不会像刚开始那样满屏函数乱飞了。
当然,本文只是先建立 I2C 子系统的整体认知,具体源码细节后面再慢慢拆。
(完)