瑞芯微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 = ®_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 = ®_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设备进行读写的实验就完成了。