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

描述

 

前面几篇文章,我们已经简单聊过 I2C 通信机制、I2C 为什么要上拉,以及 I2C 和 SMBus 的关系。

到这里,如果只是理解 I2C 协议本身,基本已经够入门了。

但真正进入 Linux 驱动开发时,又会遇到一个新问题:

Linux 内核里的 I2C 子系统到底是怎么组织的?

比如我们经常会看到这些东西:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
/dev/i2c-2i2c_transfer()i2c_smbus_read_byte_data()struct i2c_clientstruct i2c_driverstruct i2c_adapterdrivers/i2c/busses/i2c-rk3x.cdrivers/i2c/i2c-dev.c

刚开始看时,确实容易一头雾水。

这些东西到底谁管谁?
应用层怎么访问 I2C?
设备驱动怎么和设备树匹配?
最后又是谁真正去控制 SDA / SCL 产生时序?

这篇文章就以 RK3576 平台为例,从整体架构角度,先把 Linux I2C 子系统的大框架捋一遍。

不追求一上来逐行啃源码,先建立索引。后面真正看驱动时,知道每一层大概在干什么,就不会迷路。

 

1. 先说结论

Linux I2C 子系统可以简单理解成四层:

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

这一层可以理解成:

  •  
  •  
  •  
  •  
  •  
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;设备名称;设备节点信息;驱动私有数据。

驱动里经常会看到:

  •  
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 外设写的驱动。

比如:

  •  
  •  
  •  
  •  
触摸芯片驱动: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 -y 2

读取某个设备寄存器:

  •  
i2cget -y 2 0x38 0x00

这里的调用链路大概是:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
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 子系统,除了看数据传输,还要理解设备和驱动怎么绑定。

在设备树里,我们可能会这样写:

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

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

更建议先建立这条主线:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
设备树描述外设    ↓内核创建 i2c_client    ↓驱动注册 i2c_driver    ↓i2c-core 完成匹配并调用 probe    ↓设备驱动调用 i2c_transfer / smbus API    ↓控制器驱动 master_xfer 产生硬件时序

理解了这条线,再去看 i2c-rk3x.ci2c-core-base.ci2c-dev.c,就不会像刚开始那样满屏函数乱飞了。

当然,本文只是先建立 I2C 子系统的整体认知,具体源码细节后面再慢慢拆。

(完)

下期可以继续聊:硬件 I2C 和软件 I2C 谁更坑?


本人专注 Linux 嵌入式全栈开发,有项目合作 / 技术支持 / 交个朋友,欢迎后台私信。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分