LV08-01-I2C子系统-03-I2C驱动框架简介

了解下I2C的驱动框架。若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、I2C驱动框架简介

1. 之前怎么驱动I2C设备的?

看一下之前学习单片机的时候学习I2C驱动24C02的代码,我们一共编写了以下几个文件:

image-20250326092923255这些文件是通过I2C接口,通过I2C实现对24C02的寄存器读写操作的。

image-20250326093017228

这些文件是实现I2C时序的,不过这里是使用GPIO模拟的I2C,主要是实现了I2C的起始信号、结束信号、发送数据、接收数据的一些逻辑。

2. Linux中的I2C驱动?

前面的例子中,其中myiic.c IIC 接口驱动, 24cxx.c 这个 是24c02这个 I2C 设备驱动文件。相当于有两部分驱动:

  • ①、 I2C 主机驱动——完成I2C的时序,像起始信号、结束信号、等待应答、读写数据等。

  • ②、 I2C 设备驱动——完成对I2C设备寄存器的读写,完成I2C设备的寄存器配置和数据读写。

对于 I2C 主机驱动,一旦编写完成就不需要再做修改,其他的 I2C 设备直接调用主机驱动提供的 API 函数完成读写操作即可。这个正好符合 Linux 的驱动分离与分层的思想,因此 Linux内核也将 I2C 驱动分为两部分:

  • ①、 I2C 总线驱动, I2C 总线驱动就是 SOC 的 I2C 控制器驱动,也叫做 I2C 适配器驱动。
  • ②、 I2C 设备驱动, I2C 设备驱动就是针对具体的 I2C 设备而编写的驱动。

这也是一个总线-驱动-设备模型,如下图所示,i2c驱动框架包括i2c总线驱动、具体某个设备的驱动。

3|

i2c总线包括i2c设备(i2c_client)和i2c驱动(i2c_driver), 当我们向linux中注册设备或驱动的时候,按照i2c总线匹配规则进行配对,配对成功,则可以通过i2c_driver中.prob函数创建具体的设备驱动。

在现代linux中,i2c设备不再需要手动创建,而是使用设备树机制引入,设备树节点是与paltform总线相配合使用的。 所以需先对i2c总线包装一层paltform总线,当设备树节点转换为平台总线设备时,我们在进一步将其转换为i2c设备,注册到i2c总线中。

设备驱动创建成功,我们还需要实现设备的文件操作接口(file_operations),file_operations中会使用到内核中i2c核心函数(i2c系统已经实现的函数,专门开放给驱动工程师使用)。 使用这些函数会涉及到i2c适配器,也就是i2c控制器。由于ic2控制器有不同的配置,所有linux将每一个i2c控制器抽象成i2c适配器对象。 这个对象中存在一个很重要的成员变量——Algorithm,Algorithm中存在一系列函数指针,这些函数指针指向真正硬件操作代码。

3. I2C驱动框架中的层级

这里和这里的笔记是一样的,写在这里只是为了方便查阅:LV10-11-I2C驱动-01-Linux中的I2C驱动 | 苏木

在 linux 内核中, I2C 驱动如下图所示:

img

用户空间的就是用户访问 I2C 设备的一些接口,我们目前不用关心这个。接下来就来看一看各个层都是做什么的吧。

3.1 I2C设备驱动层——driver驱动层和I2C核心层

  • I2C 从设备驱动层——driver驱动层

就是上图中的 driver 驱动层,也简称为 I2C 设备驱动层,里边都是一些挂在 I2C 上的设备的驱动程序。这里边的 driver 和 client 都是需要我们自己来实现的。

实现i2c设备驱动中的i2c_driver接口,用具体的i2c device设备的attach_adapter()、detach_adapter()方法赋值给i2c_driver的成员函数指针。实现设备device与总线(或者叫adapter)的挂接。

实现i2c设备所对应的具体device的驱动,i2c_driver只是实现设备与总线的挂接,而挂接在总线上的设备则是千差万别的,所以要实现具体设备device的write()、read()、ioctl()等方法,赋值给file_operations,然后注册字符设备(多数是字符设备)。

