Linux 驱动:RK3399 从零手写 GT911 电容触摸屏驱动(完整可运行)

前言

GT911 是汇顶科技推出的一款高性能电容触摸屏控制器,支持最多 5 点同时触摸,广泛应用于各类嵌入式设备(工业触摸屏、平板、广告机、智能家居中控等)。它基于标准 I2C 总线通信,硬件连接简单,是嵌入式 Linux 开发中最常用的触摸屏方案。

网上绝大多数教程都是直接使用内核自带的 GT9XX 驱动,很少有从零手写驱动的内容。本文基于 RK3399 开发板,从硬件原理、I2C 通信、上电时序、寄存器操作讲起,一步步实现一个完整的 GT911 触摸屏驱动,支持 5 点触摸坐标上报,所有代码均可直接复制运行。

一、开发环境与前期准备

1.1 硬件连接说明

GT911 与 RK3399 的核心连接仅需 4 根线,对应原理图如下:

GT911 引脚 RK3399 引脚 功能说明
VCC 3.3V 电源供电
GND GND 接地
SCL I2C4_SCL I2C 时钟线(本文使用 I2C4 总线)
SDA I2C4_SDA I2C 数据线
INT GPIO1_20 中断引脚,触摸屏有触摸时触发中断
RST GPIO1_13 复位引脚,用于硬件复位 GT911

注意:不同开发板的 I2C 总线编号和 GPIO 引脚可能不同,请根据自己的原理图修改。

1.2 关闭内核自带 GT9XX 驱动(必做)

内核默认自带 GT9XX 驱动,会与我们自己写的驱动冲突,必须先禁用:

  1. 进入内核源码目录,执行make menuconfig

  2. 依次进入:

    复制代码
    Device Drivers
      -> Input device support
        -> Touchscreens
          -> 取消勾选 Goodix GT9xx touchscreen driver
  3. 保存配置,重新编译内核并烧录到开发板

1.3 注释设备树原有 GT9XX 节点

同样,设备树中默认的 GT9XX 节点也需要注释掉,避免设备重复注册:

复制代码
# 编辑RK3399通用设备树文件
vi arch/arm64/boot/dts/rockchip/rk3399-nanopi4-common.dtsi

找到以下节点,全部注释:

cpp 复制代码
/*
gt9xx: goodix_ts@5d {
    compatible = "goodix,gt9xx";
    reg = <0x5d>;
    interrupt-parent = <&gpio1>;
    interrupts = <20 IRQ_TYPE_EDGE_FALLING>;
    goodix,irq-gpio = <&gpio1 20 GPIO_ACTIVE_HIGH>;
    goodix,rst-gpio = <&gpio1 13 GPIO_ACTIVE_LOW>;
    status = "okay";
};
*/

重新编译设备树并烧录。

二、GT911 核心原理与关键时序

2.1 GT911 核心特性

  • 支持最多 5 点同时触摸,可输出每个触点的坐标、面积和压力
  • 工作电压:2.8V~3.3V
  • I2C 通信速率:最高 400KHz
  • 支持两种 I2C 地址:0x14/0x15(对应 0x28/0x29)和0x5d/0x5e(对应 0xBA/0xBB)
  • 内置 ADC 和数字信号处理,无需外部校准

2.2 最关键:上电复位时序(90% 的坑都在这里)

GT911 的 I2C 地址由上电复位时 INT 引脚的电平决定,这是新手最容易踩坑的地方,必须严格按照以下时序操作:

目标地址 复位时序步骤
0x14(0x28) 1. 拉低 RST 引脚 >5ms2. 拉低 INT 引脚 >100us3. 拉高 RST 引脚 >5ms4. 释放 INT 引脚(设为输入)
0x5d(0xBA) 1. 拉低 RST 引脚 >5ms2. 拉高 INT 引脚 >100us3. 拉高 RST 引脚 >5ms4. 释放 INT 引脚(设为输入)

注意:时序的时间要求必须严格满足,否则 GT911 会工作异常,I2C 通信失败。

2.3 核心寄存器说明

GT911 的所有操作都是通过读写寄存器实现,最核心的寄存器如下:

