内核提供的通用I2C设备驱动I2C-dev.c分析:file_ops篇

往期内容

I2C子系统专栏:

  1. I2C(IIC)协议讲解-CSDN博客
  2. SMBus 协议详解-CSDN博客
  3. I2C相关结构体讲解:i2c_adapter、i2c_algorithm、i2c_msg-CSDN博客
  4. 内核提供的通用I2C设备驱动I2c-dev.c分析:注册篇

总线和设备树专栏:

  1. 总线和设备树_憧憬一下的博客-CSDN博客
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客

前言

在上一篇,注册篇中,从讲解普通的字符设备驱动框架后再讲解了关于i2c-dev.c中是如何去注册字符设备驱动的,接下来就讲解关于其file_operations中定义的函数是如何去实现的。

内核中提供的驱动文件: Linux-4.9.88/drivers/i2c/i2c-dev.c📎i2c-dev.c

先来file_operation看看定义了哪些函数:

c 复制代码
static const struct file_operations i2cdev_fops = {
	.owner		= THIS_MODULE,
	.llseek		= no_llseek,
	.read		= i2cdev_read,
	.write		= i2cdev_write,
	.unlocked_ioctl	= i2cdev_ioctl,
	.open		= i2cdev_open,
	.release	= i2cdev_release,
};

1. i2cdev_open

c 复制代码
static int i2cdev_open(struct inode *inode, struct file *file)
{
    unsigned int minor = iminor(inode);   // 获取次设备号
    struct i2c_client *client;
    struct i2c_adapter *adap;

    // 根据次设备号获取对应的 I²C 适配器
    adap = i2c_get_adapter(minor);
    if (!adap)
        return -ENODEV;  // 如果适配器不存在,返回 -ENODEV 表示没有设备

    /* 创建一个匿名的 i2c_client 结构体,该结构体仅在用户空间与 I2C
     * 设备通信时使用,并不会注册到内核的 I²C 驱动模型中。
     */
    client = kzalloc(sizeof(*client), GFP_KERNEL);
    if (!client) {
        i2c_put_adapter(adap);  // 如果内存分配失败,释放适配器并返回 -ENOMEM
        return -ENOMEM;
    }

    // 将适配器编号和 "i2c-dev" 作为客户端的名字
    snprintf(client->name, I2C_NAME_SIZE, "i2c-dev %d", adap->nr);

    // 将 I²C 适配器指针存储到客户端结构体中
    client->adapter = adap;

    // 将创建的匿名客户端结构体存储在 file->private_data 中,供后续操作使用
    file->private_data = client;

    return 0;  // 成功返回 0
}
  • 匿名客户端 :此处创建的 i2c_client 结构体是匿名的,它不会注册到 I²C 驱动模型或内核的 I²C 核心代码中。这意味着该客户端仅用于用户空间与 I²C 适配器的通信,不会影响系统中的其他 I²C 驱动和设备。
  • 用例 :创建匿名客户端允许用户通过字符设备接口(如 /dev/i2c-X)对 I²C 总线进行操作,用户可以通过 ioctl 等系统调用向特定的从设备发送命令。

i2cdev_open 的核心功能是为打开的 I²C 设备文件创建一个匿名的 I²C 客户端(i2c_client),该客户端只用于当前文件操作的上下文中,允许用户通过字符设备与 I²C 适配器通信。

2. i2cdev_ioctl

i2cdev_ioctl 函数负责处理来自用户空间的 ioctl 系统调用,允许用户通过 I²C 设备文件对 I²C 设备进行各种控制操作。它主要基于用户传入的命令 (cmd) 来执行不同的功能。 先看下代码,这里添加了一些自己理解的注释,具体的参数在下面小点讲解