这一层是挂载在I2C总线上的二级外设的驱动,也称客户(client)驱动,实现对二级外设的各种操作,二级外设的几乎所有操作全部依赖于对其自身内部寄存器的读写,对这些二级外设寄存器的读写又依赖于I2C总线的发送和接收。

  • I2C 核心层

主要是承上启下,为 I2C 从设备驱动和 I2C 总线驱动开发提供接口,为 I2C 设备驱动层提供管理多个 i2c_driver 、 i2c_client 对象的数据结构,为 I2C 总线驱动层提供多个 i2c_algorithm 、 i2c_adapter 对象的数据结构。完成它们之间的匹配。

这一层会用具体适配器的xxx_xferf()函数来填充i2c_algorithm的master_xfer函数指针,并把赋值后的i2c_algorithm再赋值给i2c_adapter的algo指针。

3.2 I2C 总线驱动层——抽象层和硬件实现控制层

包括上图中的访问抽象层和硬件实现控制层,这一层是 I2C 总线自身控制器的驱动程序,每个 I2C 适配器都会有自己的驱动程序。一般 SOC 芯片都会提供多个 I2C 总线控制器,也叫I2C适配器( I2C Adaptor),每个 I2C 总线控制器提供一组 I2C 总线,每一组 I2C 总线被称为一个 I2C通道,这个通道会提供一组 SCL 时钟线和 SDA 数据线,在这两根线上会挂载不同的 I2C 设备。

Linux 内核里将 I2C 总线控制器叫做适配器( adapter ),适配器驱动主要工作就是提供通过本组 I2C 总线与二级外设进行数据传输的接口,每个二级外设驱动里必须能够获得其对应的 adapter 对象才能实现数据传输。

这一层的驱动会由 SOC 开发商在第一次移植的时候完成编写,一般我们是不需要编写的。这一层的驱动也会分两个模块编写,一个是 I2C_Adapter 模块,就很类似于 device 模块(因为总线控制器也就相当于一个设备),另一个是 Algorithm 算法模块,在这里会实现 I2C 数据的传输。

3.3 总结

在linux驱动架构中,几乎不需要驱动开发人员再添加bus,因为linux内核几乎集成所有总线bus,如usb、pci、i2c等等。并且总线bus中的(与特定硬件相关的代码)已由芯片提供商编写完成,例如三星的s3c-2440平台i2c总线bus为i2c-s3c2410.c - drivers/i2c/busses/i2c-s3c2410.c。而上面的设备驱动层就需要驱动工程师来实现了。

我们在进行 I2C 驱动编写的时候,需要编写两个内核模块,一个是 driver 模块,另一个是 client 模块(表示从设备),类比 platform 总线的话,这里的 Client 模块就相当于 platform 总线中的 device 设备模块。

  • I2C 的 driver 模块:提供二级外设(也就是从设备)驱动程序的逻辑代码。
  • I2C 的 client 模块:提供二级外设的一些资源信息。

4. 四个对象

经过上边的分析,在一个 I2C 驱动中,会有四个模块: client 、 driver 、 I2C_Adapter 和 Algorithm 这四个模块,其中 client 和 I2C_Adapter 就相当于设备模块, driver 和 Algorithm 是驱动模块。他们的关系如下:

image-20250326114451096
  • i2c_client:提供i2c设备的资源信息,在linuc内核中对应的结构体为 struct i2c_client
  • i2c_driver:提供i2c设备的驱动,在linuc内核中对应的结构体为struct i2c_driver
  • i2c_adapter:提供i2c适配器的驱动,在linuc内核中对应的结构体为 struct i2c_adapter
  • i2c_algorithm:提供i2c的通信方法,在linuc内核中对应的结构体为 struct i2c_algorithm

4.1 i2c_adapter与i2c_algorithm

i2c_adapter对应与物理上的一个适配器,而i2c_algorithm对应一套通信方法,一个i2c适配器需要i2c_algorithm中提供的(i2c_algorithm中的又是更下层与硬件相关的代码提供)通信函数来控制适配器上产生特定的访问周期。缺少i2c_algorithm的i2c_adapter什么也做不了,因此i2c_adapter中包含其使用i2c_algorithm的指针。

