瑞芯微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主板
第181章使用GPIO模拟I2C驱动
I2C通信可以分为硬件I2C和软件I2C。在之前的章节中,我们使用的都是硬件I2C,这意味着无需自己编写相应的I2C时序代码。硬件I2C依赖于微控制器内部的专用硬件模块来处理通信时序,从而简化了开发过程,提高了通信效率和可靠性,而在本章节中将会对GPIO模拟I2C也就是软件I2C进行讲解。由于前面章节的实验中使用的都是I2C1 FT5X06触摸芯片,所以本章节继续使用I2C1进行软件I2C的实验。
181.1 设备树的修改
由于要使用软件I2C,所以要取消掉在设备树中硬件I2C1的使能,具体修改步骤如下:
 首先在源码目录下使用以下命令对topeet_rk3568_lcds.dtsi文件进行修改,找到i2c1节点,将i2c1的status设置为disabled,设置完成如下图所示:


然后重新编译内核源码,得到boot.img镜像,烧写到开发板上,为了方便起见迅为已经将编译好的内核镜像放到了“iTOP-3568开发板\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动程序\111_soft_i2c\01_内核镜像”如下图所示:
 将该镜像烧写到开发板之后没有I2C-1节点就证明修改成功了。
将该镜像烧写到开发板之后没有I2C-1节点就证明修改成功了。

然后使用以下命令查看引脚复用
cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins
 
可以看到I2C1的两个复用引脚GPIO0 B3、GPIO0 B4已经是GPIO功能了。
181.2编写驱动程序
在本小节中将一步步编写模拟I2C驱动程序,最终编写完成的驱动程序存放路径为“iTOP-3568开发板\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动程序\111_soft_i2c\02_module”。
181.2.1 编写驱动框架
首先编写硬件I2C驱动程序框架,在驱动程序中申请GPIO0 B3和GPIO0 B4两个GPIO,并初始化为高电平,编写完成的驱动程序如下所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio.h>
// 定义 I2C 总线的时钟线和数据线对应的 GPIO 引脚编号
#define I2C_SCL 11
#define I2C_SDA 12
// 声明两个 GPIO 描述符变量,用于保存 SCL 和 SDA 引脚的描述符
struct gpio_desc *i2c_scl_desc;
struct gpio_desc *i2c_sda_desc;
// 驱动初始化函数
static int ft5x06_driver_init(void)
{
    // 将 GPIO 编号转换为 GPIO 描述符
    i2c_scl_desc = gpio_to_desc(I2C_SCL);
    if (i2c_scl_desc == NULL) {
        printk("gpio_to_desc error for SCL pin\n");
        return -1;
    }
    i2c_sda_desc = gpio_to_desc(I2C_SDA);
    if (i2c_sda_desc == NULL) {
        printk("gpio_to_desc error for SDA pin\n");
        return -1;
    }
    // 将 GPIO 引脚设置为输出模式,并初始化为高电平
    gpiod_direction_output(i2c_scl_desc, 1);
    gpiod_direction_output(i2c_sda_desc, 1);
    return 0;
}
// 驱动退出函数
static void ft5x06_driver_exit(void)
{
    // 释放 GPIO 描述符
    gpiod_put(i2c_scl_desc);
    gpiod_put(i2c_sda_desc);
}
// 注册驱动初始化和退出函数
module_init(ft5x06_driver_init);
module_exit(ft5x06_driver_exit);
MODULE_LICENSE("GPL");本小节编写的驱动程序重点在ft5x06_driver_init驱动初始化函数函数,下面对ft5x06_driver_init函数进行讲解:
第17-28行使用gpio_to_desc函数将I2C_SCL、I2C_SDA两个GPIO编号转换为GPIO描述符。
第31-32行使用gpiod_direction_output函数将GPIO引脚设置为输出模式并初始化为高电平。
181.2.2 编写起始和终止信号代码
在上个小节中申请了GPIO0 B3和GPIO0 B4两个GPIO,并初始化为高电平,在本小节中继续完善硬件I2C驱动程序,添加起始信号和终止信号相关的代码,起始信号和终止信号通信时序图如下所示:
 起始信号为SDA线从高电平到低电平的跳变,同时SCL线保持高电平,终止信号为为SDA线从低电平到高电平的跳变,同时SCL线保持高电平。然后根据上述时序图完善起始信号i2c_start和终止信号i2c_stop相关的代码,编写完成的驱动程序如下图所示:
起始信号为SDA线从高电平到低电平的跳变,同时SCL线保持高电平,终止信号为为SDA线从低电平到高电平的跳变,同时SCL线保持高电平。然后根据上述时序图完善起始信号i2c_start和终止信号i2c_stop相关的代码,编写完成的驱动程序如下图所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/jiffies.h>
// 定义 I2C 总线的时钟线和数据线对应的 GPIO 引脚编号
#define I2C_SCL 11
#define I2C_SDA 12
// 声明两个 GPIO 描述符变量,用于保存 SCL 和 SDA 引脚的描述符
struct gpio_desc *i2c_scl_desc;
struct gpio_desc *i2c_sda_desc;
// I2C 起始条件函数
void i2c_start(void)
{
    // 将 SCL 和 SDA 引脚设置为输出模式,并初始化为高电平
    // 这是 I2C 总线的空闲状态
    gpiod_direction_output(i2c_scl_desc, 1);
    gpiod_direction_output(i2c_sda_desc, 1);
    mdelay(1); // 延时 1 毫秒
    // 将 SDA 引脚设置为低电平,保持 SCL 为高电平
    // 这将产生 I2C 总线的起始条件
    gpiod_direction_output(i2c_sda_desc, 0);
    mdelay(1); // 延时 1 毫秒
    // 将 SCL 引脚设置为低电平
    // 起始条件建立完成
    gpiod_direction_output(i2c_scl_desc, 0);
    mdelay(1); // 延时 1 毫秒
}
// I2C 停止条件函数
void i2c_stop(void)
{
    // 将 SCL 和 SDA 引脚设置为低电平
    gpiod_direction_output(i2c_scl_desc, 0);
    gpiod_direction_output(i2c_sda_desc, 0);
    mdelay(1); // 延时 1 毫秒
    // 将 SCL 引脚设置为高电平
    gpiod_direction_output(i2c_scl_desc, 1);
    mdelay(1); // 延时 1 毫秒
    // 将 SDA 引脚设置为高电平
    // 这将产生 I2C 总线的停止条件
    gpiod_direction_output(i2c_sda_desc, 1);
    mdelay(1); // 延时 1 毫秒
}
// 驱动初始化函数
static int ft5x06_driver_init(void)
{
    // 将 GPIO 编号转换为 GPIO 描述符
    i2c_scl_desc = gpio_to_desc(I2C_SCL);
    if (i2c_scl_desc == NULL) {
        printk("gpio_to_desc error for SCL pin\n");
        return -1;
    }
    i2c_sda_desc = gpio_to_desc(I2C_SDA);
    if (i2c_sda_desc == NULL) {
        printk("gpio_to_desc error for SDA pin\n");
        return -1;
    }
    // 将 GPIO 引脚设置为输出模式,并初始化为高电平
    // 这是 I2C 总线的空闲状态
    gpiod_direction_output(i2c_scl_desc, 1);
    gpiod_direction_output(i2c_sda_desc, 1);
	i2c_start();
	i2c_stop();
    return 0;
}
// 驱动退出函数
static void ft5x06_driver_exit(void)
{
    // 释放 GPIO 描述符
    gpiod_put(i2c_scl_desc);
    gpiod_put(i2c_sda_desc);
}
// 注册驱动初始化和退出函数
module_init(ft5x06_driver_init);
module_exit(ft5x06_driver_exit);
MODULE_LICENSE("GPL");181.2.3 编写发送和接收应答信号代码
在上个小节中添加了起始信号和终止信号两个函数,本小节继续对硬件I2C驱动进行填充,添加发送和接收应答信号相关的代码,关于应答信号相关的具体时序图如下所示:

当发送设备在第9个时钟脉冲期间释放SDA线时,接收设备可以拉低SDA线并在此时钟高电平期间保持稳定低电平,这就定义了应答信号。如果在第9个时钟脉冲期间SDA线保持高电平,则定义为非应答信号。
编写完成的驱动程序如下图所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/jiffies.h>
// 定义 I2C 总线的时钟线和数据线对应的 GPIO 引脚编号
#define I2C_SCL 11
#define I2C_SDA 12
// 声明两个 GPIO 描述符变量,用于保存 SCL 和 SDA 引脚的描述符
struct gpio_desc *i2c_scl_desc;
struct gpio_desc *i2c_sda_desc;
// I2C 起始条件函数
void i2c_start(void)
{
    // 将 SCL 和 SDA 引脚设置为输出模式,并初始化为高电平
    // 这是 I2C 总线的空闲状态
    gpiod_direction_output(i2c_scl_desc, 1);
    gpiod_direction_output(i2c_sda_desc, 1);
    mdelay(1); // 延时 1 毫秒
    // 将 SDA 引脚设置为低电平,保持 SCL 为高电平
    // 这将产生 I2C 总线的起始条件
    gpiod_direction_output(i2c_sda_desc, 0);
    mdelay(1); // 延时 1 毫秒
    // 将 SCL 引脚设置为低电平
    // 起始条件建立完成
    gpiod_direction_output(i2c_scl_desc, 0);
    mdelay(1); // 延时 1 毫秒
}
// I2C 停止条件函数
void i2c_stop(void)
{
    // 将 SCL 和 SDA 引脚设置为低电平
    gpiod_direction_output(i2c_scl_desc, 0);
    gpiod_direction_output(i2c_sda_desc, 0);
    mdelay(1); // 延时 1 毫秒
    // 将 SCL 引脚设置为高电平
    gpiod_direction_output(i2c_scl_desc, 1);
    mdelay(1); // 延时 1 毫秒
    // 将 SDA 引脚设置为高电平
    // 这将产生 I2C 总线的停止条件
    gpiod_direction_output(i2c_sda_desc, 1);
    mdelay(1); // 延时 1 毫秒
}
// 发送ACK信号
void i2c_send_ack(int ack) {
    // 设置SDA线为输出模式
    gpiod_direction_output(i2c_sda_desc, 0);
    
    if (ack) {
        // 发送ACK信号, SDA线拉低
        gpiod_direction_output(i2c_sda_desc, 0);
    } else {
        // 发送NACK信号, SDA线拉高
        gpiod_direction_output(i2c_sda_desc, 1);
    }
    
    // 拉高SCL线1ms,然后拉低
    gpiod_direction_output(i2c_scl_desc, 1);
    mdelay(1);
    gpiod_direction_output(i2c_scl_desc, 0);
}
// 接收ACK信号
int i2c_recv_ack(void) {
    int value = 0;
    
    // 设置SDA线为输入模式
    gpiod_direction_input(i2c_sda_desc);
    
    // 拉高SCL线1ms
    gpiod_direction_output(i2c_scl_desc, 1);
    mdelay(1);
    
    // 读取SDA线的电平状态
    if (gpiod_get_value(i2c_sda_desc)) {
        value = 1; // 接收到NACK信号
    } else {
        value = 0; // 接收到ACK信号
    }
    
    // 拉低SCL线
    gpiod_direction_output(i2c_scl_desc, 0);
    
    // 设置SDA线为输出模式并拉高
    gpiod_direction_output(i2c_sda_desc, 1);
    
    return value;
}
// 驱动初始化函数
static int ft5x06_driver_init(void)
{
    // 将 GPIO 编号转换为 GPIO 描述符
    i2c_scl_desc = gpio_to_desc(I2C_SCL);
    if (i2c_scl_desc == NULL) {
        printk("gpio_to_desc error for SCL pin\n");
        return -1;
    }
    i2c_sda_desc = gpio_to_desc(I2C_SDA);
    if (i2c_sda_desc == NULL) {
        printk("gpio_to_desc error for SDA pin\n");
        return -1;
    }
    // 将 GPIO 引脚设置为输出模式,并初始化为高电平
    // 这是 I2C 总线的空闲状态
    gpiod_direction_output(i2c_scl_desc, 1);
    gpiod_direction_output(i2c_sda_desc, 1);
	i2c_start();
	i2c_stop();
    return 0;
}
// 驱动退出函数
static void ft5x06_driver_exit(void)
{
    // 释放 GPIO 描述符
    gpiod_put(i2c_scl_desc);
    gpiod_put(i2c_sda_desc);
}
// 注册驱动初始化和退出函数
module_init(ft5x06_driver_init);
module_exit(ft5x06_driver_exit);
MODULE_LICENSE("GPL");181.2.4 编写发送和接收数据函数
在上个小节中添加了接收和发送应答信号两个函数,本小节继续对硬件I2C驱动进行填充,添加发送和接收数据相关的代码,接收和发送相关的时序图如下所示:

1.首先发送一个7位的目标地址,后跟一个读/写方向位(R/W位)。
2.读/写方向位是第8位,0表示写操作(WRITE),1表示读操作(READ)。
编写完成的驱动程序如下图所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/jiffies.h>
// 定义 I2C 总线的时钟线和数据线对应的 GPIO 引脚编号
#define I2C_SCL 11
#define I2C_SDA 12
// 声明两个 GPIO 描述符变量,用于保存 SCL 和 SDA 引脚的描述符
struct gpio_desc *i2c_scl_desc;
struct gpio_desc *i2c_sda_desc;
// I2C 起始条件函数
void i2c_start(void)
{
    // 将 SCL 和 SDA 引脚设置为输出模式,并初始化为高电平
    // 这是 I2C 总线的空闲状态
    gpiod_direction_output(i2c_scl_desc, 1);
    gpiod_direction_output(i2c_sda_desc, 1);
    mdelay(1); // 延时 1 毫秒
    // 将 SDA 引脚设置为低电平,保持 SCL 为高电平
    // 这将产生 I2C 总线的起始条件
    gpiod_direction_output(i2c_sda_desc, 0);
    mdelay(1); // 延时 1 毫秒
    // 将 SCL 引脚设置为低电平
    // 起始条件建立完成
    gpiod_direction_output(i2c_scl_desc, 0);
    mdelay(1); // 延时 1 毫秒
}
// I2C 停止条件函数
void i2c_stop(void)
{
    // 将 SCL 和 SDA 引脚设置为低电平
    gpiod_direction_output(i2c_scl_desc, 0);
    gpiod_direction_output(i2c_sda_desc, 0);
    mdelay(1); // 延时 1 毫秒
    // 将 SCL 引脚设置为高电平
    gpiod_direction_output(i2c_scl_desc, 1);
    mdelay(1); // 延时 1 毫秒
    // 将 SDA 引脚设置为高电平
    // 这将产生 I2C 总线的停止条件
    gpiod_direction_output(i2c_sda_desc, 1);
    mdelay(1); // 延时 1 毫秒
}
// 发送ACK信号
void i2c_send_ack(int ack) {
    // 设置SDA线为输出模式
    gpiod_direction_output(i2c_sda_desc, 0);
    
    if (ack) {
        // 发送ACK信号, SDA线拉低
        gpiod_direction_output(i2c_sda_desc, 0);
    } else {
        // 发送NACK信号, SDA线拉高
        gpiod_direction_output(i2c_sda_desc, 1);
    }
    
    // 拉高SCL线1ms,然后拉低
    gpiod_direction_output(i2c_scl_desc, 1);
    mdelay(1);
    gpiod_direction_output(i2c_scl_desc, 0);
}
// 接收ACK信号
int i2c_recv_ack(void) {
    int value = 0;
    
    // 设置SDA线为输入模式
    gpiod_direction_input(i2c_sda_desc);
    
    // 拉高SCL线1ms
    gpiod_direction_output(i2c_scl_desc, 1);
    mdelay(1);
    
    // 读取SDA线的电平状态
    if (gpiod_get_value(i2c_sda_desc)) {
        value = 1; // 接收到NACK信号
    } else {
        value = 0; // 接收到ACK信号
    }
    
    // 拉低SCL线
    gpiod_direction_output(i2c_scl_desc, 0);
    
    // 设置SDA线为输出模式并拉高
    gpiod_direction_output(i2c_sda_desc, 1);
    
    return value;
}
void i2c_send_data(int data) {
    int i;
    int value;
    // 设置SCL线为输出模式并拉低
    gpiod_direction_output(i2c_scl_desc, 0);
    // 发送8位数据
    for (i = 0; i < 8; i++) {
        // 获取当前位的值
        value = (data << i) & 0x80;
        // 根据当前位的值设置SDA线
        if (value) {
            gpiod_direction_output(i2c_sda_desc, 1);
        } else {
            gpiod_direction_output(i2c_sda_desc, 0);
        }
        // 拉高SCL线1ms,然后拉低
        gpiod_direction_output(i2c_scl_desc, 1);
        mdelay(1);
        gpiod_direction_output(i2c_scl_desc, 0);
        mdelay(1);
    }
}
int i2c_recv_data(void) {
    int i;
    int temp = 0;
    int data = 0;
    // 设置SDA线为输入模式
    gpiod_direction_input(i2c_sda_desc);
    mdelay(1);
    // 接收8位数据
    for (i = 0; i < 8; i++) {
        // 拉低SCL线1ms
        gpiod_direction_output(i2c_scl_desc, 0);
        mdelay(1);
        // 拉高SCL线1ms
        gpiod_direction_output(i2c_scl_desc, 1);
        mdelay(1);
        // 读取SDA线的电平状态
        data = gpiod_get_value(i2c_sda_desc);
        // 根据当前位的值更新接收数据
        if (data) {
            temp = (temp << 1) | data;
        } else {
            temp = (temp << 1) & ~data;
        }
    }
    // 拉低SCL线
    gpiod_direction_output(i2c_scl_desc, 0);
    mdelay(1);
    // 设置SDA线为输出模式并拉高
    gpiod_direction_output(i2c_sda_desc, 1);
    return temp;
}
// 驱动初始化函数
static int ft5x06_driver_init(void)
{
    // 将 GPIO 编号转换为 GPIO 描述符
    i2c_scl_desc = gpio_to_desc(I2C_SCL);
    if (i2c_scl_desc == NULL) {
        printk("gpio_to_desc error for SCL pin\n");
        return -1;
    }
    i2c_sda_desc = gpio_to_desc(I2C_SDA);
    if (i2c_sda_desc == NULL) {
        printk("gpio_to_desc error for SDA pin\n");
        return -1;
    }
    // 将 GPIO 引脚设置为输出模式,并初始化为高电平
    // 这是 I2C 总线的空闲状态
    gpiod_direction_output(i2c_scl_desc, 1);
    gpiod_direction_output(i2c_sda_desc, 1);
    return 0;
}
// 驱动退出函数
static void ft5x06_driver_exit(void)
{
    // 释放 GPIO 描述符
    gpiod_put(i2c_scl_desc);
    gpiod_put(i2c_sda_desc);
}
// 注册驱动初始化和退出函数
module_init(ft5x06_driver_init);
module_exit(ft5x06_driver_exit);
MODULE_LICENSE("GPL");181.2.5 编写FT5X06寄存器读写函数
在上个小节中添加了数据发送和接收两个函数,本小节继续对硬件I2C驱动进行填充,添加FT5X06寄存器读写函数,编写完成的驱动程序如下图所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/jiffies.h>
// 定义 I2C 总线的时钟线和数据线对应的 GPIO 引脚编号
#define I2C_SCL 11
#define I2C_SDA 12
// 声明两个 GPIO 描述符变量,用于保存 SCL 和 SDA 引脚的描述符
struct gpio_desc *i2c_scl_desc;
struct gpio_desc *i2c_sda_desc;
// I2C 起始条件函数
void i2c_start(void)
{
    // 将 SCL 和 SDA 引脚设置为输出模式,并初始化为高电平
    // 这是 I2C 总线的空闲状态
    gpiod_direction_output(i2c_scl_desc, 1);
    gpiod_direction_output(i2c_sda_desc, 1);
    mdelay(1); // 延时 1 毫秒
    // 将 SDA 引脚设置为低电平,保持 SCL 为高电平
    // 这将产生 I2C 总线的起始条件
    gpiod_direction_output(i2c_sda_desc, 0);
    mdelay(1); // 延时 1 毫秒
    // 将 SCL 引脚设置为低电平
    // 起始条件建立完成
    gpiod_direction_output(i2c_scl_desc, 0);
    mdelay(1); // 延时 1 毫秒
}
// I2C 停止条件函数
void i2c_stop(void)
{
    // 将 SCL 和 SDA 引脚设置为低电平
    gpiod_direction_output(i2c_scl_desc, 0);
    gpiod_direction_output(i2c_sda_desc, 0);
    mdelay(1); // 延时 1 毫秒
    // 将 SCL 引脚设置为高电平
    gpiod_direction_output(i2c_scl_desc, 1);
    mdelay(1); // 延时 1 毫秒
    // 将 SDA 引脚设置为高电平
    // 这将产生 I2C 总线的停止条件
    gpiod_direction_output(i2c_sda_desc, 1);
    mdelay(1); // 延时 1 毫秒
}
// 发送ACK信号
void i2c_send_ack(int ack) {
    // 设置SDA线为输出模式
    gpiod_direction_output(i2c_sda_desc, 0);
    
    if (ack) {
        // 发送ACK信号, SDA线拉低
        gpiod_direction_output(i2c_sda_desc, 0);
    } else {
        // 发送NACK信号, SDA线拉高
        gpiod_direction_output(i2c_sda_desc, 1);
    }
    
    // 拉高SCL线1ms,然后拉低
    gpiod_direction_output(i2c_scl_desc, 1);
    mdelay(1);
    gpiod_direction_output(i2c_scl_desc, 0);
}
// 接收ACK信号
int i2c_recv_ack(void) {
    int value = 0;
    
    // 设置SDA线为输入模式
    gpiod_direction_input(i2c_sda_desc);
    
    // 拉高SCL线1ms
    gpiod_direction_output(i2c_scl_desc, 1);
    mdelay(1);
    
    // 读取SDA线的电平状态
    if (gpiod_get_value(i2c_sda_desc)) {
        value = 1; // 接收到NACK信号
    } else {
        value = 0; // 接收到ACK信号
    }
    
    // 拉低SCL线
    gpiod_direction_output(i2c_scl_desc, 0);
    
    // 设置SDA线为输出模式并拉高
    gpiod_direction_output(i2c_sda_desc, 1);
    
    return value;
}
void i2c_send_data(int data) {
    int i;
    int value;
    // 设置SCL线为输出模式并拉低
    gpiod_direction_output(i2c_scl_desc, 0);
    // 发送8位数据
    for (i = 0; i < 8; i++) {
        // 获取当前位的值
        value = (data << i) & 0x80;
        // 根据当前位的值设置SDA线
        if (value) {
            gpiod_direction_output(i2c_sda_desc, 1);
        } else {
            gpiod_direction_output(i2c_sda_desc, 0);
        }
        // 拉高SCL线1ms,然后拉低
        gpiod_direction_output(i2c_scl_desc, 1);
        mdelay(1);
        gpiod_direction_output(i2c_scl_desc, 0);
        mdelay(1);
    }
}
int i2c_recv_data(void) {
    int i;
    int temp = 0;
    int data = 0;
    // 设置SDA线为输入模式
    gpiod_direction_input(i2c_sda_desc);
    mdelay(1);
    // 接收8位数据
    for (i = 0; i < 8; i++) {
        // 拉低SCL线1ms
        gpiod_direction_output(i2c_scl_desc, 0);
        mdelay(1);
        // 拉高SCL线1ms
        gpiod_direction_output(i2c_scl_desc, 1);
        mdelay(1);
        // 读取SDA线的电平状态
        data = gpiod_get_value(i2c_sda_desc);
        // 根据当前位的值更新接收数据
        if (data) {
            temp = (temp << 1) | data;
        } else {
            temp = (temp << 1) & ~data;
        }
    }
    // 拉低SCL线
    gpiod_direction_output(i2c_scl_desc, 0);
    mdelay(1);
    // 设置SDA线为输出模式并拉高
    gpiod_direction_output(i2c_sda_desc, 1);
    return temp;
}
// ft5x06 触摸屏写寄存器函数
void ft5x06_write_reg(int addr, int reg, int value) {
    int ack;
    // 开始 I2C 通信
    i2c_start();
    // 发送触摸屏设备地址(写操作)
    i2c_send_data(addr << 1 | 0x00);
    ack = i2c_recv_ack();
    if (ack) {
        printk("send write + addr error\n");
        goto end;
    }
    // 发送寄存器地址
    i2c_send_data(reg);
    ack = i2c_recv_ack();
    if (ack) {
        printk("send reg error\n");
        goto end;
    }
    // 发送要写入的值
    i2c_send_data(value);
    ack = i2c_recv_ack();
    if (ack) {
        printk("send value error\n");
    }
end:
    // 结束 I2C 通信
    i2c_stop();
}
//  ft5x06 触摸屏读寄存器函数
int ft5x06_read_reg(int addr, int reg) {
    int ack;
    int data;
    // 开始 I2C 通信
    i2c_start();
    // 发送触摸屏设备地址(写操作)
    i2c_send_data(addr << 1 | 0x00);
    ack = i2c_recv_ack();
    if (ack) {
        printk("send write + addr error\n");
        goto end;
    }
    // 发送要读取的寄存器地址
    i2c_send_data(reg);
    ack = i2c_recv_ack();
    if (ack) {
        printk("send reg error\n");
        goto end;
    }
    // 重新开始 I2C 通信,发送读操作地址
    i2c_start();
    i2c_send_data(addr << 1 | 0x01);
    ack = i2c_recv_ack();
    if (ack) {
        printk("send read + addr error\n");
        goto end;
    }
    // 读取寄存器值
    data = i2c_recv_data();
    printk("data is %d\n", data);
    // 发送 ACK 以结束读操作
    i2c_send_ack(0);
end:
    // 结束 I2C 通信
    i2c_stop();
    return data;
}
// 驱动初始化函数
static int ft5x06_driver_init(void)
{
    // 将 GPIO 编号转换为 GPIO 描述符
    i2c_scl_desc = gpio_to_desc(I2C_SCL);
    if (i2c_scl_desc == NULL) {
        printk("gpio_to_desc error for SCL pin\n");
        return -1;
    }
    i2c_sda_desc = gpio_to_desc(I2C_SDA);
    if (i2c_sda_desc == NULL) {
        printk("gpio_to_desc error for SDA pin\n");
        return -1;
    }
    // 将 GPIO 引脚设置为输出模式,并初始化为高电平
    // 这是 I2C 总线的空闲状态
    gpiod_direction_output(i2c_scl_desc, 1);
    gpiod_direction_output(i2c_sda_desc, 1);
	ft5x06_write_reg(0x38,0x80,0x33);
	ft5x06_read_reg(0x38,0x80);
    return 0;
}
// 驱动退出函数
static void ft5x06_driver_exit(void)
{
    // 释放 GPIO 描述符
    gpiod_put(i2c_scl_desc);
    gpiod_put(i2c_sda_desc);
}
// 注册驱动初始化和退出函数
module_init(ft5x06_driver_init);
module_exit(ft5x06_driver_exit);
MODULE_LICENSE("GPL");181.3运行测试
181.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目标文件,如下图所示:
编译完生成ft5x06_driver.ko目标文件,如下图所示:

181.3.2 运行测试
首先启动开发板,开发板启动进入系统之后如下图所示:

然后将上一个小节编译完成的ko文件拷贝到开发板上,拷贝完成如下图所示:

然后使用以下命令加载驱动,加载完成如下图所示:
insmod ft5x06_driver.ko

可以看到这里打印的值为51,换算成16进制为0x33,与驱动程序中写入的值是相同的,这就证明在上个小节中编写的驱动程序是没有问题的。
至此,使用GPIO模拟I2C的驱动代码就测试完成。



