c 复制代码
static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct i2c_client *client = file->private_data; // 获取当前文件对应的 I²C 客户端
    unsigned long funcs;

    // 输出调试信息,显示 ioctl 调用的命令和参数
    dev_dbg(&client->adapter->dev, "ioctl, cmd=0x%02x, arg=0x%02lx\n", cmd, arg);

    switch (cmd) {
    case I2C_SLAVE:
    case I2C_SLAVE_FORCE:
        if ((arg > 0x3ff) || (((client->flags & I2C_M_TEN) == 0) && arg > 0x7f))
            return -EINVAL; // 检查设备地址的有效性
        if (cmd == I2C_SLAVE && i2cdev_check_addr(client->adapter, arg))
            return -EBUSY;  // 检查地址是否忙
        client->addr = arg; // 设置客户端的 I²C 地址
        return 0;

    case I2C_TENBIT:
        if (arg)
            client->flags |= I2C_M_TEN; // 启用 10 位地址模式
        else
            client->flags &= ~I2C_M_TEN; // 禁用 10 位地址模式
        return 0;

    case I2C_PEC:
        if (arg)
            client->flags |= I2C_CLIENT_PEC; // 启用 PEC 校验
        else
            client->flags &= ~I2C_CLIENT_PEC; // 禁用 PEC 校验
        return 0;

    case I2C_FUNCS:
        funcs = i2c_get_functionality(client->adapter); // 获取适配器的功能集
        return put_user(funcs, (unsigned long __user *)arg); // 将功能集返回给用户空间

    case I2C_RDWR:
        return i2cdev_ioctl_rdwr(client, arg); // 执行多字节读写操作

    case I2C_SMBUS:
        return i2cdev_ioctl_smbus(client, arg); // 处理 SMBus 操作

    case I2C_RETRIES:
        client->adapter->retries = arg; // 设置重试次数
        break;

    case I2C_TIMEOUT:
        client->adapter->timeout = msecs_to_jiffies(arg * 10); // 设置超时时间,单位为 10 毫秒
        break;

    default:
        return -ENOTTY; // 对于未识别的命令,返回 `-ENOTTY` 表示不支持此 ioctl
    }

    return 0;
}

2.1 i2cdev_ioctl: I2C_SLAVE/I2C_SLAVE_FORCE

**I2C_SLAVE** **I2C_SLAVE_FORCE** 命令

  • 这两个命令用于设置 I²C 设备的从设备地址(arg 参数)。
  • I2C_SLAVE:在设置地址之前检查是否有其他设备占用该地址,如果占用则返回 -EBUSY
  • I2C_SLAVE_FORCE:强制设置地址,不做占用检查。
  • 如果地址超出 7 位(标准地址模式)或者 10 位(十位地址模式),返回 -EINVAL 表示参数无效。

2.2 i2cdev_ioctl: I2C_RDWR

发起I2C传输

2C_RDWR 命令

  • 这个命令用于执行读写操作,调用 i2cdev_ioctl_rdwr() 实现。用户app调用ioctl时使用这个参数,可以触发读写操作。

来看一下i2cdev_ioctl_rdwr函数, 处理用户空间发起的 I2C_RDWR 命令的函数,它执行多条 I²C 消息的读写操作。这是通过 ioctl 调用实现的,用于对 I²C 总线进行低层次的数据操作。函数主要完成从用户空间获取请求,执行 I²C 传输,并将结果返回给用户空间。