i2c_algorithm中的关键函数**master_xfer()**用于产生i2c访问周期需要的start stop ack信号,以i2c_msg(即i2c消息)为单位发送和接收通信数据。i2c_msg也非常关键,调用驱动中的发送接收函数需要填充该结构体。

4.2 i2c_driver和i2c_client

  • i2c_driver对应一套驱动方法,其主要函数是attach_adapter()和detach_client()

  • i2c_client对应真实的i2c物理设备device,每个i2c设备都需要一个i2c_client来描述。

i2c_driver与i2c_client的关系是一对多。一个i2c_driver上可以支持多个同等类型的i2c_client。

4.3 i2c_adapter和i2c_client

i2c_adapter和i2c_client的关系与i2c硬件体系中适配器和设备的关系一致,即i2c_client依附于i2c_adapter,由于一个适配器上可以连接多个i2c设备,所以i2c_adapter中包含依附于它的i2c_client的链表。

从i2c驱动架构图中可以看出,linux内核对i2c架构抽象了一个叫核心层core的中间件,它分离了设备驱动device driver和硬件控制的实现细节(如操作i2c的寄存器),core层不但为上面的设备驱动提供封装后的内核注册函数,而且还为小面的硬件事件提供注册接口(也就是i2c总线注册接口),可以说core层起到了承上启下的作用。

4.4 总结

  • i2c_adapter对象实现了一组通过一个i2c控制器发送消息的所有信息, 包括时序, 地址等等, 即封装了i2c控制器的”控制信息”。它被i2c主机驱动创建, 通过clien域和i2c_client和i2c_driver相连, 这样设备端驱动就可以通过其中的方法以及i2c物理控制器来和一个i2c总线的物理设备进行交互
  • i2c_algorithm描述一个i2c主机的发送时序的信息,该类的对象algo是i2c_adapter的一个域,其中的master_xfer()注册的函数最终被设备驱动端的i2c_transfer()回调。
  • i2c_client描述一个挂接在硬件i2c总线上的设备的设备信息,即i2c设备的设备对象,与i2c_driver对象匹配成功后通过detected和i2c_driver以及i2c_adapter相连,在控制器驱动与控制器设备匹配成功后被控制器驱动通过i2c_new_device()创建。
  • i2c_driver描述一个挂接在硬件i2c总线上的设备的驱动方法,即i2c设备的驱动对象,通过i2c_bus_type和设备信息i2c_client匹配,匹配成功后通过clients和i2c_client对象以及i2c_adapter对象相连
  • i2c_msg描述一个在设备端和主机端之间进行流动的数据, 在设备驱动中打包并通过i2c_transfer()发送。相当于skbuf之于网络设备,urb之于USB设备。
img

5. 内核源码I2C目录

Linux内核中I2C相关的源码结构是怎样的呢?我们来看一下:i2c - drivers/i2c - Linux source code (v4.19.71)

image-20250326115156642
  • i2c-core-xxx.c:这些文件实现了I2C核心的功能以及/proc/bus/i2c*接口。
  • i2c-dev.c实现了I2C适配器设备文件的功能,每一个I2C适配器都被分配一个设备。通过适配器访设备时的主设备号都为89,次设备号为0-255。I2c-dev.c并没有针对特定的设备而设计,只是提供了通用的read(),write(),和ioctl()等接口,应用层可以借用这些接口访问挂接在适配器上的I2C设备的存储空间或寄存器,并控制I2C设备的工作方式。
  • busses:文件夹这个文件中包含了一些I2C总线的驱动,如针对S3C2410,S3C2440,S3C6410等处理器的I2C控制器驱动为i2c-s3c2410.c.
  • algos:文件夹实现了一些I2C总线适配器的algorithm.

6. 在sysfs中的体现

我们进入根文件系统的 /sys/bus 目录,会发现有 i2c 的一个目录:

image-20250328092437349

我们进入这个目录,查看一下目录下的文件:

image-20250328092624781

这个测试的板子上驱动和设备不多,可以直接用tree命令看一下:

image-20250328092719118

devices目录下都是注册的i2c设备,drivers目录下都是注册的i2c驱动。

二、重要的数据结构

1. I2C总线(控制器/适配器)

