RK3568驱动指南|第十五篇 I2C-第179章在应用程序中使用I2C

瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。


【公众号】迅为电子

【粉丝群】258811263(加群获取驱动文档+例程)

【视频观看】嵌入式学习之Linux驱动(第十五篇 I2C_全新升级)_基于RK3568

【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板


第179章在应用程序中使用I2C

179.1 ioctl控制I2C

ioctl是设备驱动程序中用来控制设备的接口函数,可以在应用程序中通过ioctl控制I2C控制器从而对I2C设备进行读写。RK3568的I2C控制器节点如下所示:

关于Ioctl函数的相关介绍如下所示:

函数原型:

       int ioctl(int fd, unsigned int cmd, unsigned long args);

头文件

       #include <sys/ioctl.h>

函数作用:

用于向设备发送控制和配置命令。

参数含义:

fd :是用户程序打开设备时返回的文件描述符

cmd :是用户程序对设备的控制命令,

args:应用程序向驱动程序下发的参数,如果传递的参数为指针类型,则可以接收驱动向用户空间传递的数据(在下面的实验中会进行使用)

对于I2C控制器的控制命令CMD定义在“include/uapi/linux/i2c-dev.h”文件中,具体内容如下所示:

#define I2C_RETRIES     0x0701  //设置重试次数,即当从设备没有响应时要重新轮询的次数
#define I2C_TIMEOUT     0x0702  //设置超时时间,单位为 10 毫秒
#define I2C_SLAVE       0x0703	//使用此从机地址
#define I2C_SLAVE_FORCE 0x0706	//强制使用此从机地址
#define I2C_TENBIT      0x0704	//0 表示 7 位地址, 非 0 表示 10 位地址
#define I2C_FUNCS       0x0705	//获取适配器功能掩码
#define I2C_RDWR        0x0707  //执行合并读写传输(只有一个 STOP 信号)
#define I2C_PEC         0x0708	//使用 PEC(校验码)进行 SMBus 传输
#define I2C_SMBUS       0x0720  //执行 SMBus 传输

这里提供了9个ioctl 控制命令,在本章节的读写实验中只会用到I2C_RDWR,用来进行读写传输,而第三个参数args为要传输的数据,数据类型为i2c_rdwr_ioctl_data,该结构体具体内容如下所示:

struct i2c_rdwr_ioctl_data {
    /* 指向 i2c_msg 结构体的指针数组 */
    struct i2c_msg __user *msgs;
    /* i2c_msg 结构体的数量 */
    __u32 nmsgs;
};

这个结构体用于在 I2C_RDWR ioctl 调用中传递 I2C 消息。其中:msgs 是一个指向 i2c_msg 结构体数组的指针,用于存储一个或多个 I2C 消息。nmsgs 是 i2c_msg 结构体数组的长度,即 I2C 消息的数量。

所以跟上面章节编写的FT5X06 I2C通信驱动程序相同,在应用程序中也要编写对应的I2C读写函数,编写完成的读函数 ft5x06_read_reg内容如下所示:

/**
 * @brief 从 I2C 设备的寄存器中读取数据
 * @param fd 打开的 I2C 设备文件描述符
 * @param slave_addr I2C 设备的从机地址
 * @param reg_addr 要读取的寄存器地址
 * @return 寄存器的值
 */
int ft5x06_read_reg(int fd, unsigned char slave_addr, unsigned char reg_addr) {
    unsigned char data;
    struct i2c_rdwr_ioctl_data i2c_msgs;
    int ret;

    // 定义两个 i2c_msg 结构体, 第一个用于写入寄存器地址, 第二个用于读取数据
    struct i2c_msg dev_msgs[] = {
        [0] = {
            .addr = slave_addr,
            .flags = 0,
            .len = sizeof(reg_addr),
            .buf = &reg_addr,
        },
        [1] = {
            .addr = slave_addr,
            .flags = I2C_M_RD, // 设置读取标志
            .len = sizeof(data),
            .buf = &data,
        }
    };

    i2c_msgs.msgs = dev_msgs;
    i2c_msgs.nmsgs = 2; // 两个 i2c_msg 结构体

    // 调用 ioctl 函数执行读取操作
    ret = ioctl(fd, I2C_RDWR, &i2c_msgs);
    if (ret < 0) {
        printf("read error\n");
        return ret;
    }

    return data;
}

