上篇文章介绍了LCD屏幕的使用,这个屏幕还有触摸功能,本篇就来介绍LCD的触摸功能的使用。

关于触摸的内容有点多,分为上下两篇进行讲解,本篇先介绍触摸驱动的编写以及将触摸点坐标实时打印出来进行测试,先有一个整体的使用感受,下篇文章再介绍具体的触摸上报协议以及图形化的测试方法

[TOC]

1 触摸介绍

LCD的触摸功能,本质就是显示屏上再叠加一层透明的触摸屏,实现触摸的方式与LCD进行交互。

触摸屏分为电阻触摸屏电容触摸屏

  • 电阻触摸屏是一种传感器,其结构是薄膜加上玻璃的结构,两结构相邻的一面上均涂有ITO(一种导电性和透明性很好的)涂层。当触摸操作时,两层结构挤压接触,经由感应器传出相应的电信号,通过运算转化为屏幕上的X、Y值。
  • 电容技术触摸屏CTP(Capacity Touch Panel)是利用人体的电流感应进行工作的。电容屏是一块四层复合玻璃屏,电容式触摸屏就是支持多点触摸的人机交互方式,普通电阻式触摸屏只能进行单一点的触控。

1.1 硬件原理图

本篇使用的是野火的7寸电容触摸屏,分辨率和屏幕一样,800x480。触摸驱动芯片我GT911,是IIC接口的芯片。

触摸芯片有四个引脚:

  • SDA:触摸芯片的IIC 通信引脚
  • SCL:触摸芯片的IIC 通信引脚
  • RSTN:触摸芯片的复位引脚
  • INT:触摸芯片的中断引脚

对应板子原理图的触摸接口如下:

对应屏幕原理图的触摸接口如下:

2 编写触摸驱动代码

触摸芯片用到IIC通信,还要用到复位引脚中断引脚,因此需要先在设备树中对引脚信息进行配置。

2.1 修改设备树

修改imx6ull_myboard.dts文件。

在设备树中把触摸要用到的引脚追加到 iomuxc即可。

引脚 功能
UART4_RX_DATA 复用为 I2C1_SDA,用作 IIC1 的 SDA 引脚
UART4_TX_DATA 复用为 I2C1_SCL,用作 IIC1 的 SCL 引脚
LCD_RST 复用为 GPIO3_IO04 用作触摸芯片的复位引脚
SNVS_TAMPER9 复用为 GPIO5_IO09 用作触摸芯片的 irq 引脚,接收触摸中断

需要注意的是,SNVS_TAMPER9 引脚被复用为 GPIO5_IO09,需要追加到 iomuxc_snvs 节点。

2.1.1 IIC引脚

触摸芯片用到的是IIC1,这两个引脚在设备树中以及默认添加了,无需修改:

2.1.2 复位引脚

&iomuxc节点中添加:

1
2
3
4
5
6
7
/*my gt911*/
pinctrl_tsc_reset: tscresetgrp {
fsl,pins = <
/* used for tsc reset */
MX6UL_PAD_LCD_RESET__GPIO3_IO04 0x10b0
>;
};

2.1.3 中断引脚

&iomuxc_snvs节点中添加:

1
2
3
4
5
6
7
/*my gt911*/
pinctrl_tsc_irq: tsc_irq {
fsl,pins = <
/* used for tsc irq */
MX6ULL_PAD_SNVS_TAMPER9__GPIO5_IO09 0x4001b8b0
>;
}

2.1.4 IIC设备添加GT911

GT911触摸驱动作为一个IIC设备挂载在IIC1总线上,找到IIC1节点:

需要在 IIC1 设备节点下追加相应的子节点:

1
2
3
4
5
6
7
8
9
10
gt911_tsc@5d {
compatible = "goodix,gt911";
reg = <0x5d>;
pinctrl-0 = <&pinctrl_tsc_reset>;
pinctrl-1 = <&pinctrl_tsc_irq>;
reset-gpios = <&gpio3 4 GPIO_ACTIVE_LOW>;
irq-gpios = <&gpio5 9 GPIO_ACTIVE_HIGH>;
interrupt-parent = <&gpio5>;
interrupts = <9 IRQ_TYPE_EDGE_FALLING>;
};

修改后:

reg = <0x5d>是GT911触摸芯片在 IIC1总线上的地址。

2.2 触摸芯片数据寄存器

查看GT911的数据手册,找到寄存器相关的表格:

主要关注以下这些寄存器,它们是用来读取触摸坐标点的:

Addr Access bit7~bit0
0x814E R/W buffer status(7) large detect(6) Reserved(54) number of touch points(30)
0x814F R track id
0x8150 R point 1 x coordinate (low byte)
0x8151 R point 1 x coordinate (high byte)
0x8152 R point 1 y coordinate (low byte)
0x8153 R point 1 y coorte (high byte)
0x8154 R Point 1 size (low byte)
0x8155 R Point 1 size (high byte)
0x8156 R Reserved
0x8157 R track id
0x815F R track id
0x8167 R track id
0x816F R track id
0x8170 R point 5 x coordinate (low byte)
0x8171 R point 5 x coordinate (high byte)
0x8172 R point 5 y coordinate (low byte)
0x8173 R point 5 y coordinate (high byte)
0x8174 R Point 5 size (low byte)
0x8175 R Point 5 size (high byte)
0x8176 R Reserved
  • 0x814E:这个寄存器很重要,它是可读可写的寄存器,通过读取该寄存器,可以知道当前是否有触摸点(由最高位表示),以及有几个触摸点(由低3位表示)
  • 0x814F~0x8156:是第一组触摸的坐标数据
    • 0x814F:是触摸点的追踪id,GT911支持5点触摸,这里id的取值为0~4
    • 0x8150:触摸点1的x坐标(低字节)
    • 0x8151:触摸点1的x坐标(高字节)y
    • 0x8152:触摸点1的y坐标(低字节)
    • 0x8153:触摸点1的y坐标(高字节)
    • 0x8154~0x8156:暂不使用
  • 0x8157以后的寄存器:与第一组触摸的坐标数据的含义类似,一个有五组

注:GT911支持硬件追踪触摸点,因此为每个触摸点提供了一个track id,举个简单的例子,当5个手指依次触摸到屏幕时,5组坐标寄存器中的track id会依次是0、1、2、3、4,当松开第1个手指时,即track id为0的点没有了,此时5组坐标寄存器,是只有前45组坐标寄存器有数据,track id会依次是1、2、3、4(理解这个很重要,因为我一开始想当然的误认为,移开第1个手指时,是第1组坐标寄存器没数据了)

2.3 编写驱动程序

新建gt911.c文件作为驱动文件

触摸芯片GT911的使用,本质是使用IIC通信,进行数据的读写,因为触摸屏的驱动,实际就是IIC驱动。另外,触摸的数据是通过中断的方式触发的,因此触摸驱动的编写,涉及到中断的处理。在中断时,读取到触摸数据后,要传递到应用层,这里是使用Linux的input子系统(这也是Linux的一种软件分层设计的方式)。

所以,编写触摸驱动,主要涉及3点:

  • IIC协议的驱动
  • 中断的处理(获取触摸数据)
  • input子系统(将触摸数据传递到应用层)

2.3.1 IIC驱动架构

GT911的驱动按照IIC驱动来写,当驱动运行时,会自动运行gt911_probe,在这个函数中会进行各种初始化操作。另外注意匹配列表,这里的“goodix,gt911”对应设备树中添加的设备节点,两处的名字要一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 匹配列表 */
static const struct of_device_id gt911_of_match[] = {
{.compatible = "goodix,gt911"},
{/* Sentinel */}
};

/* i2c驱动结构体 */
struct i2c_driver gt911_i2c_driver = {
.driver = {
.owner = THIS_MODULE,
.name = "gt911", /* 驱动名字 用于和设备匹配 适用于没有设备树的情况*/
.of_match_table =gt911_of_match, /* 设备树匹配列表 */
},
.probe =gt911_probe,
.remove =gt911_remove,
.id_table = gt911_id, /* id配置列表 */
};

2.3.2 驱动的初始化流程

gt911_probe函数进行触摸驱动的初始化,基本流程就是从设备树获取触摸节点,然后进行IO的初始化,中断和复位的初始化以及触摸器件的初始化等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
static int gt911_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
u8 ret = 0;
gt911.client = client;
printk("[BSP] gt911 driver and device has match!\r\n");

/* 获取设备树中的中断和复位引脚 */
printk("[BSP] get gpios\r\n");
gt911.irq_pin = of_get_named_gpio(client->dev.of_node, "irq-gpios", 0);
gt911.reset_pin = of_get_named_gpio(client->dev.of_node, "reset-gpios", 0);