c 复制代码
static noinline int i2cdev_ioctl_rdwr(struct i2c_client *client,
		unsigned long arg)
{
	struct i2c_rdwr_ioctl_data rdwr_arg;
	struct i2c_msg *rdwr_pa;  //传输的基本单位,msg
	u8 __user **data_ptrs;
	int i, res;

	// 从用户空间复制数据到 rdwr_arg
	if (copy_from_user(&rdwr_arg,
			   (struct i2c_rdwr_ioctl_data __user *)arg,
			   sizeof(rdwr_arg)))
		return -EFAULT;

	// 检查消息数量是否超过限制
	if (rdwr_arg.nmsgs > I2C_RDWR_IOCTL_MAX_MSGS)
		return -EINVAL;

	// 从用户空间复制消息结构数组到内核空间
	rdwr_pa = memdup_user(rdwr_arg.msgs,
			      rdwr_arg.nmsgs * sizeof(struct i2c_msg));
	if (IS_ERR(rdwr_pa))
		return PTR_ERR(rdwr_pa);

	// 为数据指针数组分配内存
	data_ptrs = kmalloc(rdwr_arg.nmsgs * sizeof(u8 __user *), GFP_KERNEL);
	if (data_ptrs == NULL) {
		kfree(rdwr_pa);
		return -ENOMEM;
	}

	res = 0;
	// 遍历每条 I²C 消息
	for (i = 0; i < rdwr_arg.nmsgs; i++) {
		// 检查消息长度是否合法
		if (rdwr_pa[i].len > 8192) {
			res = -EINVAL;
			break;
		}

		// 保存用户空间指针,并从用户空间复制消息的缓冲区
		data_ptrs[i] = (u8 __user *)rdwr_pa[i].buf;
		rdwr_pa[i].buf = memdup_user(data_ptrs[i], rdwr_pa[i].len);
		if (IS_ERR(rdwr_pa[i].buf)) {
			res = PTR_ERR(rdwr_pa[i].buf);
			break;
		}

		// 如果消息长度是由从设备决定的,需要处理接收缓冲区
		if (rdwr_pa[i].flags & I2C_M_RECV_LEN) {
			if (!(rdwr_pa[i].flags & I2C_M_RD) ||
			    rdwr_pa[i].buf[0] < 1 ||
			    rdwr_pa[i].len < rdwr_pa[i].buf[0] +
					     I2C_SMBUS_BLOCK_MAX) {
				res = -EINVAL;
				break;
			}
			rdwr_pa[i].len = rdwr_pa[i].buf[0]; // 设置接收长度
		}
	}

	// 如果发生错误,释放分配的内存
	if (res < 0) {
		int j;
		for (j = 0; j < i; ++j)
			kfree(rdwr_pa[j].buf);
		kfree(data_ptrs);
		kfree(rdwr_pa);
		return res;
	}

	// 调用内核的 i2c_transfer 函数进行 I²C 传输
	res = i2c_transfer(client->adapter, rdwr_pa, rdwr_arg.nmsgs);

	// 传输完成后,将数据从内核空间复制回用户空间
	while (i-- > 0) {
		if (res >= 0 && (rdwr_pa[i].flags & I2C_M_RD)) {
			if (copy_to_user(data_ptrs[i], rdwr_pa[i].buf,
					 rdwr_pa[i].len))
				res = -EFAULT;
		}
		kfree(rdwr_pa[i].buf); // 释放消息缓冲区
	}
	kfree(data_ptrs); // 释放数据指针数组
	kfree(rdwr_pa);   // 释放消息结构数组
	return res;
}

2.3 i2cdev_ioctl: I2C_SMBUS

发起SMBus传输

当用户app调用了ioctl时传的参数是I2C_SMBUS,设备驱动程序中就会调用i2cdev_ioctl_smbus来发起smbus传输,i2cdev_ioctl_smbus在i2c-dev.c中定义如下,这里添加了一些自己理解的注释:

c 复制代码
static noinline int i2cdev_ioctl_smbus(struct i2c_client *client,
		unsigned long arg)
{
	struct i2c_smbus_ioctl_data data_arg;
	union i2c_smbus_data temp = {};
	int datasize, res;

	// 从用户空间获取 SMBus 请求数据
	if (copy_from_user(&data_arg,
			   (struct i2c_smbus_ioctl_data __user *) arg,
			   sizeof(struct i2c_smbus_ioctl_data)))
		return -EFAULT;

	// 检查数据大小是否在有效范围内
	if ((data_arg.size != I2C_SMBUS_BYTE) &&
	    (data_arg.size != I2C_SMBUS_QUICK) &&
	    (data_arg.size != I2C_SMBUS_BYTE_DATA) &&
	    (data_arg.size != I2C_SMBUS_WORD_DATA) &&
	    (data_arg.size != I2C_SMBUS_PROC_CALL) &&
	    (data_arg.size != I2C_SMBUS_BLOCK_DATA) &&
	    (data_arg.size != I2C_SMBUS_I2C_BLOCK_BROKEN) &&
	    (data_arg.size != I2C_SMBUS_I2C_BLOCK_DATA) &&
	    (data_arg.size != I2C_SMBUS_BLOCK_PROC_CALL)) {
		dev_dbg(&client->adapter->dev,
			"size out of range (%x) in ioctl I2C_SMBUS.\n",
			data_arg.size);
		return -EINVAL;
	}

	// 检查读/写标志是否合法
	if ((data_arg.read_write != I2C_SMBUS_READ) &&
	    (data_arg.read_write != I2C_SMBUS_WRITE)) {
		dev_dbg(&client->adapter->dev,
			"read_write out of range (%x) in ioctl I2C_SMBUS.\n",
			data_arg.read_write);
		return -EINVAL;
	}

	// 如果是 I2C_SMBUS_QUICK 或 I2C_SMBUS_BYTE 的写操作,不使用数据指针
	if ((data_arg.size == I2C_SMBUS_QUICK) ||
	    ((data_arg.size == I2C_SMBUS_BYTE) &&
	    (data_arg.read_write == I2C_SMBUS_WRITE)))
		return i2c_smbus_xfer(client->adapter, client->addr,
				      client->flags, data_arg.read_write,
				      data_arg.command, data_arg.size, NULL);

	// 检查数据指针是否为空
	if (data_arg.data == NULL) {
		dev_dbg(&client->adapter->dev,
			"data is NULL pointer in ioctl I2C_SMBUS.\n");
		return -EINVAL;
	}

	// 根据 SMBus 命令类型确定数据大小
	if ((data_arg.size == I2C_SMBUS_BYTE_DATA) ||
	    (data_arg.size == I2C_SMBUS_BYTE))
		datasize = sizeof(data_arg.data->byte);
	else if ((data_arg.size == I2C_SMBUS_WORD_DATA) ||
		 (data_arg.size == I2C_SMBUS_PROC_CALL))
		datasize = sizeof(data_arg.data->word);
	else // 块数据或块处理调用
		datasize = sizeof(data_arg.data->block);

	// 如果是写操作或处理调用,复制用户空间的数据到内核空间
	if ((data_arg.size == I2C_SMBUS_PROC_CALL) ||
	    (data_arg.size == I2C_SMBUS_BLOCK_PROC_CALL) ||
	    (data_arg.size == I2C_SMBUS_I2C_BLOCK_DATA) ||
	    (data_arg.read_write == I2C_SMBUS_WRITE)) {
		if (copy_from_user(&temp, data_arg.data, datasize))
			return -EFAULT;
	}

	// 处理旧版 I2C 块命令,保持二进制兼容性
	if (data_arg.size == I2C_SMBUS_I2C_BLOCK_BROKEN) {
		data_arg.size = I2C_SMBUS_I2C_BLOCK_DATA;
		if (data_arg.read_write == I2C_SMBUS_READ)
			temp.block[0] = I2C_SMBUS_BLOCK_MAX;
	}

	// 调用内核的 i2c_smbus_xfer 执行 SMBus 传输
	res = i2c_smbus_xfer(client->adapter, client->addr, client->flags,
	      data_arg.read_write, data_arg.command, data_arg.size, &temp);

	// 如果是读取操作,传输完成后,将数据复制回用户空间
	if (!res && ((data_arg.size == I2C_SMBUS_PROC_CALL) ||
		     (data_arg.size == I2C_SMBUS_BLOCK_PROC_CALL) ||
		     (data_arg.read_write == I2C_SMBUS_READ))) {
		if (copy_to_user(data_arg.data, &temp, datasize))
			return -EFAULT;
	}

	return res;
}

i2cdev_ioctl_smbus 函数用于处理用户空间发起的 SMBus(系统管理总线)相关的 ioctl 调用。SMBus 是基于 I²C 总线协议的一种更高层的通信协议,常用于传感器和设备管理器之间的通信。这个函数的主要作用是根据传入的 SMBus 命令,对设备执行相应的读写操作。

2.3 read和write

c 复制代码
static ssize_t i2cdev_read(struct file *file, char __user *buf, size_t count,
		loff_t *offset)
{
	char *tmp;
	int ret;

	struct i2c_client *client = file->private_data;

	// 限制读取数据的大小,防止超过8192字节
	if (count > 8192)
		count = 8192;

	// 分配内核缓冲区用于存储从设备读取的数据
	tmp = kmalloc(count, GFP_KERNEL);
	if (tmp == NULL)
		return -ENOMEM;  // 内存分配失败,返回错误码

	pr_debug("i2c-dev: i2c-%d reading %zu bytes.\n",
		iminor(file_inode(file)), count);

	// 从I2C设备读取数据
	ret = i2c_master_recv(client, tmp, count);   ------(1)
	if (ret >= 0)
		// 将读取的数据复制到用户空间,若失败则返回 -EFAULT
		ret = copy_to_user(buf, tmp, count) ? -EFAULT : ret;

	// 释放内核缓冲区
	kfree(tmp);
	return ret;  // 返回读取的字节数或者错误码
}