编写完成的写函数 ft5x06_write_reg内容如下所示:

/**
 * @brief 向 I2C 设备的寄存器写入数据
 * @param fd 打开的 I2C 设备文件描述符
 * @param slave_addr I2C 设备的从机地址
 * @param reg_addr 要写入的寄存器地址
 * @param data 要写入的数据
 * @param len 数据长度
 */
void ft5x06_write_reg(int fd, unsigned char slave_addr, unsigned char reg_addr, unsigned char *data, int len)
{
    unsigned char buff[256];
    struct i2c_rdwr_ioctl_data i2c_msgs;
    int ret;

    // 定义一个 i2c_msg 结构体, 用于写入寄存器地址和数据
    struct i2c_msg dev_msgs[] = {
        [0] = {
            .addr = slave_addr,
            .flags = 0,
            .len = len + 1,
            .buf = buff,
        }
    };

    // 将寄存器地址和数据拷贝到 buff 数组中
    buff[0] = reg_addr;
    memcpy(&buff[1], data, len);

    i2c_msgs.msgs = dev_msgs;
    i2c_msgs.nmsgs = 1;

    // 调用 ioctl 函数执行写入操作
    ret = ioctl(fd, I2C_RDWR, &i2c_msgs);
    if (ret < 0) {
        printf("write error\n");
    }
}

179.2 编写应用测试程序

在上个小节中讲解了如何使用ioctl控制I2C设备,并填充了相应的读函数和写函数,在本小节将编写一个完整的应用程序,用来测试读函数和写函数能否正常工作。

编写好的测试程序存放位置为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\108_ft5x06_06,具体内容如下所示:

#include <stdio.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>

/**
 * @brief 从 I2C 设备的寄存器中读取数据
 * @param fd 打开的 I2C 设备文件描述符
 * @param slave_addr I2C 设备的从机地址
 * @param reg_addr 要读取的寄存器地址
 * @return 寄存器的值
 */
int ft5x06_read_reg(int fd, unsigned char slave_addr, unsigned char reg_addr) {
    unsigned char data;
    struct i2c_rdwr_ioctl_data i2c_msgs;
    int ret;

    // 定义两个 i2c_msg 结构体, 第一个用于写入寄存器地址, 第二个用于读取数据
    struct i2c_msg dev_msgs[] = {
        [0] = {
            .addr = slave_addr,
            .flags = 0,
            .len = sizeof(reg_addr),
            .buf = &reg_addr,
        },
        [1] = {
            .addr = slave_addr,
            .flags = I2C_M_RD, // 设置读取标志
            .len = sizeof(data),
            .buf = &data,
        }
    };

    i2c_msgs.msgs = dev_msgs;
    i2c_msgs.nmsgs = 2; // 两个 i2c_msg 结构体

    // 调用 ioctl 函数执行读取操作
    ret = ioctl(fd, I2C_RDWR, &i2c_msgs);
    if (ret < 0) {
        printf("read error\n");
        return ret;
    }

    return data;
}

/**
 * @brief 向 I2C 设备的寄存器写入数据
 * @param fd 打开的 I2C 设备文件描述符
 * @param slave_addr I2C 设备的从机地址
 * @param reg_addr 要写入的寄存器地址
 * @param data 要写入的数据
 * @param len 数据长度
 */
