LV100-I2C驱动简介
一、I2C 驱动框架简介
1. 之前怎么驱动 I2C 设备的?
看一下之前学习单片机的时候学习 I2C 驱动 24C02 的代码,我们一共编写了以下几个文件:
这些文件是通过 I2C 接口,通过 I2C 实现对 24C02 的寄存器读写操作的。
这些文件是实现 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 总线驱动、具体某个设备的驱动。

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 驱动如下图所示:

用户空间的就是用户访问 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 是驱动模块。他们的关系如下:

- 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 设备。

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

- 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 的一个目录:
我们进入这个目录,查看一下目录下的文件:

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

devices 目录下都是注册的 i2c 设备,drivers 目录下都是注册的 i2c 驱动。
二、重要的数据结构
1. I2C 总线(控制器/适配器)
1.1 struct i2c_adapter
i2c_adapter 适配器对应一个 i2c 控制器,是用于标识物理 i2c 总线以及访问它所需的访问算法的结构。I2C 总线驱动重点就是 I2C 适配器(也就是 SOC 的 I2C 接口控制器)驱动 :
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 数据:
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
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 写,对于写,通常按照下面的时序:
对于读,通常是按照下面的时序
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 是什么等信息:
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 设备驱动重点要处理的内容 :
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_driver 的 of_match_table 成员变量,也就是驱动的兼容(compatible)属性。
- id_table: struct i2c_device_id 要匹配的从设备信息。是传统的、未使用设备树的设备匹配 ID 表。
- address_list: 设备地址。
- clients: 设备链表。
- detect: 设备探测函数
2.3 i2c_bus_type
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_client 和 i2c_driver 匹配的函数指针。
2.4 总结

参考资料:
【驱动】linux 下 I2C 驱动架构全面分析 - Leo.cheng - 博客园
Linux i2c 子系统(一) _动手写一个 i2c 设备驱动_Linux 编程_Linux 公社-Linux 系统门户网站
I2C 子系统–mpu6050 驱动实验 野火嵌入式 Linux 驱动开发实战指南——基于 i.MX6ULL 系列 文档