static ssize_t i2cdev_write(struct file *file, const char __user *buf,
		size_t count, loff_t *offset)
{
	int ret;
	char *tmp;
	struct i2c_client *client = file->private_data;

	// 限制写入数据的大小,防止超过8192字节
	if (count > 8192)
		count = 8192;

	// 将用户空间的数据复制到内核空间
	tmp = memdup_user(buf, count);
	if (IS_ERR(tmp))
		return PTR_ERR(tmp);  // 内存分配或数据复制失败

	pr_debug("i2c-dev: i2c-%d writing %zu bytes.\n",
		iminor(file_inode(file)), count);

	// 向I2C设备写入数据
	ret = i2c_master_send(client, tmp, count);   ------(2)

	// 释放内核缓冲区
	kfree(tmp);
	return ret;  // 返回写入的字节数或错误码
}

诶???怎么没有看到i2c_transfer函数呢,其实就在i2c_master_recv进而i2c_master_send内,这两个是i2c-core.c核心层提供给设备驱动的接口:

c 复制代码
int i2c_master_recv(const struct i2c_client *client, char *buf, int count)
{
	struct i2c_adapter *adap = client->adapter;
	struct i2c_msg msg;
	int ret;

	msg.addr = client->addr;
	msg.flags = client->flags & I2C_M_TEN;
	msg.flags |= I2C_M_RD;
	msg.len = count;
	msg.buf = buf;

	ret = i2c_transfer(adap, &msg, 1);

	/*
	 * If everything went ok (i.e. 1 msg received), return #bytes received,
	 * else error code.
	 */
	return (ret == 1) ? count : ret;
}

int i2c_master_send(const struct i2c_client *client, const char *buf, int count)
{
	int ret;
	struct i2c_adapter *adap = client->adapter;
	struct i2c_msg msg;

	msg.addr = client->addr;
	msg.flags = client->flags & I2C_M_TEN;
	msg.len = count;
	msg.buf = (char *)buf;

	ret = i2c_transfer(adap, &msg, 1);

	/*
	 * If everything went ok (i.e. 1 msg transmitted), return #bytes
	 * transmitted, else error code.
	 */
	return (ret == 1) ? count : ret;
}

就不用解释吧,其实也很好懂了,设置i2c_msg msg,标志其从设备地址,控制器,读写标志位flags等,然后利用i2c_transfer发起传输。

3. 总结

可以看出,发起i2c传输的函数最基本的就是要牢记:i2c_transferi2c_smbus_xfer,至于发起的是读还是写,就由msg.flags决定,关于msg结构体,在之前的文章中也有讲过,见上文 往期内容 。

相关推荐
长潇若雪1 小时前
指针进阶(四)(C 语言)
c语言·开发语言·经验分享·1024程序员节
Gui林1 小时前
【GL07】C语言要点
c语言·算法
爱编程— 的小李2 小时前
有序序列合并(c语言)
c语言·算法
混迹网络的权某2 小时前
每天一道C语言精选编程题之求数字的每⼀位之和
c语言·开发语言·考研·算法·改行学it·1024程序员节
加载中loading...6 小时前
Linux线程安全(二)条件变量实现线程同步
linux·运维·服务器·c语言·1024程序员节
Wx120不知道取啥名6 小时前
C语言之长整型有符号数与短整型有符号数转换
c语言·开发语言·单片机·mcu·算法·1024程序员节
憧憬一下8 小时前
input子系统的框架和重要数据结构详解
arm开发·嵌入式·c/c++·1024程序员节·linux驱动开发
无际单片机项目实战10 小时前
为什么STM32的HAL库那么难用,ST还是要硬推HAL库?
c语言·stm32·单片机·嵌入式硬件·物联网
小卡皮巴拉10 小时前
【力扣刷题实战】相同的树
c语言·算法·leetcode·二叉树·递归
爱编程— 的小李11 小时前
开关灯问题(c语言)
c语言·算法·1024程序员节