/* 初始化复位引脚 */
ret = gt911_ts_reset(client, &gt911);

/* 初始化gt911 */
printk("[BSP] init gt911\r\n");
gt911_write_reg(&gt911, GT_CTRL_REG, 2); /* 软复位 */
mdelay(100);
gt911_write_reg(&gt911, GT_CTRL_REG, 0); /* 停止软复位 */
mdelay(100);

/* input 注册设备*/
printk("[BSP] init input device\r\n");
gt911.input = devm_input_allocate_device(&client->dev);

/* 初始化input */
gt911.input->name = client->name;
gt911.input->id.bustype = BUS_I2C;
gt911.input->dev.parent = &client->dev;

/* 设置input设备需要上报哪些事件*/
__set_bit(EV_SYN, gt911.input->evbit);
__set_bit(EV_KEY, gt911.input->evbit); /* 按键事件 */
__set_bit(EV_ABS, gt911.input->evbit); /* 重复事件 */
/* 设置input设备需要上报哪些按键*/
__set_bit(BTN_TOUCH, gt911.input->keybit); /* 触摸值 */

/* 多点触摸 */
input_mt_init_slots(gt911.input, MAX_SUPPORT_POINTS, 0); /*触摸点的数量 */
input_set_abs_params(gt911.input, ABS_MT_POSITION_X,0, 800, 0, 0);
input_set_abs_params(gt911.input, ABS_MT_POSITION_Y,0, 480, 0, 0);

/* 注册input */
ret = input_register_device(gt911.input);

/* 最后初始化中断 */
ret = gt911_ts_irq(client, &gt911);

printk("[BSP] %s done \r\n",__FUNCTION__);
return 0;
}

2.3.3 复位与中断的初始化

复位引脚的初始化主要就是拉低再拉高复位引脚,实现复位,主要内容为:

1
2
3
4
5
6
7
8
9
10
/* 申请复位IO 并且默认输出高电平 */
devm_gpio_request_one(&client->dev,
dev->reset_pin,
GPIOF_OUT_INIT_HIGH,
"gt911 reset");

gpio_set_value(dev->reset_pin, 0); /* 复位 */
msleep(10);
gpio_set_value(dev->reset_pin, 1); /* 停止复位 */
msleep(300);

中断的初始化,包括IO的申请和中断函数的注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 申请复位 IO */
devm_gpio_request_one(&client->dev,
dev->irq_pin,
GPIOF_IN,
"gt911 irq");

/* 申请中断 */
devm_request_threaded_irq(&client->dev,
client->irq,
NULL,
gt911_irq_handler, /* 中断处理函数 */
IRQF_TRIGGER_FALLING | IRQF_ONESHOT, /* 触发方式 */
client->name,
&gt911);

2.3.4 中断处理函数

这里仅贴出gt911_irq_handler的主要内容,基本思路是先读取0x814E(GT_GSTID_REG)这一个寄存器,判断触摸点的数量,然后再读取对应的坐标点数据寄存器,依次上报数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/* -----读取触摸信息寄存器----- */
ret = gt911_read_regs(dev, GT_GSTID_REG, &data, 1);
if(data == 0x00) /* 没有触摸数据*/
{
goto fail;
}
else
{ /* 统计触摸信息 */
status = data >> 7; // bit7:1表示坐标(或按键)已经准备好,主控可以读取 0 表示未就绪,数据无效
large_detect = (data >> 6) & 0x01; // bit6:
touch_num = data & 0x0f; // bit3~0:屏上的坐标点个数
}

if(touch_num) /* 有触摸按下 */
{
/* -----读取具体的触摸点数据寄存器----- */
gt911_read_regs(dev, GT_TP1_REG, buf, BUFFER_SIZE);
id = buf[0]; // 数据中的第一个触摸点的id
touch_index |= (0x01<<id);

/* 上报每一个触摸点坐标 */
for (i = 0; i < 5; i++)
{
if ((touch_index & (0x01<<i)))
{
input_x = (buf[pos + 1] | (buf[pos + 2] << 8)) & 0x0fff; // x坐标
input_y = (buf[pos + 3] | (buf[pos + 4] << 8)) & 0x0fff; // y坐标

input_mt_slot (dev->input, id); // 产生ABS_MT_SLOT 事件 报告是哪个触摸点的坐标
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, true); // 指定手指触摸 连续触摸
input_report_abs(dev->input, ABS_MT_POSITION_X, input_x); // 上报触摸点坐标信息
input_report_abs(dev->input, ABS_MT_POSITION_Y, input_y); // 上报触摸点坐标信息
printk("[%d](%d, %d) ", id, input_x, input_y);

report_num++;
if (report_num < touch_num)
{
pos += 8;
id = buf[pos];
touch_index |= (0x01<<id);
}
}
else
{
input_mt_slot(dev->input, i);
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, false); // 关闭手指触摸
}
}
printk("\r\n");
}
else if(last_index)/* 触摸释放 */
{
for (i = 0; i < 5; i++)
{
if ((last_index & (0x01<<i)))
{
input_mt_slot(dev->input, i); /* 上报触摸点 */
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, false); // 关闭手指触摸
}
}
}
last_index = touch_index;