寄存器地址 功能 说明
0x8140~0x8143 产品 ID 读取 4 字节 ASCII 码,正常为 "911"
0x8047~0x80FF 配置寄存器 初始化时写入厂家配置序列,决定屏幕分辨率、触摸灵敏度等
0x814E 状态寄存器 Bit7:1 表示有新的触摸数据;Bit3~0:当前触摸点数读取完坐标后必须写 0 清零
0x814F~0x817D 坐标数据区 每个触点占 8 字节,格式:0x814F+8n:触点 ID 0x8150+8 n:X 坐标低 8 位0x8151+8n:X 坐标高 8 位 0x8152+8n:Y 坐标低 8 位0x8153+8*n:Y 坐标高 8 位

三、I2C 通信基础封装

GT911 通过 I2C 总线与主控通信,我们先封装通用的 I2C 读写寄存器函数,基于内核标准i2c_transfer接口。

3.1 I2C 核心函数与结构体

内核中 I2C 通信的核心函数是i2c_transfer,它通过i2c_msg结构体描述每一次数据传输:

cpp 复制代码
// 写寄存器:先写寄存器地址,再写数据
// 读寄存器:先写寄存器地址,再读数据
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);

struct i2c_msg {
    __u16 addr;    // 从机地址(7位)
    __u16 flags;   // 传输方向:I2C_M_RD=读,0=写
    __u16 len;     // 数据长度
    __u8 *buf;     // 数据缓冲区
};

3.2 封装 GT911 读写函数

cpp 复制代码
/**
 * @brief GT911写多个寄存器
 * @param client I2C客户端指针
 * @param reg 寄存器起始地址(16位)
 * @param buf 待写入数据缓冲区
 * @param len 写入数据长度
 * @return 成功返回0,失败返回负数
 */
static int gt911_write_regs(struct i2c_client *client, u16 reg, u8 *buf, int len)
{
    u8 tx_buf[256];
    struct i2c_msg msg;

    // 组装数据:先写寄存器高8位,再写低8位,最后写数据
    tx_buf[0] = reg >> 8;
    tx_buf[1] = reg & 0xFF;
    memcpy(&tx_buf[2], buf, len);

    msg.addr = client->addr;
    msg.flags = 0; // 写操作
    msg.buf = tx_buf;
    msg.len = len + 2;

    return i2c_transfer(client->adapter, &msg, 1) == 1 ? 0 : -EIO;
}

/**
 * @brief GT911读多个寄存器
 * @param client I2C客户端指针
 * @param reg 寄存器起始地址(16位)
 * @param buf 读取数据缓冲区
 * @param len 读取数据长度
 * @return 成功返回0,失败返回负数
 */
static int gt911_read_regs(struct i2c_client *client, u16 reg, u8 *buf, int len)
{
    u8 reg_buf[2] = {reg >> 8, reg & 0xFF};
    struct i2c_msg msgs[2];

    // 第一步:写寄存器地址
    msgs[0].addr = client->addr;
    msgs[0].flags = 0;
    msgs[0].buf = reg_buf;
    msgs[0].len = 2;

    // 第二步:读数据
    msgs[1].addr = client->addr;
    msgs[1].flags = I2C_M_RD;
    msgs[1].buf = buf;
    msgs[1].len = len;

    return i2c_transfer(client->adapter, msgs, 2) == 2 ? 0 : -EIO;
}

// 封装单个寄存器读写
static inline int gt911_write_reg(struct i2c_client *client, u16 reg, u8 val)
{
    return gt911_write_regs(client, reg, &val, 1);
}

static inline int gt911_read_reg(struct i2c_client *client, u16 reg, u8 *val)
{
    return gt911_read_regs(client, reg, val, 1);
}

四、完整 GT911 触摸屏驱动实现

4.1 驱动整体框架

基于 Linux I2C 驱动分离模型,驱动分为以下几个部分:

  1. 设备树匹配表
  2. probe 函数:设备匹配成功后执行,完成硬件初始化、中断申请、输入设备注册
  3. 中断处理函数:触摸屏触发中断时读取坐标数据,上报给输入子系统
  4. remove 函数:驱动卸载时释放资源

4.2 完整驱动代码

cpp 复制代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/i2c.h>
#include <linux/input.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
#include <linux/of_gpio.h>