void ft5x06_write_reg(int fd, unsigned char slave_addr, unsigned char reg_addr, unsigned char *data, int len)
{
    unsigned char buff[256];
    struct i2c_rdwr_ioctl_data i2c_msgs;
    int ret;

    // 定义一个 i2c_msg 结构体, 用于写入寄存器地址和数据
    struct i2c_msg dev_msgs[] = {
        [0] = {
            .addr = slave_addr,
            .flags = 0,
            .len = len + 1,
            .buf = buff,
        }
    };

    // 将寄存器地址和数据拷贝到 buff 数组中
    buff[0] = reg_addr;
    memcpy(&buff[1], data, len);

    i2c_msgs.msgs = dev_msgs;
    i2c_msgs.nmsgs = 1;

    // 调用 ioctl 函数执行写入操作
    ret = ioctl(fd, I2C_RDWR, &i2c_msgs);
    if (ret < 0) {
        printf("write error\n");
    }
}

int main(int argc, char *argv[])
{
    int fd;
    int ID_G_THGROUP;

    // 打开 I2C 设备文件
    fd = open("/dev/i2c-1", O_RDWR);
    if (fd < 0) {
        printf("open error\n");
        return fd;
    }

    // 向 0x38 地址的寄存器 0x80 写入 0x55
    unsigned char data = 0x55;
    ft5x06_write_reg(fd, 0x38, 0x80, &data, 1);

    // 从 0x38 地址的寄存器 0x80 读取数据
    ID_G_THGROUP = ft5x06_read_reg(fd, 0x38, 0x80);
    printf("ID_G_THGROUP is 0x%02X\n", ID_G_THGROUP);

    return 0;
}

179.3.2 编译应用程序

首先进行应用程序的编译,因为测试APP是要在开发板上运行的,所以需要aarch64-linux-gnu-gcc来编译,输入以下命令,编译完成以后会生成一个app的可执行程序,如下图所示:

 aarch64-linux-gnu-gcc app.c -o app

然后将编译完成的可执行程序拷贝到开发板上.

179.3.3 运行测试

由于本章节是使用应用程序来控制I2C设备,所以并不需要加载对应的驱动,系统启动之后,使用以下命令运行上个小节编译完成的可执行程序,具体如下图所示:

 

可以看到读取到的ID_G_THGROUP变量值为0x55,表示编写的写函数和读函数都正常工作,至此,关于在应用程序中使用I2C实验就完成了。

179.4 通用I2C驱动讲解

在前面的小节中,我们学习了如何使用ioctl函数来控制具体的I2C设备,那在驱动程序中是如何实现该功能的呢?这就需要对Linux源码中的通用I2C驱动程序进行分析了。

通用I2C驱动文件为drivers/i2c/i2c-dev.c,它为 I2C 外设提供了统一的驱动框架,分为 I2C 客户端 (I2C client) 和 I2C 驱动 (I2C driver)。它为上层应用程序提供了通用的设备节点 /dev/i2c-X(X 代表 I2C 总线号)。应用程序可以直接通过打开这个设备节点 /dev/i2c-X,并使用标准的 I/O 操作如 open()、ioctl()、read()、write() 等来与 I2C 从设备进行通信。

该驱动程序一般情况下都是默认使能的,具体路径如下所示,如果进入系统之后发现在/dev目录下没有i2c-X相应的节点,就需要修改内核配置文件了。

然后来对该驱动程序进行分析,首先来看它的入口函数i2c_dev_init,具体内容如下所示:

static int __init i2c_dev_init(void)
{
    int res;

    // 打印内核日志,表示 i2c /dev 条目驱动已经初始化
    printk(KERN_INFO "i2c /dev entries driver\n");

    // 注册字符设备驱动,主设备号为 I2C_MAJOR,次设备号范围为 0 到 I2C_MINORS-1,设备名为"i2c"
    res = register_chrdev_region(MKDEV(I2C_MAJOR, 0), I2C_MINORS, "i2c");
    if (res)
        goto out;

    // 创建一个 class 对象,名称为 "i2c-dev",用于在用户空间创建设备节点
    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_groups 数组设置为该 class 的 dev_groups 属性
    i2c_dev_class->dev_groups = i2c_groups;

    // 注册一个总线通知函数 i2cdev_notifier,用于追踪 i2c 总线上新添加或删除的适配器
    res = bus_register_notifier(&i2c_bus_type, &i2cdev_notifier);
    if (res)
        goto out_unreg_class;

    // 立即绑定已经存在的 i2c 适配器到 i2c 设备
    i2c_for_each_dev(NULL, i2cdev_attach_adapter);

    return 0;

out_unreg_class:
    // 销毁创建的 class 对象
    class_destroy(i2c_dev_class);
out_unreg_chrdev:
    // 注销已注册的字符设备驱动
    unregister_chrdev_region(MKDEV(I2C_MAJOR, 0), I2C_MINORS);
out:
    // 打印初始化失败的内核日志
    printk(KERN_ERR "%s: Driver Initialisation failed\n", __FILE__);
    return res;
}

