I2C Core(I2C 核心层)是 Linux 内核中 I2C 驱动架构的重要组成部分,它为 I2C 适配器驱动和 I2C 设备驱动提供了一系列标准接口,以实现统一的管理和操作。
I2C Core 提供的一些主要标准接口:
1. 适配器注册与注销接口
i2c_add_adapter
- 功能:向 I2C 核心层注册一个 I2C 适配器。注册成功后,该适配器就可以被系统识别和使用。
- 原型:
int i2c_add_adapter(struct i2c_adapter *adapter);
- 参数:
adapter
:指向要注册的struct i2c_adapter
结构体的指针。
- 返回值:成功返回 0,失败返回负数错误码。
i2c_del_adapter
- 功能:从 I2C 核心层注销一个已注册的 I2C 适配器。注销后,该适配器将不再被系统使用。
- 原型:
void i2c_del_adapter(struct i2c_adapter *adapter);
- 参数:
adapter
:指向要注销的struct i2c_adapter
结构体的指针。
2. I2C 传输接口
i2c_transfer
- 功能:发起一系列的 I2C 消息传输。它是 I2C 核心层提供的核心传输接口,会调用适配器驱动中实现的
master_xfer
回调函数来完成实际的传输操作。 - 原型:
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
- 参数:
adap
:指向要使用的 I2C 适配器的指针。msgs
:指向struct i2c_msg
结构体数组的指针,每个i2c_msg
结构体表示一个 I2C 消息。num
:i2c_msg
数组的元素个数。
- 返回值:成功返回实际传输的消息数量,失败返回负数错误码。
3. I2C 设备注册与注销接口
i2c_new_device
- 功能:在指定的 I2C 适配器上注册一个新的 I2C 设备。
- 原型:
struct i2c_client *i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info);
- 参数:
adap
:指向要使用的 I2C 适配器的指针。info
:指向struct i2c_board_info
结构体的指针,该结构体包含了 I2C 设备的相关信息,如设备名称、从设备地址等。
- 返回值:成功返回指向新注册的
struct i2c_client
结构体的指针,失败返回NULL
。
i2c_unregister_device
- 功能:注销一个已注册的 I2C 设备。
- 原型:
void i2c_unregister_device(struct i2c_client *client);
- 参数:
client
:指向要注销的struct i2c_client
结构体的指针。
4. SMBus 操作接口
i2c_smbus_xfer
- 功能:执行 SMBus(System Management Bus)操作。SMBus 是 I2C 总线的一个子集,提供了一些标准的读写操作命令。
- 原型:
s32 i2c_smbus_xfer(struct i2c_adapter *adapter, u16 addr,
u16 flags, char read_write, u8 command,
int size, union i2c_smbus_data *data);
- 参数:
adapter
:指向要使用的 I2C 适配器的指针。addr
:I2C 从设备的地址。flags
:操作标志。read_write
:读写标志,I2C_SMBUS_READ
表示读操作,I2C_SMBUS_WRITE
表示写操作。command
:SMBus 命令。size
:数据传输的大小。data
:指向union i2c_smbus_data
结构体的指针,用于存储传输的数据。
- 返回值:成功返回传输的数据,失败返回负数错误码。
5. 设备树相关接口
i2c_of_register_devices
- 功能:根据设备树中的信息,自动注册 I2C 设备。
- 原型:
void i2c_of_register_devices(struct i2c_adapter *adap);
- 参数:
adap
:指向要使用的 I2C 适配器的指针。
这些接口为 I2C 驱动的开发提供了便利,使得开发者可以专注于具体的硬件操作和功能实现,而无需关心 I2C 核心层的底层细节。
核心通信函数i2c_transfer详解
在 Linux 内核的 I2C 驱动架构中,I2C 核心层、i2c_transfer
函数以及 master_xfer
回调函数之间存在着紧密的调用关系,下面为你详细解释它们之间的调用流程。
1. 概述
本节说明下I2C 核心层->i2c_transfer->适配器驱动master_xfer调用栈
2. I2C 核心层调用 i2c_transfer
的过程
2.1 应用层发起 I2C 操作请求
当应用程序需要进行 I2C 设备的读写操作时,通常会通过系统调用(如 ioctl
)来请求内核进行相应的 I2C 传输。例如,使用 I2C_RDWR
命令发起一次读写操作。
2.2 内核层接收请求
在内核层,设备文件系统会将应用层的请求传递给 I2C 核心层。I2C 核心层会根据请求的内容,调用 i2c_transfer
函数来完成具体的 I2C 传输。
2.3 i2c_transfer
函数的定义和作用
i2c_transfer
是 I2C 核心层提供的一个通用函数,用于发起一系列的 I2C 消息传输。其原型如下:
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
adap
:指向要使用的 I2C 适配器的指针。msgs
:指向i2c_msg
结构体数组的指针,每个i2c_msg
结构体表示一个 I2C 消息。num
:i2c_msg
数组的元素个数。
2.4 调用示例
在 I2C 核心层的代码中,当接收到应用层的 I2C 操作请求时,会调用 i2c_transfer
函数,示例代码如下:
// 假设已经获取到了 i2c_adapter 指针 adap 和 i2c_msg 数组 msgs 以及消息数量 num
int ret = i2c_transfer(adap, msgs, num);
if (ret < 0) {
// 处理传输错误
}
3. i2c_transfer
调用 master_xfer
的过程
3.1 i2c_algorithm
结构体
每个 I2C 适配器都有一个 i2c_algorithm
结构体,该结构体定义了该适配器支持的 I2C 操作方法。其中,master_xfer
是一个回调函数指针,指向具体的 I2C 传输实现函数。
struct i2c_algorithm {
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
u32 (*functionality)(struct i2c_adapter *adap);
// 其他成员...
};
3.2 i2c_adapter
结构体
i2c_adapter
结构体表示一个 I2C 适配器,其中包含了指向 i2c_algorithm
结构体的指针。
struct i2c_adapter {
struct module *owner;
unsigned int id;
unsigned int class;
const struct i2c_algorithm *algo;
// 其他成员...
};
3.3 i2c_transfer
内部调用 master_xfer
在 i2c_transfer
函数的实现中,会通过 i2c_adapter
结构体获取到对应的 i2c_algorithm
结构体,然后调用其中的 master_xfer
回调函数来完成实际的 I2C 传输。以下是简化的 i2c_transfer
函数实现示例:
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
const struct i2c_algorithm *algo = adap->algo;
if (algo->master_xfer) {
return algo->master_xfer(adap, msgs, num);
} else {
// 处理不支持的情况
return -ENOTSUPP;
}
}
3.4 适配器驱动实现 master_xfer
I2C 适配器驱动需要实现 master_xfer
回调函数,该函数负责与具体的 I2C 控制器硬件进行交互,完成 I2C 数据的读写操作。例如,在之前提供的代码中,arm_i2c_xfer
函数就是 master_xfer
的具体实现:
static int arm_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
int i, ret;
for (i = 0; i < num; i++) {
struct i2c_msg *msg = &msgs[i];
// 发送起始信号
// 这里需要根据具体硬件设置寄存器来产生起始信号
// 示例代码:
// writel(START_BIT_MASK, i2c_con_reg);
// 发送设备地址
// 这里需要将设备地址写入数据寄存器并发送
// 示例代码:
// writel(msg->addr << 1 | (msg->flags & I2C_M_RD), i2c_data_reg);
if (msg->flags & I2C_M_RD) {
// 读取数据
for (int j = 0; j < msg->len; j++) {
// 等待数据准备好
// 示例代码:
// while (!(readl(i2c_stat_reg) & DATA_READY_MASK));
// 读取数据
// msg->buf[j] = readl(i2c_data_reg);
}
} else {
// 写入数据
for (int j = 0; j < msg->len; j++) {
// 写入数据到寄存器
// 示例代码:
// writel(msg->buf[j], i2c_data_reg);
// 等待写入完成
// 示例代码:
// while (!(readl(i2c_stat_reg) & WRITE_COMPLETE_MASK));
}
}
// 发送停止信号
// 这里需要根据具体硬件设置寄存器来产生停止信号
// 示例代码:
// writel(STOP_BIT_MASK, i2c_con_reg);
}
return num;
}
总结
I2C 核心层在接收到应用层的 I2C 操作请求后,会调用 i2c_transfer
函数。i2c_transfer
函数通过 i2c_adapter
结构体获取到对应的 i2c_algorithm
结构体,然后调用其中的 master_xfer
回调函数,最终由适配器驱动的 master_xfer
实现完成与 I2C 控制器硬件的交互,从而完成 I2C 数据的传输。
应用层到I2C核心层调用栈
下面详细介绍应用程序从发起 I2C 操作请求到调用 i2c_transfer
的完整流程以及代码调用栈。
完整流程
1. 应用程序层面
应用程序通常会使用 Linux 系统提供的文件操作接口(如 open
、ioctl
、read
、write
等)来与 I2C 设备进行交互。一般使用 ioctl
函数结合 I2C_RDWR
命令来发起一次完整的 I2C 读写操作。
2. 内核层面
- 设备文件系统:应用程序的系统调用会通过设备文件系统传递到内核中对应的 I2C 设备驱动。
- I2C 核心层:I2C 核心层负责接收这些请求,并调用
i2c_transfer
函数来完成实际的 I2C 传输。 - I2C 适配器驱动:
i2c_transfer
函数会进一步调用 I2C 适配器驱动中实现的master_xfer
回调函数,该函数负责与具体的 I2C 控制器硬件进行交互,完成数据的读写。
代码调用栈
应用程序代码示例
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#define I2C_DEV_PATH "/dev/i2c-0"
#define I2C_SLAVE_ADDR 0x50
int main()
{
int fd;
struct i2c_rdwr_ioctl_data msgset;
struct i2c_msg msgs[2];
unsigned char buf[2];
// 打开 I2C 设备文件
fd = open(I2C_DEV_PATH, O_RDWR);
if (fd < 0) {
perror("Failed to open I2C device");
return -1;
}
// 设置从设备地址
if (ioctl(fd, I2C_SLAVE, I2C_SLAVE_ADDR) < 0) {
perror("Failed to set I2C slave address");
close(fd);
return -1;
}
// 准备写入数据
buf[0] = 0x00; // 寄存器地址
buf[1] = 0xAA; // 写入的数据
msgs[0].addr = I2C_SLAVE_ADDR;
msgs[0].flags = 0;
msgs[0].len = 2;
msgs[0].buf = buf;
// 准备读取数据
msgs[1].addr = I2C_SLAVE_ADDR;
msgs[1].flags = I2C_M_RD;
msgs[1].len = 1;
msgs[1].buf = buf;
msgset.msgs = msgs;
msgset.nmsgs = 2;
// 执行 I2C 传输
if (ioctl(fd, I2C_RDWR, &msgset) < 0) {
perror("Failed to perform I2C transfer");
close(fd);
return -1;
}
// 打印读取的数据
printf("Read data: 0x%02x\n", buf[0]);
// 关闭 I2C 设备文件
close(fd);
return 0;
}
内核层面代码调用栈
1. ioctl
系统调用
应用程序调用 ioctl(fd, I2C_RDWR, &msgset)
时,会触发内核中的 vfs_ioctl
函数,该函数是内核中处理 ioctl
系统调用的通用函数。
用户空间: main() -> ioctl(fd, I2C_RDWR, &msgset)
内核空间: vfs_ioctl()
2. I2C 设备文件操作
vfs_ioctl
会根据文件描述符 fd
找到对应的 I2C 设备文件操作结构体 file_operations
,并调用其中的 unlocked_ioctl
或 compat_ioctl
函数(取决于内核配置)。对于 I2C 设备,这些函数会进一步处理 I2C_RDWR
命令。
内核空间: vfs_ioctl() -> i2c_dev_ioctl()
3. 解析 I2C_RDWR
命令
i2c_dev_ioctl
函数会解析 I2C_RDWR
命令,提取出 i2c_rdwr_ioctl_data
结构体中的 i2c_msg
数组和消息数量,然后调用 i2c_transfer
函数。
内核空间: i2c_dev_ioctl() -> i2c_transfer()
4. 调用 master_xfer
i2c_transfer
函数会通过 i2c_adapter
结构体获取到对应的 i2c_algorithm
结构体,然后调用其中的 master_xfer
回调函数,该函数由 I2C 适配器驱动实现,负责与具体的 I2C 控制器硬件进行交互。
内核空间: i2c_transfer() -> algo->master_xfer()
完整代码调用栈总结
用户空间: main()
-> ioctl(fd, I2C_RDWR, &msgset)
内核空间: vfs_ioctl()
-> i2c_dev_ioctl()
-> i2c_transfer()
-> algo->master_xfer()
详细代码解释
i2c_dev_ioctl
函数(简化示例)
static long i2c_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct i2c_client *client = file->private_data;
struct i2c_adapter *adap = client->adapter;
struct i2c_rdwr_ioctl_data *msgset;
struct i2c_msg *msgs;
int ret;
switch (cmd) {
case I2C_RDWR:
msgset = (struct i2c_rdwr_ioctl_data *)arg;
msgs = msgset->msgs;
ret = i2c_transfer(adap, msgs, msgset->nmsgs);
return ret;
// 其他命令处理...
default:
return -ENOTTY;
}
}
i2c_transfer
函数(简化示例)
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
const struct i2c_algorithm *algo = adap->algo;
if (algo->master_xfer) {
return algo->master_xfer(adap, msgs, num);
} else {
return -ENOTSUPP;
}
}
通过以上流程和代码调用栈,应用程序可以通过系统调用发起 I2C 操作请求,最终由 I2C 适配器驱动完成与硬件的交互。
I2C-DEV模块
在 Linux 系统中,I2C-DEV模块是I2C Core的重要组成部分,/dev/i2c-0
这样的设备节点,以及i2c_dev_ioctl()这类供上层调用的API皆在其中实现。
1. I2C 驱动框架概述
Linux 内核的 I2C 驱动框架主要由三部分组成:I2C 核心层、I2C 适配器驱动层和 I2C 设备驱动层。/dev/i2c-0
设备节点的产生主要与 I2C 适配器驱动的注册和设备文件系统的操作有关。
2. I2C 适配器驱动的加载与注册
2.1 设备树描述(可选)
现代 Linux 系统通常使用设备树(Device Tree)来描述硬件设备信息。在设备树文件(一般以 .dts
或 .dtb
为扩展名)中,会对 I2C 控制器进行描述,例如:
i2c0: i2c@12345678 {
compatible = "vendor,i2c-controller";
reg = <0x12345678 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
clock-frequency = <100000>;
};
上述代码描述了一个 I2C 控制器,compatible
属性指定了设备的兼容性,reg
属性指定了寄存器的基地址和大小,clock - frequency
指定了 I2C 总线的时钟频率。
2.2 适配器驱动的加载
内核启动时,会根据设备树信息加载相应的 I2C 适配器驱动。适配器驱动通常是一个内核模块,包含了对特定 I2C 控制器硬件的操作函数。例如,对于 ARM 架构的 I2C 控制器,可能有类似以下的驱动代码:
#include <linux/module.h>
#include <linux/i2c.h>
// 适配器操作函数实现
static int my_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
// 实现 I2C 数据传输的具体逻辑
return num;
}
static u32 my_i2c_func(struct i2c_adapter *adap)
{
return I2C_FUNC_I2C;
}
// 适配器算法结构体
static const struct i2c_algorithm my_i2c_algo = {
.master_xfer = my_i2c_xfer,
.functionality = my_i2c_func,
};
// 适配器结构体
static struct i2c_adapter my_i2c_adapter = {
.owner = THIS_MODULE,
.class = I2C_CLASS_HWMON,
.algo = &my_i2c_algo,
.name = "my_i2c_adapter",
};
// 驱动初始化函数
static int __init my_i2c_init(void)
{
int ret;
ret = i2c_add_adapter(&my_i2c_adapter);
if (ret < 0) {
printk(KERN_ERR "Failed to add I2C adapter\n");
return ret;
}
printk(KERN_INFO "I2C adapter registered\n");
return 0;
}
// 驱动退出函数
static void __exit my_i2c_exit(void)
{
i2c_del_adapter(&my_i2c_adapter);
printk(KERN_INFO "I2C adapter unregistered\n");
}
module_init(my_i2c_init);
module_exit(my_i2c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My I2C Adapter Driver");
在上述代码中,my_i2c_init
函数调用 i2c_add_adapter
函数将 my_i2c_adapter
注册到 I2C 核心层。
3. I2C 设备文件系统的处理
3.1 i2c-dev
模块的加载
i2c - dev
是一个内核模块,它为 I2C 适配器提供了用户空间的访问接口。当 i2c - dev
模块加载时,会遍历系统中已注册的 I2C 适配器,并为每个适配器创建一个对应的设备节点。
3.2 设备节点的创建
i2c - dev
模块中的代码会调用 misc_register
或 cdev_add
等函数来创建字符设备节点。例如,i2c - dev
模块中的部分代码如下:
static int __init i2c_dev_init(void)
{
int res;
printk(KERN_INFO "i2c /dev entries driver\n");
res = register_chrdev_region(MKDEV(I2C_MAJOR, 0), I2C_MINORS, "i2c");
if (res)
goto out;
i2c_dev_class = class_create(THIS_MODULE, "i2c-dev");
if (IS_ERR(i2c_dev_class)) {
res = PTR_ERR(i2c_dev_class);
goto out_unreg_chrdev;
}
i2c_register_adapter_notifier(&i2c_dev_notifier);
i2c_for_each_dev(NULL, i2c_dev_attach_adapter);
return 0;
out_unreg_chrdev:
unregister_chrdev_region(MKDEV(I2C_MAJOR, 0), I2C_MINORS);
out:
return res;
}
上述代码中,register_chrdev_region
函数用于分配字符设备的主设备号和次设备号,class_create
函数用于创建一个设备类,i2c_for_each_dev
函数会遍历所有已注册的 I2C 适配器,并调用 i2c_dev_attach_adapter
函数为每个适配器创建设备节点。
3.3 设备节点的命名
对于第一个 I2C 适配器,i2c - dev
模块会创建名为 /dev/i2c - 0
的设备节点;对于第二个 I2C 适配器,会创建 /dev/i2c - 1
节点,依此类推。设备节点的命名规则是 /dev/i2c - <适配器编号>
。
4. 总结
综上所述,/dev/i2c - 0
设备节点的产生过程如下:
- 内核启动时,根据设备树信息加载 I2C 适配器驱动,并将适配器注册到 I2C 核心层。
- 加载
i2c - dev
内核模块,该模块遍历已注册的 I2C 适配器。 i2c - dev
模块为每个 I2C 适配器分配主设备号和次设备号,创建设备类,并为每个适配器创建对应的字符设备节点,命名为/dev/i2c - <适配器编号>
。
这样,用户空间的应用程序就可以通过操作 /dev/i2c - 0
等设备节点来与 I2C 设备进行通信。