// GT911寄存器定义
#define GT911_REG_STATUS    0x814E
#define GT911_REG_POINT_DATA 0x814F
#define GT911_REG_CONFIG    0x8047
#define GT911_CONFIG_LEN    186

// GT911出厂配置序列(HD702屏幕,根据自己的屏幕修改)
static const u8 gt911_config[] = {
    0x50,0x20,0x03,0x00,0x05,0x05,0x34,0x20,0x02,0x2B,0x28,
    0x0F,0x50,0x3C,0x03,0x05,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x18,0x1A,0x1E,0x14,0x8D,0x2D,0x88,0x3A,0x37,0x33,
    0x0F,0x00,0x00,0x00,0x02,0x02,0x2D,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1C,0x41,0x94,0xC5,
    0x02,0x07,0x00,0x00,0x04,0xD6,0x1E,0x1E,0xB6,0x24,0x00,
    0x9F,0x2A,0x00,0x8A,0x32,0x00,0x79,0x3B,0x00,0x79,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x01,0x04,0x05,0x06,0x07,0x08,0x09,0x0C,
    0x0D,0x0E,0x0F,0x10,0x11,0x14,0x15,0xFF,0xFF,0xFF,0xFF,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x27,
    0x26,0x25,0x24,0x23,0x22,0x21,0x20,0x1F,0x1E,0x1C,0x1B,
    0x19,0x13,0x12,0x11,0x10,0x0F,0x0C,0x0A,0x08,0x07,0x06,
    0x04,0x02,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7F,0x01
};

// 驱动私有数据结构体
struct gt911_data {
    struct i2c_client *client;
    struct input_dev *input;
    int irq_gpio;
    int rst_gpio;
};

/**
 * @brief GT911硬件复位并设置I2C地址为0x5d
 * @param data 驱动私有数据
 */
static void gt911_hw_reset(struct gt911_data *data)
{
    // 1. 拉低RST引脚
    gpio_set_value(data->rst_gpio, 0);
    msleep(10); // 大于5ms

    // 2. 拉高INT引脚(设置地址为0x5d)
    gpio_set_value(data->irq_gpio, 1);
    udelay(150); // 大于100us

    // 3. 拉高RST引脚
    gpio_set_value(data->rst_gpio, 1);
    msleep(10); // 大于5ms

    // 4. 释放INT引脚,设为输入
    gpio_direction_input(data->irq_gpio);
    msleep(50); // 等待GT911初始化完成
}

/**
 * @brief 中断处理函数:读取触摸坐标并上报
 */
static irqreturn_t gt911_interrupt(int irq, void *dev_id)
{
    struct gt911_data *data = dev_id;
    u8 buf[40];
    u8 status;
    int touch_num;
    int i;

    // 读取状态寄存器
    gt911_read_reg(data->client, GT911_REG_STATUS, &status);

    // 检查是否有新的触摸数据
    if (!(status & 0x80)) {
        goto out;
    }

    // 获取触摸点数
    touch_num = status & 0x0F;
    if (touch_num > 5) {
        touch_num = 5;
    }

    // 读取所有触点数据
    gt911_read_regs(data->client, GT911_REG_POINT_DATA, buf, touch_num * 8);

    // 上报每个触点的坐标
    for (i = 0; i < touch_num; i++) {
        int x = (buf[1 + 8*i] << 8) | buf[0 + 8*i];
        int y = (buf[3 + 8*i] << 8) | buf[2 + 8*i];
        int id = buf[0 + 8*i] >> 4;

        // 上报坐标
        input_report_abs(data->input, ABS_MT_POSITION_X, x);
        input_report_abs(data->input, ABS_MT_POSITION_Y, y);
        input_report_abs(data->input, ABS_MT_TRACKING_ID, id);
        input_mt_sync(data->input);
    }

    // 上报触摸结束
    input_sync(data->input);

out:
    // 清零状态寄存器,准备下一次触摸
    gt911_write_reg(data->client, GT911_REG_STATUS, 0x00);
    return IRQ_HANDLED;
}