第9行:注册字符设备驱动,为 i2c 设备创建设备节点。

第13-20行:创建一个 class 对象,用于在用户空间创建设备节点。

第23-25行:调用bus_register_notifier函数注册一个总线通知函数,用于追踪 i2c 总线上新添加或删除的适配器。

第28行:循环遍历I2C适配器并调用i2cdev_attach_adapter函数绑定已经存在的 i2c 适配器到 i2c 设备。

i2cdev_attach_adapter函数的具体内容如下所示:

static int i2cdev_attach_adapter(struct device *dev, void *dummy)
{
    struct i2c_adapter *adap;
    struct i2c_dev *i2c_dev;
    int res;

    // 检查设备类型是否为 i2c_adapter_type,如果不是则返回
    if (dev->type != &i2c_adapter_type)
        return 0;
    adap = to_i2c_adapter(dev);

    // 从 i2c_dev_list 中获取一个空闲的 i2c_dev 结构体
    i2c_dev = get_free_i2c_dev(adap);
    if (IS_ERR(i2c_dev))
        return PTR_ERR(i2c_dev);

    // 初始化 i2c_dev 结构体中的 cdev 字段,设置文件操作函数为 i2cdev_fops
    cdev_init(&i2c_dev->cdev, &i2cdev_fops);
    i2c_dev->cdev.owner = THIS_MODULE;

    // 初始化设备对象 i2c_dev->dev
    device_initialize(&i2c_dev->dev);
    // 设置设备号为主设备号 I2C_MAJOR 和次设备号 adap->nr
    i2c_dev->dev.devt = MKDEV(I2C_MAJOR, adap->nr);
    i2c_dev->dev.class = i2c_dev_class;
    i2c_dev->dev.parent = &adap->dev;
    i2c_dev->dev.release = i2cdev_dev_release;
    // 设置设备名称为 "i2c-{adap->nr}"
    dev_set_name(&i2c_dev->dev, "i2c-%d", adap->nr);

    // 将 i2c_dev 设备添加到设备树中
    res = cdev_device_add(&i2c_dev->cdev, &i2c_dev->dev);
    if (res) {
        // 如果添加失败,则释放 i2c_dev 结构体
        put_i2c_dev(i2c_dev, false);
        return res;
    }

    // 打印调试信息,表示适配器 [adap->name] 已注册为次设备号 adap->nr
    pr_debug("i2c-dev: adapter [%s] registered as minor %d\n",
             adap->name, adap->nr);
    return 0;
}

这个函数的作用是在系统总线上发现新的 i2c 适配器时,为其创建对应的字符设备节点。

第8-10行:检查设备类型是否为 i2c_adapter_type,如果不是则直接返回

第13-15行:从 i2c_dev_list 中获取一个空闲的 i2c_dev 结构体。

第18-19行:初始化 i2c_dev 结构体中的 cdev 字段,设置文件操作函数为 i2cdev_fops。

第22-29行:初始化设备对象 i2c_dev->dev,设置设备号、class、父设备、设备名称等属性。

第32-37行:调用cdev_device_add() 函数,将字符设备与对应的物理设备进行关联,并将 i2c_dev设备添加到设备树中。

i2c_dev结构体中的cdev字段指定的文件操作集结构体为i2cdev_fops,具体内容如下所示:

