前言
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 驱动,会与我们自己写的驱动冲突,必须先禁用:
-
进入内核源码目录,执行
make menuconfig -
依次进入:
Device Drivers -> Input device support -> Touchscreens -> 取消勾选 Goodix GT9xx touchscreen driver -
保存配置,重新编译内核并烧录到开发板
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 驱动分离模型,驱动分为以下几个部分:
- 设备树匹配表
- probe 函数:设备匹配成功后执行,完成硬件初始化、中断申请、输入设备注册
- 中断处理函数:触摸屏触发中断时读取坐标数据,上报给输入子系统
- 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 嵌入式开发板。如果本文对你有帮助,欢迎点赞收藏,有任何问题可在评论区交流讨论。