static int gt911_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct gt911_data *data;
    struct input_dev *input;
    int ret;

    // 1. 分配驱动私有数据
    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data) {
        return -ENOMEM;
    }
    data->client = client;
    i2c_set_clientdata(client, data);

    // 2. 从设备树获取GPIO引脚
    data->irq_gpio = of_get_named_gpio(client->dev.of_node, "goodix,irq-gpio", 0);
    data->rst_gpio = of_get_named_gpio(client->dev.of_node, "goodix,rst-gpio", 0);
    if (!gpio_is_valid(data->irq_gpio) || !gpio_is_valid(data->rst_gpio)) {
        dev_err(&client->dev, "Failed to get gpios\n");
        return -EINVAL;
    }

    // 3. 申请并初始化GPIO
    ret = devm_gpio_request(&client->dev, data->rst_gpio, "gt911-rst");
    if (ret) {
        dev_err(&client->dev, "Failed to request rst gpio\n");
        return ret;
    }
    gpio_direction_output(data->rst_gpio, 1);

    ret = devm_gpio_request(&client->dev, data->irq_gpio, "gt911-irq");
    if (ret) {
        dev_err(&client->dev, "Failed to request irq gpio\n");
        return ret;
    }
    gpio_direction_output(data->irq_gpio, 1);

    // 4. 硬件复位GT911
    gt911_hw_reset(data);
    dev_info(&client->dev, "GT911 hardware reset done\n");

    // 5. 写入配置序列
    ret = gt911_write_regs(client, GT911_REG_CONFIG, (u8 *)gt911_config, GT911_CONFIG_LEN);
    if (ret) {
        dev_err(&client->dev, "Failed to write config\n");
        return ret;
    }
    dev_info(&client->dev, "GT911 config written\n");

    // 6. 注册输入设备
    input = devm_input_allocate_device(&client->dev);
    if (!input) {
        return -ENOMEM;
    }
    data->input = input;

    input->name = "Goodix GT911 Touchscreen";
    input->id.bustype = BUS_I2C;

    // 设置支持的事件类型
    set_bit(EV_ABS, input->evbit);
    set_bit(EV_KEY, input->evbit);
    set_bit(BTN_TOUCH, input->keybit);

    // 设置坐标范围(根据自己的屏幕分辨率修改)
    input_set_abs_params(input, ABS_MT_POSITION_X, 0, 800, 0, 0);
    input_set_abs_params(input, ABS_MT_POSITION_Y, 0, 1280, 0, 0);
    input_set_abs_params(input, ABS_MT_TRACKING_ID, 0, 9, 0, 0);

    ret = input_register_device(input);
    if (ret) {
        dev_err(&client->dev, "Failed to register input device\n");
        return ret;
    }

    // 7. 申请中断
    ret = devm_request_threaded_irq(&client->dev, client->irq, NULL, gt911_interrupt,
                                    IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                    "gt911-irq", data);
    if (ret) {
        dev_err(&client->dev, "Failed to request irq\n");
        return ret;
    }

    dev_info(&client->dev, "GT911 touchscreen driver probed successfully\n");
    return 0;
}

static int gt911_remove(struct i2c_client *client)
{
    dev_info(&client->dev, "GT911 touchscreen driver removed\n");
    return 0;
}

// 设备树匹配表
static const struct of_device_id gt911_of_match[] = {
    { .compatible = "goodix,gt911" },
    {},
};
MODULE_DEVICE_TABLE(of, gt911_of_match);

// I2C驱动结构体
static struct i2c_driver gt911_driver = {
    .probe = gt911_probe,
    .remove = gt911_remove,
    .driver = {
        .name = "gt911",
        .of_match_table = gt911_of_match,
    },
};