static const struct file_operations i2cdev_fops = {
	.owner		= THIS_MODULE,
	.llseek		= no_llseek,
	.read		= i2cdev_read,
	.write		= i2cdev_write,
	.unlocked_ioctl	= i2cdev_ioctl,
	.compat_ioctl	= compat_i2cdev_ioctl,
	.open		= i2cdev_open,
	.release	= i2cdev_release,
};

分别实现了常用的open、read、write和ioctl,接下来对上述函数的实现进行讲解,首先来看i2cdev_open函数,函数的具体内容如下所示:

static int i2cdev_open(struct inode *inode, struct file *file)
{
    // 获取次设备号
    unsigned int minor = iminor(inode);
    // 声明 i2c_client 和 i2c_adapter 结构体指针
    struct i2c_client *client;
    struct i2c_adapter *adap;

    // 根据次设备号获取对应的 i2c_adapter
    adap = i2c_get_adapter(minor);
    // 如果没有找到对应的 i2c_adapter,返回 -ENODEV 错误
    if (!adap)
        return -ENODEV;

    /* 
     * 创建一个匿名的 i2c_client 结构体实例
     * 该 i2c_client 实例稍后可以通过 I2C_SLAVE 或 I2C_SLAVE_FORCE 命令设置从设备地址
     * 但是这个 i2c_client 实例永远不会被注册到设备模型或 I2C 核心代码中
     * 它只是持有私有的地址信息和可能的 PEC 标志
     */
    client = kzalloc(sizeof(*client), GFP_KERNEL);
    // 如果内存分配失败,释放 i2c_adapter 并返回 -ENOMEM 错误
    if (!client) {
        i2c_put_adapter(adap);
        return -ENOMEM;
    }
    // 设置 i2c_client 的名称
    snprintf(client->name, I2C_NAME_SIZE, "i2c-dev %d", adap->nr);

    // 将 i2c_adapter 赋值给 i2c_client 的 adapter 字段
    client->adapter = adap;
    // 将 i2c_client 指针保存到 file 的 private_data 字段
    file->private_data = client;

    // 返回成功
    return 0;
}

第4-13行:获取次设备号,根据次设备号获取对应的i2c_adapter实例。

第21-28行:创建一个匿名的i2c_client实例,并设置它的一些基本信息。

第31行:i2c_adapter 赋值给i2c_client的adapter字段。

第33行:将 i2c_client 指针保存到file的私有数据private_data字段。

然后来看I2C读函数i2cdev_read,该函数的具体内容如下所示:

static ssize_t i2cdev_read(struct file *file, char __user *buf, size_t count,
        loff_t *offset)
{
    // 声明一个临时缓冲区指针
    char *tmp;
    // 保存 i2c_master_recv 的返回值
    int ret;

    // 从 file 的 private_data 字段获取 i2c_client 指针
    struct i2c_client *client = file->private_data;

    // 限制最大读取字节数为 8192
    if (count > 8192)
        count = 8192;

    // 分配临时缓冲区
    tmp = kzalloc(count, GFP_KERNEL);
    // 如果内存分配失败,返回 -ENOMEM 错误
    if (tmp == NULL)
        return -ENOMEM;

    // 打印调试信息
    pr_debug("i2c-dev: i2c-%d reading %zu bytes.\n",
        iminor(file_inode(file)), count);

    // 使用 i2c_master_recv 函数从 i2c_client 设备读取数据
    ret = i2c_master_recv(client, tmp, count);
    // 如果读取成功
    if (ret >= 0)
        // 将读取的数据拷贝到用户空间缓冲区
        if (copy_to_user(buf, tmp, ret))
            // 如果拷贝失败,返回 -EFAULT 错误 
            ret = -EFAULT;
    // 释放临时缓冲区
    kfree(tmp);
    // 返回实际读取的字节数,或者错误码
    return ret;
}

在第27行调用了i2c_master_recv函数,该函数在前面I2C子系统框架以及编写I2C通信驱动的章节中讲解过,最后会调用i2c_transfer函数进行I2C数据的读取,最后在第31行调用copy_to_user将读取到的数据拷贝到用户空间。

然后来看I2C写函数i2cdev_write,该函数的具体内容如下所示:

static ssize_t i2cdev_write(struct file *file, const char __user *buf,
        size_t count, loff_t *offset)
{
    // 保存 i2c_master_send 的返回值
    int ret;
    // 声明一个临时缓冲区指针
    char *tmp;
    // 从 file 的 private_data 字段获取 i2c_client 指针
    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_master_send 函数将数据写入 i2c_client 设备
    ret = i2c_master_send(client, tmp, count);
    // 释放临时缓冲区
    kfree(tmp);
    // 返回实际写入的字节数,或者错误码
    return ret;
}

在第26行调用了i2c_master_send函数,该函数和i2c_master_recv函数一样,最后都会调用i2c_transfer函数,只是参数不同这里是将I2C数据写入I2C设备。

最后来看i2cdev_ioctl函数,正是因为i2cdev_ioctl函数,才能够在应用程序中调用ioctl控制I2C设备,该函数的具体内容如下所示:

static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    // 从 file 的 private_data 字段获取 i2c_client 指针
    struct i2c_client *client = file->private_data;
    // 声明一个无符号长整型变量,用于保存设备功能
    unsigned long funcs;

    // 打印调试信息
    dev_dbg(&client->adapter->dev, "ioctl, cmd=0x%02x, arg=0x%02lx\n",
        cmd, arg);

    // 根据不同的 ioctl 命令进行处理
    switch (cmd) {
    case I2C_SLAVE:
    case I2C_SLAVE_FORCE:
        // 检查从设备地址是否合法
        if ((arg > 0x3ff) ||
            (((client->flags & I2C_M_TEN) == 0) && arg > 0x7f))
            return -EINVAL;
        // 如果是 I2C_SLAVE 命令,检查地址是否被占用
        if (cmd == I2C_SLAVE && i2cdev_check_addr(client->adapter, arg))
            return -EBUSY;
        // 设置从设备地址
        client->addr = arg;
        return 0;
    case I2C_TENBIT:
        // 设置 10 位地址模式
        if (arg)
            client->flags |= I2C_M_TEN;
        else
            client->flags &= ~I2C_M_TEN;
        return 0;
    case I2C_PEC:
        // 设置 PEC 标志
        if (arg)
            client->flags |= I2C_CLIENT_PEC;
        else
            client->flags &= ~I2C_CLIENT_PEC;
        return 0;
    case I2C_FUNCS:
        // 获取 i2c 适配器的功能
        funcs = i2c_get_functionality(client->adapter);
        // 将结果写入用户空间的地址
        return put_user(funcs, (unsigned long __user *)arg);
    case I2C_RDWR: {
        // 处理 I2C_RDWR 命令
        struct i2c_rdwr_ioctl_data rdwr_arg;
        struct i2c_msg *rdwr_pa;

        // 从用户空间拷贝参数结构体
        if (copy_from_user(&rdwr_arg,
                (struct i2c_rdwr_ioctl_data __user *)arg,
                sizeof(rdwr_arg)))
            return -EFAULT;

        // 检查参数合法性
        if (!rdwr_arg.msgs || rdwr_arg.nmsgs == 0)
            return -EINVAL;

        // 限制最大消息数为 I2C_RDWR_IOCTL_MAX_MSGS
        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);

        // 调用 i2cdev_ioctl_rdwr 函数进行 i2c 读写操作
        return i2cdev_ioctl_rdwr(client, rdwr_arg.nmsgs, rdwr_pa);
    }
    case I2C_SMBUS: {
        // 处理 I2C_SMBUS 命令
        struct i2c_smbus_ioctl_data data_arg;
        // 从用户空间拷贝参数结构体
        if (copy_from_user(&data_arg,
                (struct i2c_smbus_ioctl_data __user *) arg,
                sizeof(struct i2c_smbus_ioctl_data)))
            return -EFAULT;
        // 调用 i2cdev_ioctl_smbus 函数进行 SMBus 读写操作
        return i2cdev_ioctl_smbus(client, data_arg.read_write,
                data_arg.command,
                data_arg.size,
                data_arg.data);
    }
    case I2C_RETRIES:
        // 设置 i2c 适配器的重试次数
        if (arg > INT_MAX)
            return -EINVAL;
        client->adapter->retries = arg;
        break;
    case I2C_TIMEOUT:
        // 设置 i2c 适配器的超时时间
        if (arg > INT_MAX)
            return -EINVAL;
        // 用户空间设置的单位是 10 ms
        client->adapter->timeout = msecs_to_jiffies(arg * 10);
        break;
    default:
        // 不支持的 ioctl 命令
        return -ENOTTY;
    }
    return 0;
}

