首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

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

  • 25-03-04 14:01
  • 3393
  • 7910
blog.csdn.net

往期内容

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看看定义了哪些函数:

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

1. i2cdev_open

img

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
}
  • 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
  • 匿名客户端:此处创建的 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) 来执行不同的功能。 先看下代码,这里添加了一些自己理解的注释,具体的参数在下面小点讲解

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;
}
  • 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

2.1 i2cdev_ioctl: I2C_SLAVE/I2C_SLAVE_FORCE

img

**I2C_SLAVE** 和 **I2C_SLAVE_FORCE** 命令:

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

2.2 i2cdev_ioctl: I2C_RDWR

发起I2C传输

img

2C_RDWR 命令:

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

来看一下i2cdev_ioctl_rdwr函数, 处理用户空间发起的 I2C_RDWR 命令的函数,它执行多条 I²C 消息的读写操作。这是通过 ioctl 调用实现的,用于对 I²C 总线进行低层次的数据操作。函数主要完成从用户空间获取请求,执行 I²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;
}
  • 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
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87

2.3 i2cdev_ioctl: I2C_SMBUS

发起SMBus传输

img

当用户app调用了ioctl时传的参数是I2C_SMBUS,设备驱动程序中就会调用i2cdev_ioctl_smbus来发起smbus传输,i2cdev_ioctl_smbus在i2c-dev.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;
}
  • 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
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93

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

2.3 read和write

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;  // 返回写入的字节数或错误码
}
  • 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_transfer函数呢,其实就在i2c_master_recv进而i2c_master_send内,这两个是i2c-core.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;
}
  • 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

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

3. 总结img

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

文章知识点与官方知识档案匹配,可进一步学习相关知识
C技能树首页概览220410 人正在系统学习中
注:本文转载自blog.csdn.net的憧憬一下的文章"https://blog.csdn.net/caiji0169/article/details/142962559"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top