module_i2c_driver(gt911_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Driver");
MODULE_DESCRIPTION("Goodix GT911 Touchscreen Driver");
MODULE_VERSION("1.0");

4.3 添加设备树节点

在刚才注释掉的原有 GT9XX 节点位置,添加我们自己的设备树节点:

cpp 复制代码
gt911: goodix_ts@5d {
    compatible = "goodix,gt911";
    reg = <0x5d>;
    interrupt-parent = <&gpio1>;
    interrupts = <20 IRQ_TYPE_EDGE_FALLING>;
    goodix,irq-gpio = <&gpio1 20 GPIO_ACTIVE_HIGH>;
    goodix,rst-gpio = <&gpio1 13 GPIO_ACTIVE_LOW>;
    status = "okay";
};

重新编译设备树并烧录。

五、编译、加载与测试

5.1 编写 Makefile

makefile

cs 复制代码
KERNELDIR := /home/xxx/rk3399_kernel
CURRENT_PATH := $(shell pwd)
CROSS_COMPILE := aarch64-linux-gnu-
ARCH := arm64

obj-m += gt911_ts.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

5.2 编译与加载

复制代码
# 编译驱动
make

# 传输到开发板
scp gt911_ts.ko root@开发板IP:/root/

# 加载驱动
insmod gt911_ts.ko

# 查看内核日志
dmesg | tail

正常输出:

复制代码
[12345.678901] GT911 hardware reset done
[12345.789012] GT911 config written
[12345.890123] GT911 touchscreen driver probed successfully

5.3 测试触摸功能

使用evtest工具测试触摸坐标上报:

复制代码
# 安装evtest(如果没有)
apt-get install evtest

# 运行evtest
evtest

选择 GT911 触摸屏对应的输入设备,触摸屏幕即可看到实时的坐标上报信息

复制代码
Event: time 1712345678.123456, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 456
Event: time 1712345678.123456, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 789
Event: time 1712345678.123456, type 3 (EV_ABS), code 57 (ABS_MT_TRACKING_ID), value 0
Event: time 1712345678.123456, type 0 (EV_SYN), code 0 (SYN_REPORT), value 0

六、常见踩坑与排错指南

6.1 I2C 通信失败,返回 - 6

  • 检查 GT911 的电源和 I2C 接线是否正确
  • 严格按照上电复位时序操作,确保时间要求满足
  • 确认 I2C 地址是否正确,可通过i2cdetect -y 4扫描 I2C 总线查看
  • 检查内核是否已经禁用了自带的 GT9XX 驱动

6.2 中断不触发,触摸无反应

  • 检查中断引脚的 GPIO 配置是否正确
  • 确认设备树中中断触发方式为IRQ_TYPE_EDGE_FALLING
  • 用万用表测量 INT 引脚,触摸时是否有电平变化
  • 检查是否在复位后正确释放了 INT 引脚并设为输入

6.3 坐标不准或方向颠倒

  • 修改输入设备的坐标范围:input_set_abs_params
  • 坐标颠倒:交换 X 和 Y 坐标,或用800-x/1280-y反转坐标
  • 触摸偏移:重新校准 GT911,或修改配置序列中的分辨率参数

6.4 只能单点触摸,不能多点

  • 确认配置序列正确,支持多点触摸
  • 检查输入设备是否注册了ABS_MT_TRACKING_ID事件
  • 确保在中断处理函数中循环上报了所有触点的坐标

本文所有代码均基于标准 Linux 内核接口,无平台依赖,只需修改 GPIO 引脚和屏幕分辨率,即可适配所有 ARM 嵌入式开发板。如果本文对你有帮助,欢迎点赞收藏,有任何问题可在评论区交流讨论。

相关推荐
翼龙云_cloud1 天前
亚马逊云代理商:CloudWatch Agent 全解析 5 步实现服务器监控
运维·服务器·云计算·aws·云服务器
Cyber4K1 天前
【Nginx专项】基础入门篇:状态页、微更新、内容替换、读取、压缩及防盗链
linux·运维·服务器·nginx·github
shining1 天前
当拿到一个新服务器时所需准备工作
linux·程序员
门思科技1 天前
LoRaWAN项目无需NS和平台?一体化网关如何简化部署与成本
服务器·网络·物联网
Bruce_Liuxiaowei1 天前
顺藤摸瓜:一次从防火墙告警到设备实物的溯源实战
运维·网络·网络协议·安全
maosheng11461 天前
linux的综合教程(搭建论坛教程)
linux
IpdataCloud1 天前
效果广告中点击IP与转化IP不一致?用IP查询怎么做归因分析?
运维·服务器·网络
Deitymoon1 天前
linux——TCPIP协议原理
linux·网络
独小乐1 天前
018.使用I2C总线EEPROM|千篇笔记实现嵌入式全栈/裸机篇
linux·笔记·单片机·嵌入式硬件·arm·信息与通信
SPC的存折1 天前
2、Docker命令与镜像、容器管理
linux·运维·服务器·docker·容器·eureka