瑞芯微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节点就证明修改成功了。
然后使用以下命令查看引脚复用
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相关的代码,编写完成的驱动程序如下图所示:
#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目标文件,如下图所示:
181.3.2 运行测试
首先启动开发板,开发板启动进入系统之后如下图所示:
然后将上一个小节编译完成的ko文件拷贝到开发板上,拷贝完成如下图所示:
然后使用以下命令加载驱动,加载完成如下图所示:
insmod ft5x06_driver.ko
可以看到这里打印的值为51,换算成16进制为0x33,与驱动程序中写入的值是相同的,这就证明在上个小节中编写的驱动程序是没有问题的。
至此,使用GPIO模拟I2C的驱动代码就测试完成。