驱动之路#43:一文理清 I2C 子系统架构

本系列分享的是我当初学习 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

大致流程如下:

  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:负责给用户空间提供调试入口。

刚开始学习时,不建议直接扎进源码里逐行看。

更建议先建立这条主线:

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 子系统的整体认知,具体源码细节后面再慢慢拆。

(完)

相关推荐
HRTOS3 小时前
8051 单片机 RTOS 开发的难点在哪里?从任务切换、中断管理到资源控制
单片机·51单片机·嵌入式·8051·hrtos
Y Shy13 小时前
2026.10 在受限的嵌入式机器人设备中使用LLM Agent
机器人·llm·嵌入式·agent
用户7661660031118 小时前
先中转、后打洞:libnpf 的 NAT 穿透与对称型 NAT 实战
嵌入式
优信电子18 小时前
HC05蓝牙模块主机与从机进行连接通信
单片机·物联网·嵌入式
K成长日志18 小时前
BLE Host层L2CAP -- 配置参数选项
物联网·网络协议·嵌入式·蓝牙·iot·ble·bluetooth
en.en..3 天前
第一篇:位域打包:看懂 I2C 地址左移的底层逻辑
stm32·嵌入式·位运算·i2c·底层驱动
youcans_3 天前
【嵌入式软件AI编程】21. 整理通用提示词模板
stm32·单片机·嵌入式·ai编程
番茄灭世神3 天前
PN学堂GD32课程第15篇——HardFault异常
c语言·单片机·嵌入式
LCG米3 天前
STM32串口接收卡死根因排查:ORE溢出标志陷阱、错误标志清除顺序与中断恢复实测
stm32·单片机·嵌入式硬件·串口·嵌入式