在前面应用程序的编写中只调用了I2C_RDWR用来进行I2C数据的读写,所以只需要关注45-72行即可,而这部分的重点在最后一行调用i2cdev_ioctl_rdwr函数进行i2c读写操作,i2cdev_ioctl_rdwr函数最终也是调用了i2c_transfer函数进行的读写,至此对于通用I2C驱动就讲解完成了。

根据本小节的分析,无论是i2cdev_ioctl函数、i2cdev_read函数还是i2cdev_write函数,最终都是调用的i2c_transfer函数进行的读写,所以在下个小节中将会使用i2cdev_read函数和i2cdev_write函数在应用程序中控制I2C。

179.5 编写应用测试程序

在前面个小节中讲解了如何使用ioctl控制I2C设备,而在上个小节中讲解了i2cdev_ioctl函数、i2cdev_read函数和i2cdev_write函数的实现,发现每个函数最终都是由i2c_transfer函数实现的,所以在应用程序中直接使用read函数和write函数也可以在应用程序中使用I2C,本小节将编写对应的测试应用程序。

编写好的测试程序存放位置为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\109_ft5x06_07,具体内容如下所示:

#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <unistd.h>

/*
 * 从 I2C 设备读取寄存器值
 * @param fd: I2C 设备文件句柄
 * @param reg_addr: 要读取的寄存器地址
 */
void ft5x06_read_reg(int fd, unsigned char reg_addr) {
    unsigned char rd_data[1];
    rd_data[0] = reg_addr;
    write(fd, rd_data, 1);
    read(fd, rd_data, 1);
    printf("reg value is %x\n", rd_data[0]);
}

/*
 * 向 I2C 设备写入寄存器值
 * @param fd: I2C 设备文件句柄
 * @param reg_addr: 要写入的寄存器地址
 * @param data: 要写入的数据
 */
void ft5x06_write_reg(int fd, unsigned char reg_addr, unsigned char data) {
    unsigned char wr_data[2];
    wr_data[0] = reg_addr;
    wr_data[1] = data;
    write(fd, wr_data, 2);
}

int main(int argc, char *argv[]) {
    int fd;

    // 打开 I2C 设备文件
    fd = open("/dev/i2c-1", O_RDWR);
    if (fd < 0) {
        printf("open error\n");
        return fd;
    }

    // 设置从设备地址为 0x38
    ioctl(fd, I2C_SLAVE_FORCE, 0x38);

    // 向寄存器 0x80 写入数据 0x66
    ft5x06_write_reg(fd, 0x80, 0x66);

    // 读取寄存器 0x80 的值
    ft5x06_read_reg(fd, 0x80);

    return 0;
}

179.3 运行测试

179.3.2 编译应用程序

首先进行应用程序的编译,因为测试APP是要在开发板上运行的,所以需要aarch64-linux-gnu-gcc来编译,输入以下命令,编译完成以后会生成一个app的可执行程序,如下图所示:

 aarch64-linux-gnu-gcc app.c -o app

然后将编译完成的可执行程序拷贝到开发板上.

179.3.3 运行测试

由于本章节是使用应用程序来控制I2C设备,所以并不需要加载对应的驱动,系统启动之后,使用以下命令运行上个小节编译完成的可执行程序,具体如下图所示:

可以看到读取到的变量值为0x66,表示编写的写函数和读函数都正常工作,至此,关于在应用程序中使用wirte和read对I2C设备进行读写的实验就完成了。 

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值