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 嵌入式开发板。如果本文对你有帮助,欢迎点赞收藏,有任何问题可在评论区交流讨论。

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式