瑞芯微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主板
第174章 FT5X06驱动程序I2C通信部分编写
174.1 I2C通信
在讲解I2C核心层的时候提到过I2C核心层位于I2C设备驱动层和I2C适配器驱动层中间,起到了承上启下的作用,负责I2C设备驱动层和I2C适配器驱动层之间数据的传递,I2C核心层的主要函数为i2c_master_send、i2c_master_recv和i2c_transfer,其中i2c_master_send和i2c_master_recv函数,是I2C核心层提供的基本读写接口。这两个函数负责生成符合I2C协议的时序和数据帧,并通过对应的I2C适配器驱动程序进行实际的总线操作。两个函数定义在“include/linux/i2c.h”文件当中给,具体内容如下所示:
static inline int i2c_master_recv(const struct i2c_client *client,
char *buf, int count)
{
return i2c_transfer_buffer_flags(client, buf, count, I2C_M_RD);
};
static inline int i2c_master_send(const struct i2c_client *client,
const char *buf, int count)
{
return i2c_transfer_buffer_flags(client, (char *)buf, count, 0);
};
可以看到两个函数都是调用的i2c_transfer_buffer_flags函数,该函数定义在“drivers/i2c/i2c-core-base.c”文件中,具体内容如下所示:
int i2c_transfer_buffer_flags(const struct i2c_client *client, char *buf,
int count, u16 flags)
{
int ret; // 保存传输结果
// 构建i2c_msg结构体,描述本次传输操作
struct i2c_msg msg = {
.addr = client->addr, // 设置从设备地址
// 设置传输标志位,包括用户传入的标志位和客户端对象自身的标志位
.flags = flags | (client->flags & I2C_M_TEN),
.len = count, // 设置传输数据长度
.buf = buf, // 设置数据缓冲区
};
// 调用i2c_transfer函数进行数据传输
// 该函数会根据传输的消息数量返回实际传输成功的消息数量
ret = i2c_transfer(client->adapter, &msg, 1);
/*
* 如果传输成功(即传输了1条消息),返回传输的字节数;
* 否则返回错误码。
*/
return (ret == 1) ? count : ret;
}
这个函数用于通过I2C总线传输数据。它首先构建了一个i2c_msg结构体,描述本次传输操作,包括从设备地址、传输标志位、数据长度和数据缓冲区。然后调用i2c_transfer函数进行实际的数据传输。
综上i2c_master_send和i2c_master_recv函数实际上便是调用的i2c_transfer,在后面的实验中我们并不会直接使用i2c_master_send和i2c_master_recv函数,而是通过i2c_transfer函数来编写I2C发送和接收的函数。
i2c_transfer函数定义在“drivers/i2c/i2c-core-base.c”文件中,具体内容如下所示:
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
int ret; // 保存传输结果
/*
* 这里存在一些故障报告模型的弱点:
* 1. 当我们在从设备接收N个字节后出错时,没有办法报告"N"。
* 2. 当我们在向从设备传输N个字节后收到NAK时,没有办法报告"N",
* 也没有办法让主设备继续执行剩余的消息(如果这是适当的响应)。
* 3. 当"num"为两个,我们成功完成第一条消息但在第二条消息中途出错时,
* 不清楚应该报告为一个(丢弃第二条消息的状态)还是errno(丢弃第一条消息的状态)。
*/
// 如果适配器支持master_xfer操作,则使用该操作传输消息
if (adap->algo->master_xfer) {
#ifdef DEBUG
// 打印每条消息的信息,用于调试
for (ret = 0; ret < num; ret++) {
dev_dbg(&adap->dev,
"master_xfer[%d] %c, addr=0x%02x, len=%d%s\n",
ret, (msgs[ret].flags & I2C_M_RD) ? 'R' : 'W',
msgs[ret].addr, msgs[ret].len,
(msgs[ret].flags & I2C_M_RECV_LEN) ? "+" : "");
}
#endif
// 如果当前上下文不允许睡眠,则尝试锁定I2C总线
// 如果总线已经被锁定,返回-EAGAIN
if (in_atomic() || irqs_disabled()) {
ret = i2c_trylock_bus(adap, I2C_LOCK_SEGMENT);
if (!ret)
return -EAGAIN;
} else {
// 否则,锁定I2C总线
i2c_lock_bus(adap, I2C_LOCK_SEGMENT);
}
// 调用__i2c_transfer执行实际的消息传输
ret = __i2c_transfer(adap, msgs, num);
// 解锁I2C总线
i2c_unlock_bus(adap, I2C_LOCK_SEGMENT);
return ret;
} else {
// 如果适配器不支持master_xfer操作,返回不支持的错误
dev_dbg(&adap->dev, "I2C level transfers not supported\n");
return -EOPNOTSUPP;
}
}
该函数有三个参数,每个参数的具体作用如下所示:
(1)struct i2c_adapter *adap: 表示要使用的 I2C 适配器。每个 I2C 控制器对应一个 i2c_adapter 结构体,里面包含了这个适配器的各种属性和操作函数。
(2)struct i2c_msg *msgs: 指向一个 i2c_msg 结构体数组,用于描述要传输的一个或多个 I2C 消息。i2c_msg 结构体具体内容如下所示:
struct i2c_msg {
__u16 addr; // 从设备的7位地址
__u16 flags; // 消息标志位
#define I2C_M_RD 0x0001 // 表示读操作,必须是0x0001
#define I2C_M_TEN 0x0010 // 使用10位从设备地址,需要I2C_FUNC_10BIT_ADDR支持
#define I2C_M_DMA_SAFE 0x0200 // 表示数据缓冲区是安全的(仅在内核空间使用)
#define I2C_M_RECV_LEN 0x0400 // 表示在读操作中需要接收可变长度的数据
#define I2C_M_NO_RD_ACK 0x0800 // 表示不需要在读操作后接收ACK
#define I2C_M_IGNORE_NAK 0x1000 // 表示即使收到NAK也要继续传输
#define I2C_M_REV_DIR_ADDR 0x2000// 表示地址字节需要反转,
#define I2C_M_NOSTART 0x4000 // 表示不需要发送起始条件
#define I2C_M_STOP 0x8000 // 表示在消息传输后需要发送停止条
__u16 len; // 要传输的数据长度(字节数)
__u8 *buf; // 指向要传输的数据缓冲区
};
注:在该结构体中没有写操作,根据i2c_master_send函数可以得到0为写操作。
(3)int num: 数组 msgs 中包含的消息个数。
174.2 完善I2C通信部分驱动
本实验修改完成的驱动对应的网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动程序\105_ft5x06_04\。
在上一章节中完善了FT5X06 两个GPIO相关的设备树节点和驱动部分,而本章节将会进一步完善FT5X06 关于I2C通信相关的驱动,添加FT5X06 I2C读和写两个函数,并在probe初始化函数调用并测试。编写完成的驱动程序如下所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
// 定义 ft5x06 设备的 GPIO 描述符
// 用于保存 reset 和 interrupt 引脚的 GPIO 描述符
struct gpio_desc *reset_gpio, *irq_gpio;
// 保存 ft5x06 设备的 i2c 客户端对象指针
struct i2c_client *ft5x06_client;
// 读取 ft5x06 设备寄存器的函数
int ft5x06_read_reg(u8 reg_addr)
{
u8 data;
// 定义两个 i2c_msg 结构体,分别表示写操作和读操作
struct i2c_msg msgs[2] = {
[0] = {
.addr = ft5x06_client->addr, // 设备地址
.flags = 0, // 写操作
.len = sizeof(reg_addr),
.buf = ®_addr, // 写入要读取的寄存器地址
},
[1] = {
.addr = ft5x06_client->addr,
.flags = I2C_M_RD, // 读操作
.len = sizeof(data),
.buf = &data, // 读取到的数据存储位置
},
};
// 使用 i2c_transfer 函数进行 i2c 总线读取操作
// 如果读取失败,返回 -EIO 错误码
if (i2c_transfer(ft5x06_client->adapter, msgs, ARRAY_SIZE(msgs)) != ARRAY_SIZE(msgs)){
return -EIO;
}
return data; // 返回读取到的寄存器值
}
// 向 ft5x06 设备写入寄存器的函数
void ft5x06_write_reg(u8 reg_addr, u8 *data, u16 len)
{
u8 buff[256];
struct i2c_msg msgs[] = {
[0] = {
.addr = ft5x06_client->addr, // 设备地址
.flags = 0, // 写操作
.len = len + 1, // 数据长度 + 寄存器地址长度
.buf = buff, // 数据缓冲区
},
};
buff[0] = reg_addr; // 写入寄存器地址
memcpy(&buff[1], data, len); // 写入数据
// 使用 i2c_transfer 函数进行 i2c 总线写入操作
// 如果写入失败,直接返回
if (i2c_transfer(ft5x06_client->adapter, msgs, ARRAY_SIZE(msgs)) != ARRAY_SIZE(msgs)) {
return;
}
}
// ft5x06 中断处理函数
irqreturn_t ft5x06_handler(int irq, void *args)
{
printk("This is ft5x06 handler\n");
// 返回中断已处理标志
return IRQ_RETVAL(IRQ_HANDLED);
}
// ft5x06 设备的探测函数
int ft5x06_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret,value;
printk("This is ft5x06 probe\n");
ft5x06_client = client; // 保存 i2c 客户端对象指针
// 获取 reset GPIO 描述符
reset_gpio = gpiod_get_optional(&client->dev, "reset", 0);
if (!reset_gpio) {
printk("gpiod_get_optional reset gpio is error\n");
return -1;
}
// 获取 irq GPIO 描述符
irq_gpio = gpiod_get_optional(&client->dev, "interrupts", 0);
if (!irq_gpio) {
printk("gpiod_get_optional irq gpio is error\n");
return -1;
}
// 设置 reset GPIO 为输出,并拉低 5ms 后拉高
gpiod_direction_output(reset_gpio, 0);
msleep(5);
gpiod_direction_output(reset_gpio, 1);
// 请求中断,设置为下降沿触发,单次触发
ret = request_irq(client->irq, ft5x06_handler,
IRQ_TYPE_EDGE_FALLING | IRQF_ONESHOT, "ft5x06 irq", NULL);
if (ret < 0) {
printk("request irq is error\n");
return -2;
}
// 写寄存器
ft5x06_write_reg(0x80, &(u8){0x4b}, 1);
// 读寄存器
value = ft5x06_read_reg(0x80);
printk("reg 0x80 is %x\n", value);
return 0;
}
// ft5x06 设备的移除函数
// 参数 client 是 i2c 客户端对象指针
int ft5x06_remove(struct i2c_client *client)
{
// 释放中断
free_irq(client->irq, NULL);
// 释放 GPIO 资源
gpiod_put(reset_gpio);
gpiod_put(irq_gpio);
return 0;
}
// 定义 i2c_device_id 结构体数组,用于标识 ft5x06 设备
static const struct i2c_device_id ft5x06_id[] = {
{ "my-ft5x06", 0 },
{ }
};
// 定义 i2c_driver 结构体,描述 ft5x06 设备驱动
static struct i2c_driver ft5x06_driver = {
.driver = {
.name = "my-ft5x06",
.owner = THIS_MODULE,
},
.probe = ft5x06_probe,
.remove = ft5x06_remove,
.id_table = ft5x06_id,
};
// 驱动初始化函数
static int __init ft5x06_driver_init(void)
{
int ret;
// 注册 I2C 设备驱动
ret = i2c_add_driver(&ft5x06_driver);
if (ret < 0) {
printk("i2c_add_driver is error\n");
return ret;
}
return 0;
}
// 驱动退出函数
static void __exit ft5x06_driver_exit(void)
{
// 注销 I2C 设备驱动
i2c_del_driver(&ft5x06_driver);
}
module_init(ft5x06_driver_init);
module_exit(ft5x06_driver_exit);
MODULE_LICENSE("GPL");
173.3 运行测试
173.3.1 编译驱动程序
首先在上一小节中的ft5x06_driver.c代码同一目录下创建 Makefile 文件,Makefile 文件内容如下所示:
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m += ft5x06_driver.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
对于Makefile的内容注释已在上图添加,保存退出之后,来到存放platform_driver.c和Makefile文件目录下,如下图所示:
然后使用命令“make”进行驱动的编译,编译完成如下图所示:
编译完生成ft5x06_driver.ko目标文件,如下图所示:
173.3.2 运行测试
首先启动开发板,开发板启动进入系统之后如下图所示:
然后将上一个小节编译完成的ko文件拷贝到开发板上,拷贝完成如下图所示:
然后使用以下命令加载驱动,加载完成如下图所示:
insmod ft5x06_driver.ko
从上面的打印结果可以得到,写函数和读函数运行成功,成功向0x80地址写入了0x4b,然后使用以下命令进行驱动模块的卸载,如下图所示:
rmmod ft5x06_driver.ko
由于没有在remove卸载函数中添加打印相关内容,所以使用rmmod命令卸载驱动之后,没有任何打印,至此,FT5X06 I2C驱动读写实验就完成了。