1.1 struct i2c_adapter

i2c_adapter适配器对应一个i2c控制器,是用于标识物理i2c总线以及访问它所需的访问算法的结构。I2C 总线驱动重点就是 I2C 适配器(也就是 SOC 的 I2C 接口控制器)驱动 :

1
2
3
4
5
6
7
8
9
10
11
struct i2c_adapter {
struct module *owner;
unsigned int class; /* classes to allow probing for */
const struct i2c_algorithm *algo; /* the algorithm to access the bus */
void *algo_data;
//......
struct device dev; /* the adapter device */

int nr;
//......
};
  • algo: struct i2c_algorithm 结构体,对于一个 I2C 适配器,肯定要对外提供读写 API 函数,设备驱动程序可以使用这些 API 函数来完成读写操作。i2c_algorithm 就是 I2C 适配器与 IIC 设备进行通信的方法。

  • dev: struct device 结构体,控制器,表明这是一个设备。

  • nr:第几个I2C BUS(I2C Controller)。

1.2 struct i2c_algorithm

i2c_algorithm 里面有该I2C BUS的传输函数,用来收发I2C数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct i2c_algorithm {
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
int num);//I2C传输函数指针
int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
unsigned short flags, char read_write,
u8 command, int size, union i2c_smbus_data *data);//smbus传输函数指针

/* To determine what the adapter supports */
u32 (*functionality) (struct i2c_adapter *);//返回适配器支持的功能

#if IS_ENABLED(CONFIG_I2C_SLAVE)
int (*reg_slave)(struct i2c_client *client);
int (*unreg_slave)(struct i2c_client *client);
#endif
};
  • master_xfer: 作为主设备时的传输函数,应该返回成功处理的消息数,或者在出错时返回负值。可以通过此函数来完成与 IIC 设备之间的通信。
  • smbus_xfer: 作为从设备时的传输函数。
  • reg_slave:有些I2C Adapter也可工作与Slave模式,用来实现或模拟一个I2C设备。reg_slave就是让把一个i2c_client注册到I2C Adapter,换句话说就是让这个I2C Adapter模拟该i2c_client。
  • unreg_slave:反注册。

1.3 struct i2c_msg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct i2c_msg {
__u16 addr; /* slave address */
__u16 flags;
#define I2C_M_RD 0x0001 /* read data, from slave to master */
/* I2C_M_RD is guaranteed to be 0x0001! */
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_DMA_SAFE 0x0200 /* the buffer of this message is DMA safe */
/* makes only sense in kernelspace */
/* userspace buffers are copied anyway */
#define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_NOSTART */
#define I2C_M_STOP 0x8000 /* if I2C_FUNC_PROTOCOL_MANGLING */
__u16 len; /* msg length */
__u8 *buf; /* pointer to msg data */
};
  • addr:从机地址
  • flags:操作标志,I2C_M_RD为读(0),写为1
  • len:有效数据长度
  • buf:装载有效数据的头指针

我们知道,i2c总线上传入数据是以字节为单位的,而我们的通信类别分为两种:读and写,对于写,通常按照下面的时序:

image-20250327135909918

对于读,通常是按照下面的时序

image-20250327135932833

i2c子系统为了实现这种通信方法,为我们封装了i2c_msg结构,对于每一个START信号,都对应一个i2c_msg对象,实际操作中我们会将所有的请求封装成一个struct i2c_msg[],一次性将所有的请求通过**i2c_transfer()**发送给匹配到的client的从属的adapter,由adapter根据相应的algo域以及master_xfer域通过主机驱动来将这些请求发送给硬件上的设备。

1.4 总结

I2C 总线驱动,或者说 I2C 适配器驱动的主要工作就是初始化 i2c_adapter 结构体变量,然后设置 i2c_algorithm 中的 master_xfer 函数。

完成以后通过 i2c_add_numbered_adapter()i2c_add_adapter() 这两个函数向系统注册设置好的 i2c_adapter,这两个函数的区别在于 i2c_add_adapter() 使用动态的总线号,而 i2c_add_numbered_adapter() 使用静态总线号。