input_mt_report_pointer_emulation(dev->input, true);
input_sync(dev->input); /* 同步数据 数据上报完成 */

data = 0x00; /* 向0x814E寄存器写0 不然就会一直进入中断 */
gt911_write_regs(dev, GT_GSTID_REG, &data, 1); //写入

总结一下GT911多点触摸驱动的执行流程:

3 使用Linux内核自带的驱动(未测试)

对于触摸屏的驱动,NXP已经编写好了触摸驱动,加以修改可以在自己的板子上使用。

不过我没有测试成功,以后有时间再搞,所以这一部分内容可以跳过

我这个7寸屏的驱动型号为GT911,属于 GOODIX 公司生产的触摸芯片,该触摸驱动已默认添加到了Linux内核中,位于:/drivers/input/touchscreen/goodix.c。

使用Linux内核自代的驱动,还需要进行内核配置。在Linux内核源码目录,输入以下指令打开内核的图形化配置:

1
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

到达Linux内核配置界面,然后按下路径找到对应的配置项:

1
2
3
4
-> Device Drivers 
-> Input device support
-> Touchscreens (INPUT_TOUCHSCREEN [=y])
<*> Goodix I2C touchscreen

最终到达这个界面:

按下y勾选上星号,连按多次ESC退出,最后提示保存,按下y保存配置。

然后需要重新编译zImage和设备树,到Linux内核源码目录,执行之前的编写的编译脚本

1
./build_myboard.sh 

编译的时候会弹出Linux图形配置界面, 不需要做任何的配置, 直接按两下ESC键退出图形界面

将编译出zImage(arch/arm/boot目录)和imx6ull-myboard.dtb (arch/arm/boot/dts目录)复制到网络启动位置

1
2
cp arch/arm/boot/zImage ~/myTest/tftpboot/nxp/
cp arch/arm/boot/dts/imx6ull-myboard.dtb ~/myTest/tftpboot/nxp/

4 触摸测试

使用自己编写的触摸驱动程序,进行测试。

4.1 编译设备树

首先是编译设备树,验证添加的触摸节点是否工作正常,在Linux内核源码目录执行下面的命令,重新编译设备树并拷贝到网络启动位置。

1
2
make imx6ull-myboard.dtb
cp arch/arm/boot/dts/imx6ull-myboard.dtb ~/myTest/tftpboot/nxp/

然后重启开发板,可以先到如下位置,查看设备树的节点是否正常:

4.2 编译驱动文件

然后是编译驱动文件,也就是gt911.c,编译方式和之前一样,在ubuntu中使用Makefile进行交叉编译。

本篇暂未用到对应的触摸应用程序,所有的触摸坐标打印都是在驱动程序中通过printk的方式进行内核打印。

编译完驱动后,将对应的.ko文件复制到板子中。

4.3 测试触摸点的坐标输出

先加载触摸驱动,串口会打印出为触摸分配的event,我这里是event2

然后执行下面的指令进行触摸测试:

1
hexdump /dev/input/event2 

将手指放到屏幕上,就可以在LCD屏幕上看到坐标值的打印,比如将手指放到屏幕左下角,对应输出的值大致就是屏幕的最大位置(800,480):

GT911支持多点触摸,驱动程序中也对多点数据进行了获取和打印,将多个手指放到屏幕上,可以看到最多有5个触摸点的坐标打印:

5 总结

本篇主要介绍了多点触摸芯片GT911的驱动编写与使用,并通过将触摸点实时打印的方式,测试触摸功能。


视频演示

https://www.bilibili.com/video/BV1sZ4y1Q7da?spm_id_from=333.999.0.0