I2C子系统

I2C子系统的架构

Linux内核的I2C子系统同样遵循者”驱动分层”和”主机驱动和设备驱动分离”的理念,它的结构可分为3部分:

image-20250330171253889
  • 核心层:由Linux内核提供,为适配器和设备驱动层提供了统一的API,包括注册、注销、传数据等
  • 总线驱动层:由芯片原厂负责,主要包括适配器(SoC内的I2C控制器)的配置,以及数据发送/接收函数的定义这2部分
  • 设备驱动层:由普通驱动工程师负责,通过核心层提供的通用API,实现对I2C特定波形的发送从而控制具体的设备

核心文件

在Linux内核源代码中的drivers目录下有一个i2c目录,而在i2c目录下又包含如下文件和文件夹:

  • i2c-core.c:实现了I2C核心的功能以及/proc/bus/i2c*接口
  • i2c-dev.c:通过创建一个字符设备/dev/i2c-x),提供了一种在用户空间对I2C适配器直接操作的方式,即可以直接在应用层让适配器发送某些波形,和具体的设备无关
  • busses文件夹:包含一些具体SoC的I2C总线驱动
  • algos文件夹:实现了一些I2C总线适配器的通信方法

核心数据结构

内核中的include/linux/i2c.h定义了几个I2C子系统的核心数据结构

总线驱动层

上面提到了总线驱动层需要完成对适配器的配置以及实现I2C波形信号的发送,所以有2个重要的数据结构

i2c_adpter

struct i2c_adpter用于描述一个I2C适配器,定义了 I2C 控制器的硬件特性、操作方法以及所连接的设备。==每个==物理 I2C 总线(如 i2c-0i2c-1)在内核中==均对应==一个 i2c_adapter 实例