一般 SOC 的 I2C 总线驱动都是由半导体厂商编写的,比如 I.MX6U 的 I2C 适配器驱动 NXP 已经编写好了,这个不需要用户去编写。因此 I2C 总线驱动对我们这些 SOC 使用者来说是被屏蔽掉的,我们只要专注于 I2C 设备驱动即可。除非我们是在半导体公司上班,工作内容就是写 I2C 适配器驱动。

2. I2C设备驱动

2.1 struct i2c_client

i2c_client表示i2c从设备,既然是从设备,那么一定有设备地址,它连接在哪个I2C Controller上,即对应的i2c_adapter是什么等信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct i2c_client {
unsigned short flags; /* div., see below */
unsigned short addr; /* chip address - NOTE: 7bit */
/* addresses are stored in the */
/* _LOWER_ 7 bits */
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter; /* the adapter we sit on */
struct device dev; /* the device structure */
int init_irq; /* irq set at initialization */
int irq; /* irq issued by device */
struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_t slave_cb; /* callback for slave mode */
#endif
};
  • flags: 标志,可以看这里i2c.h。当flag为I2C_CLIENT_TEN表示设备使用10位芯片地址,I2C客户端PEC表示它使用SMBus数据包错误检查
  • addr: addr在连接到父适配器的I2C总线上使用的地址。
  • name: 表示设备的类型,通常是芯片名。
  • adapter: struct i2c_adapter 结构体,管理托管这个I2C设备的总线段。
  • dev: Driver model设备节点。
  • init_irq: 作为从设备时的发送函数。
  • irq: 表示该设备生成的中断号。
  • detected: struct list_head i2c的成员_驱动程序.客户端列表或i2c核心的用户空间设备列表。
  • slave_cb: 使用适配器的I2C从模式时回调。适配器调用它来将从属事件传递给从属驱动程序。i2c_客户端识别连接到i2c总线的单个设备(即芯片)。暴露在Linux下的行为是由管理设备的驱动程序定义的。

一个设备对应一个 i2c_client,每检测到一个 I2C 设备就会给这个 I2C 设备分配一个i2c_client。

2.2 struct i2c_driver

i2c_driver类似 platform_driver,是我们编写 I2C 设备驱动重点要处理的内容 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct i2c_driver {
unsigned int class;
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
int (*probe_new)(struct i2c_client *);
void (*shutdown)(struct i2c_client *);
void (*alert)(struct i2c_client *, enum i2c_alert_protocol protocol,
unsigned int data);

int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
struct device_driver driver;
const struct i2c_device_id *id_table;
/* Device detection callback for automatic device creation */
int (*detect)(struct i2c_client *, struct i2c_board_info *);
const unsigned short *address_list;
struct list_head clients;
bool disable_i2c_core_irq_mapping;
};
  • probe:当 I2C 设备和驱动匹配成功以后 probe 函数就会执行,和 platform 驱动一样。
  • driver:struct device_driver类型结构体成员,如果使用设备树的话,需要设置 device_driverof_match_table 成员变量,也就是驱动的兼容(compatible)属性。
  • id_table: struct i2c_device_id 要匹配的从设备信息。是传统的、未使用设备树的设备匹配 ID 表。
  • address_list: 设备地址。
  • clients: 设备链表。
  • detect: 设备探测函数

2.3 i2c_bus_type

1
2
3
4
5
6
7
8
struct bus_type i2c_bus_type = {
.name = "i2c",
.match = i2c_device_match,
.probe = i2c_device_probe,
.remove = i2c_device_remove,
.shutdown = i2c_device_shutdown,
};
EXPORT_SYMBOL_GPL(i2c_bus_type);

这个其实是是一个全局变量,它是 struct bus_type 类型的结构体变量,里面包含了i2c_clienti2c_driver匹配的函数指针。

2.4 总结

image-20210227151413993

参考资料:

【驱动】linux下I2C驱动架构全面分析 - Leo.cheng - 博客园

Linux i2c子系统(一) _动手写一个i2c设备驱动_Linux编程_Linux公社-Linux系统门户网站

I2C子系统–mpu6050驱动实验 野火嵌入式Linux驱动开发实战指南——基于i.MX6ULL系列 文档