核心作用:

  • 抽象 I2C 控制器硬件:封装控制器的寄存器操作、时钟频率、中断等
  • 提供通信方法:通过 i2c_algorithm 实现底层I2C报文的收发
  • 管理连接的设备:维护挂载在该总线上的I2C设备列表(i2c_client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct i2c_adapter {
struct module *owner; // 所属模块(通常为 THIS_MODULE)
unsigned int class; // 设备类(I2C_CLASS_* 标志)
const struct i2c_algorithm *algo; // 总线通信算法(关键操作函数)
void *algo_data; // 算法私有数据(驱动自定义)

/* 调度器管理 */
struct rt_mutex bus_lock; // 总线锁(防止并发访问)
struct rt_mutex mux_lock; // 多路复用锁(用于 I2C 多路复用器)

int timeout; // 传输超时(毫秒)
int retries; // 重试次数
struct device dev; // 关联的设备(嵌入的 device 结构体)

int nr; // 总线编号(如 i2c-0 的 0)
char name[48]; // 适配器名称(如 "i2c-STM32")
struct list_head clients; // 挂载的 I2C 设备链表(i2c_client)
struct list_head userspace_clients; // 用户空间创建的设备链表

/* 多路复用支持 */
struct i2c_mux_core *mux_core; // 多路复用器核心
struct i2c_bus_recovery_info *bus_recovery_info; // 总线恢复方法(如 GPIO 恢复)

/* 统计与调试 */
struct irq_domain *host_notify_domain; // 主机通知中断域
u16 slave_cfg; // 从模式配置
u32 bus_clk_rate; // 总线时钟频率(Hz)
u32 quirks; // 硬件特性标志(如 I2C_AQ_NO_ZERO_LEN)
struct dentry *debugfs; // 调试文件系统节点

/* 电源管理 */
bool suspended; // 是否处于挂起状态
};

i2c_algorithm

struct i2c_algorithm:定义了如何通过硬件控制器进行 I2C 通信(如发送/接收数据、支持的功能等)。每个 i2c_adapter都会关联一个 i2c_algorithm,以提供底层硬件操作的具体实现

核心作用:

  • 抽象硬件操作:
    将 I²C 控制器的寄存器操作(如启动/停止条件、数据读写)封装成统一的接口。
  • 支持多种协议:
    同时兼容 ​标准 I²C和 SMBus​(System Management Bus)协议。
  • 功能查询:告知上层驱动当前适配器支持哪些特性(如 10 位地址、DMA 传输等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct i2c_algorithm {
// 标准 I²C 报文传输函数(必须实现),用于产生波形信号
int (*master_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs,
int num);

// SMBus 协议传输函数(可选)
int (*smbus_xfer)(struct i2c_adapter *adap,
u16 addr,
unsigned short flags,
char read_write,
u8 command,
int size,
union i2c_smbus_data *data);

// 返回适配器支持的功能(必须实现)
u32 (*functionality)(struct i2c_adapter *adap);
};

常见标志:

标志 说明
I2C_FUNC_I2C 支持标准 I²C
I2C_FUNC_SMBUS_EMUL 可模拟 SMBus(通过 master_xfer
I2C_FUNC_10BIT_ADDR 支持 10 位地址
I2C_FUNC_PROTOCOL_MANGLING 支持特殊时序(如重复启动)

设备驱动层

I2C子系统和platform框架类似,遵循着Linux的设备驱动模型,并实现了驱动和设备的分离。且i2c_driveri2c_client也是一对多的关系,即同一份驱动可以创建多个同类型设备(只要匹配的上)

i2c_driver

struct i2c_driver:类似platfor_driver,它用于管理I2C设备的驱动,定义了驱动如何和I2C设备进行交互,包括proberemove、与设备匹配等操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct i2c_driver {
unsigned int class; // 设备类(I2C_CLASS_*)
int (*probe)(struct i2c_client *client,
const struct i2c_device_id *id); // 设备初始化
int (*remove)(struct i2c_client *client); // 设备移除
void (*shutdown)(struct i2c_client *client); // 设备关机
int (*suspend)(struct i2c_client *client, pm_message_t mesg); // 休眠
int (*resume)(struct i2c_client *client); // 唤醒
void (*alert)(struct i2c_client *client,
enum i2c_alert_protocol type,
unsigned int data); // 警报处理(如 SMBus Alert)
int (*command)(struct i2c_client *client,
unsigned int cmd,
void *arg); // 自定义命令(ioctl 类似)
struct device_driver driver; // 设备驱动基类
const struct i2c_device_id *id_table; // 设备 ID 匹配表
int (*detect)(struct i2c_client *client,
struct i2c_board_info *info); // 动态设备检测
const unsigned short *address_list; // 支持的地址列表(动态探测)
struct list_head clients; // 绑定的设备链表
};

i2c_client

struct i2c_client:它是 Linux 内核中表示 一个具体的 I²C 设备 的数据结构,它描述了连接到 I²C 总线上的物理设备(如传感器、EEPROM 等)。每个被内核识别并绑定了驱动的 I²C 设备都会对应一个 i2c_client 实例

1
2
3
4
5
6
7
8
9
10
struct i2c_client {
unsigned short flags; // 标志位(如 I2C_CLIENT_TEN 表示10位地址)
unsigned short addr; // 设备地址(7位或10位)
char name[I2C_NAME_SIZE]; // 设备名称(如 "lm75")
struct i2c_adapter *adapter; // 所属的 I²C 总线适配器
struct device dev; // 内嵌的设备模型(继承自 device)
int irq; // 设备中断号(可选)
struct list_head detected; // 链表节点(用于管理设备列表)
// ... 其他内核内部字段
};
  • i2c_client依附于i2c_adapter,每个i2c_adapter可以挂多个i2c_client
  • i2c_client就和platform_device一样,由内核根据设备树自动创建,不需要人手动创建,创建完成后由总线进行匹配

核心层

i2c_msg

struct i2c_msg:它是 I2C 设备驱动适配器之间传递数据的基本单元,用于封装一次 I2C 传输的详细信息

1
2
3
4
5
6
struct i2c_msg {
__u16 addr; // 从设备地址(7位或10位)
__u16 flags; // 标志位(读/写、地址格式等)
__u16 len; // 消息数据长度(字节数)
__u8 *buf; // 指向数据缓冲区的指针
};
  1. addr
    • 表示 I2C 从设备的地址(7位或10位格式)
    • 地址通常由设备制造商指定(如 0x50 表示 EEPROM)
  2. flags
    控制消息行为的标志位,常用值包括:
    • I2C_M_RD:表示读取从设备的数据(否则为写入
    • I2C_M_TEN:使用 10 位地址(默认为 7 位)
    • I2C_M_STOP:在当前消息后发送停止条件(默认自动添加)
    • I2C_M_NOSTART:复用前一个消息的起始条件(用于复合操作)
  3. len
    • 指定消息数据的长度(字节数),即 buf 指向的缓冲区大小
  4. buf
    • 指向存储读/写数据的缓冲区,该缓冲区在内核中分配,如果要传递到用户空间,需要用copy_from_user之类的API

常见API

总线驱动层

源码分析请看飞书12-2

虽然I2C是一个总线,但其适配器的驱动实际上是个platform驱动而不是I2C驱动,因为SoC上的各种控制器都位于CPU的内存空间中(可以通过内存直接访问寄存器),并没有挂在总线上,只有控制器连接的设备才挂载在总线上

1
2
3
4
// adpter的注册/注销
int i2c_add_adapter(struct i2c adapter *adapter); //自动分配 adapter ID
int i2c_add_numbered_adapter(struct i2c adapter *adap);//指定 ID
void i2c_del_adapter(struct i2c adapter * adap);

设备驱动层

1
2
3
// 设备驱动的注册/注销
int i2c_add_driver(struct i2c driver *driver);
void i2c_del_driver(struct i2c driver *driver);
  • 这些API一般在probe/remove中使用

核心层

1
2
3
int i2c_transfer(struct i2c_adapter * adap, struct i2c_msg *msgs, int num);
int i2c_master_send(struct i2c_client *client,const char *buf ,int count);
int i2c_master_recv(struct i2c_client *client, char *buf ,int count);
  • i2c_transfer函数用于进行I2C适配器和I2C设备之间的一组消息交互(可同时包含写或读),其中第2个参数是一个指向 i2c_msg数组的指针,所以i2c_transfer一次可以传输多个i2c_msg
  • 后面2个函数都是对i2c_transfer的封装,只能进行单一的读或写操作
  • i2c_transfer本身不具备发送波形的能力,其内部调用了adpter->alogritm的master_xfer
image-20250401105646190

使用i2c_transfer发送数据的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* i2c_read_regs - 从 I2C 设备连续读取多个寄存器
* @client: I2C 设备客户端
* @reg: 起始寄存器地址
* @val: 存储读取数据的缓冲区
* @len: 要读取的字节数
*
* 返回: 0 成功,负数表示错误
*/
int i2c_read_regs(struct i2c_client *client, u8 reg, u8 *val, int len)
{
struct i2c_msg msgs[2];
int ret;

// 消息1:写入寄存器地址(指定起始位置)
msgs[0].addr = client->addr;
msgs[0].flags = 0; // 写操作
msgs[0].len = 1; // 寄存器地址占1字节
msgs[0].buf = ® // 指向寄存器地址

// 消息2:从设备读取数据
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD; // 读操作
msgs[1].len = len; // 读取长度
msgs[1].buf = val; // 存储数据的缓冲区

// 执行复合传输(写入地址 + 读取数据)
ret = i2c_transfer(client->adapter, msgs, 2);
}

/**
* i2c_write_regs - 向 I2C 设备连续写入多个寄存器
* @client: I2C 设备客户端
* @reg: 起始寄存器地址
* @val: 要写入的数据缓冲区
* @len: 要写入的字节数
*
* 返回: 0 成功,负数表示错误
*/
int i2c_write_regs(struct i2c_client *client, u8 reg, const u8 *val, int len)
{
u8 *buf;
int ret;

// 分配缓冲区:寄存器地址 + 数据
buf = kmalloc(len + 1, GFP_KERNEL);
if (!buf)
return -ENOMEM;

// 组合数据:首字节为寄存器地址,后续为写入数据
buf[0] = reg;
memcpy(&buf[1], val, len);

// 单条消息:写入寄存器地址 + 数据
ret = i2c_master_send(client, buf, len + 1);
kfree(buf);
}

注意:

  • 读寄存器的时候一般需要同时进行“读”和“写”操作,因为读之前要先写入设备寄存器的地址
  • 写寄存器的时候最后的数据长度是len+1,因为也要写入设备寄存器的地址

调试

sysfs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看当前设备树中注册的i2c设备(具体设备和适配器)
ls /sys/bus/i2c/devices/ -l
# 具体设备(目录)
0-001e -> ../../../devices/platform/soc/2100000.aips-bus/21a0000.i2c/i2c-0/0-001e
0-003c -> ../../../devices/platform/soc/2100000.aips-bus/21a0000.i2c/i2c-0/0-003c
1-0014 -> ../../../devices/platform/soc/2100000.aips-bus/21a4000.i2c/i2c-1/1-0014
1-001a -> ../../../devices/platform/soc/2100000.aips-bus/21a4000.i2c/i2c-1/1-001a
# 适配器(目录)
i2c-0 -> ../../../devices/platform/soc/2100000.aips-bus/21a0000.i2c/i2c-0
i2c-1 -> ../../../devices/platform/soc/2100000.aips-bus/21a4000.i2c/i2c-1

# 查看当前内核中注册的i2c设备的驱动
[ root@lrq]: /sys/bus/i2c/drivers$:ls
at24 mc13xxx stmpe-i2c
da9052 mma8450 tlv320aic23-codec
dummy ov2640 tsc2007
edt_ft5x06 pca953x vtl_ts
egalax_ts pfuze100-regulator wm8960
gt9147 sgtl5000 wm8962
ir-kbd-i2c sii902x
  • /sys/bus/i2c/devices/下的设备,不一定有匹配上的驱动,如果匹配成功了,目录中会有driver目录,反之则没有
  • 同样的,/sys/bus/i2c/drivers/下的驱动,不一定有匹配上的设备,如果匹配成功了,目录中会有设备目录

i2c-dev

Linux内核通过通过字符设备框架,提供了一种在用户空间对I2C适配器直接操作的方式,即可以直接在应用层让适配器发送某些波形,和具体的设备无关

  • I2C适配器的设备节点为:/dev/i2c-x

i2c-tools

i2c-tools是一个用于调试i2c设备的命令行工具,它底层其实依赖了i2c-dev,它具备以下功能:

命令 功能 使用示例
i2cdetect 扫描 I2C 设备 sudo i2cdetect -y 1
i2cget 读取寄存器 sudo i2cget -y 1 0x50 0x00
i2cset 写入寄存器 sudo i2cset -y 1 0x50 0x01 0xAA
i2cdump 导出所有寄存器 sudo i2cdump -y 1 0x50
i2ctransfer 复杂读写(SMBus) sudo i2ctransfer -y 1 w2@0x50 0x01 0xAA r2

设备树写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//imx6ull.dtsi

/{
soc{
aips2{
i2c1: i2c@021a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_I2C1>;
status = "disabled";
};
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// imx6ull-alientek-emmc.dts

&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";

oled@3c {
compatible = "alientek,oled";
reg = <0x3c>;
status = "okay";
};

gt9147:gt9147@14 {
compatible = "goodix,gt9147", "goodix,gt9xx";
reg = <0x14>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_tsc
&pinctrl_tsc_reset >;
interrupt-parent = <&gpio1>;
interrupts = <9 0>;
reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
interrupt-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
status = "okay";
};
};
  • I2C控制器位于SoC的内存空间,所以它直接是SoC里某个数据总线的子节点
  • I2C控制器还要在板级设备树中设置时钟频率pinctrl
  • I2C设备节点需要作为==对应控制器==的子节点