正点原子嵌入式linux驱动开发——Linux 多点电容触摸屏

随着智能手机的发展,电容触摸屏也得到了飞速的发展。相比电阻触摸屏,电容触摸屏有很多的优势,比如支持多点触控、不需要按压,只需要轻轻触摸就有反应。ALIENTEK的三款RGB LCD屏幕都支持多点电容触摸,本章就以ATK7016这款RGB LCD屏幕为例讲解一下如何驱动电容触摸屏,并获取对应的触摸坐标值

多点电容触摸简介

触摸屏很早就有了,一开始是电阻触摸屏,电阻触摸屏只能单点触摸。 和电阻触摸屏相比,电容触摸屏最大的优点是支持多点触摸(后面的电阻屏也支持多点触摸),电容屏只需要手指轻触即可,而电阻屏是需要手指给予一定的压力才有反应,而且电容屏不需要校准。本章就来学习一下如何使用多点触摸屏,如何获取到多点触摸值。只需要关注如何使用电容屏,如何得到其多点触摸坐标值即可(这边可以去看我的知乎里面的笔记,之前学精英板HAL库的时候,课程里面是有讲到原理的,而且原理也不难挺好理解的)。ALIENTEK的三款RGB LCD屏幕都是支持5点电容触摸屏的,本章以ATK-7016这款屏幕为例来讲解如何使用多点电容触摸屏。

ATK-7016这款屏幕其实是由TFT LCD+触摸屏组合起来的。底下是LCD面板,上面是触摸面板,将两个封装到一起就成了带有触摸屏的LCD屏幕。电容触摸屏也是需要一个驱动IC的,驱动IC一般会提供一个I2C接口给主控制器,主控制器可以通过I2C接口来读取驱动IC里面的触摸坐标数据ATK-7016、ATK-7084这两款屏幕使用的触摸控制 IC是FT5426,ATK-4342使用的驱动IC是GT9147。这三个电容屏触摸IC都是I2C接口的, 使用方法基本一样。

FT5426这款驱动IC采用15*28的驱动结构,也就是15个感应通道,28个驱动通道,最多支持5点电容触摸。ATK-7016的电容触摸屏部分有4个IO用于连接主控制器:SCL、SDA、RST和INT,SCL和SDA是I2C引脚,RST是复位引脚,INT是中断引脚一般通过INT引脚来通知主控制器有触摸点按下,然后在INT中断服务函数中读取触摸数据。也可以不使用中断功能,采用轮询的方式不断查询是否有触摸点按下,本章实验使用中断方式来获取触摸数据。

跟所有的I2C器件一样,FT5426也是通过读写寄存器来完成初始化和触摸坐标数据读取的,STM32MP1的I2C之前已经有过学习,所以本章的主要工作就是读写FT5426的寄存器。FT5426的I2C设备地址为0X38,FT5426的寄存器有很多,本章只用到了其中的一部分,如下图所示:

FT5426使用的寄存器表

上图中就是本次触摸屏实验会使用到的寄存器。

Linux下电容触摸屏驱动框架简介

多点触摸(MT)协议详解

电容触摸驱动的基本原理就不详细讲了,回顾一下几个重要的知识点:

  1. 电容触摸屏是IIC接口的,需要触摸IC,以正点原子的ATK7016为例,其所使用的触摸屏控制IC为FT5426,因此所谓的电容触摸驱动就是IIC设备驱动
  2. 触摸IC提供了中断信号引脚(INT),可以通过中断来获取触摸信息
  3. 电容触摸屏得到的是触摸位置绝对信息以及触摸屏是否有按下
  4. 电容触摸屏不需要校准,当然了,这只是理论上的,如果电容触摸屏质量比较差,或者触摸玻璃和TFT之间没有完全对齐,那么也是需要校准的。

根据以上几个知识点,可以得出电容触摸屏驱动其实就是以下几种linux驱动框架的组合:

  1. IIC设备驱动,因为电容触摸IC基本都是IIC接口的,因此大框架就是IIC设备驱动。
  2. 通过中断引脚(INT)向 linux内核上报触摸信息,因此需要用到linux中断驱动框架。坐标的上报在中断服务函数中完成。
  3. 触摸屏的坐标信息、屏幕按下和抬起信息都属于linux的input子系统,因此向linux内核上报触摸屏坐标信息就得使用input子系统。只是,得按照linux内核规定的规则来上报坐标信息。

经过简单的分析,IIC驱动、中断驱动、input子系统都已经在前面学过了,唯独没学过的就是input子系统下的多点电容触摸协议,这个才是本章学习的重点。linux内核中有一份文档详细的讲解了多点电容触摸屏协议,文档路径为:Documentation/input/multi-touch-protocol.txt

老版本的linux内核是不支持多点电容触摸的(Multi-touch,简称MT)。MT协议是后面加入的,因此如果使用2.x版本linux内核的话可能找不到MT协议。 MT协议被分为两种类型,Type A和TypeB,这两种类型的区别如下:

  • Type A:适用于触摸点不能被区分或者追踪,此类型的设备上报原始数据(此类型在实际使用中非常少! !)。
  • Type B:适用于有硬件追踪并能区分触摸点的触摸设备,此类型设备通过slot更新某一个触摸点的信息,FT5426就属于此类型,一般的多点电容触摸屏IC都有此能力。

触摸点的信息通过一系列的ABS_MT事件(有的资料也叫消息)上报给linux内核,只有ABS_MT事件是用于多点触摸的,ABS_MT事件定义在文件include/uapi/linux/input-event-codes.h 中,相关事件如下所示:

ABS_MT事件

在上面这些众多的ABS_MT事件中,最常用的就是ABS_MT_SLOT、ABS_MT_POSITION_X、ABS_MT_POSITION_Y和ABS_MT_TRACKING_ID。其中ABS_MT_POSITION_X和ABS_MT_POSITION_Y用来上报触摸点的(X,Y)坐标信息,ABS_MT_SLOT用来上报触摸点ID,对于Type B类型的设备,需要用到ABS_MT_TRACKING_ID事件来区分触摸点

对于Type A类型的设备,通过input_mt_sync()函数来隔离不同的触摸点数据信息,此函数原型如下所示:

void input_mt_sync(struct input_dev *dev)

此函数只要一个参数,类型为input_dev,用于指定具体的input_dev设备。input_mt_sync()函数会触发SYN_MT_REPORT事件,此事件会通知接收者获取当前触摸数据,并且准备接收下一个触摸点数据。

对于Type B类型的设备,上报触摸点信息的时候需要通过input_mt_slot()函数区分是哪一个触摸点,input_mt_slot()函数原型如下所示:

void input_mt_slot(struct input_dev *dev, int slot)

此函数有两个参数,第一个参数是input_dev设备,第二个参数slot用于指定当前上报的是哪个触摸点信息。input_mt_slot()函数会触发ABS_MT_SLOT事件,此事件会告诉接收者当前正在更新的是哪个触摸点(slot)的数据

不管是哪个类型的设备,最终都要调用input_sync()函数来标识多点触摸信息传输完成,告诉接收者处理之前累计的所有消息,并且准备好下一次接收。Type B和Type A相比最大的区
别就是Type B可以区分出触摸点,因此可以减少发送到用户空间的数据。Type B使用slot协
议区分具体的触摸点,slot需要用到ABS_MT_TRACKING_ID消息,这个ID需要硬件提供,或者通过原始数据计算出来。对于Type A设备,内核驱动需要一次性将触摸屏上所有的触摸点
信息全部上报,每个触摸点的信息在本次上报事件流中的顺序不重要,因为事件的过滤和手指(触摸点)跟踪是在内核空间处理的。

Type B设备驱动需要给每个识别出来的触摸点分配一个slot,后面使用这个slot来上报触摸点信息。可以通过slot的ABS_MT_TRACKING_ID来新增、替换或删除触摸点。一个非负数的ID表示一个有效的触摸点,-1这个ID表示未使用slot。一个以前不存在的ID表示这是一个新加的触摸点,一个ID如果再也不存在了就表示删除了。

有些设备识别或追踪的触摸点信息要比他上报的多,这些设备驱动应该给硬件上报的每个触摸点分配一个Type B的slot。一旦检测到某一个slot关联的触摸点ID发生了变化,驱动就应该改变这个slot的ABS_MT_TRACKING_ID,使这个slot失效。如果硬件设备追踪到了比他正在上报的还要多的触摸点,那么驱动程序应该发送BTN_TOOL_*TAP消息,并且调用input_mt_report_pointer_emulation()函数,将此函数的第二个参数use_count设置为false

Type A触摸点信息上报时序

对于Type A类型的设备,发送触摸点信息的时序如下所示,这里以2个触摸点为例

Type A触摸点数据上报时序

第1行,通过ABS_MT_POSITION_X事件上报第一个触摸点的X坐标数据,通过input_report_abs函数实现,下面同理。

第2行,通过ABS_MT_POSITION_Y事件上报第一个触摸点的Y坐标数据。

第3行,上报SYN_MT_REPORT事件,通过调用input_mt_sync函数来实现。

第4行,通过ABS_MT_POSITION_X事件上报第二个触摸点的X坐标数据。

第5行,通过ABS_MT_POSITION_Y事件上报第二个触摸点的Y坐标数据。

第6行,上报SYN_MT_REPORT事件,通过调用input_mt_sync函数来实现。

第7行,上报SYN_REPORT事件,通过调用input_sync函数实现。

在编写Type A类型的多点触摸驱动的时候就需要按照示例代码47.2.2.1中的时序上报坐标信息。Linux内核里面也有Type A类型的多点触摸驱动,找到st2332.c这个驱动文件,路径为drivers/input/touchscreen/st1232.c,找到st1232_ts_irq_handler函数,此函数里面就是上报触摸点坐标信息的。

st1232_ts_irq_handler函数代码段

第111行,获取所有触摸点信息。

第116~125行,按照Type A类型轮流上报所有的触摸点坐标信息,第121和122行分别上报触摸点的(X,Y)轴坐标,也就是ABS_MT_POSITION_X和ABS_MT_POSITION_Y事件。每上报完一个触摸点坐标,都要在第123行调用input_mt_sync函数上报一个SYN_MT_REPORT信息

第142行,每上报完一轮触摸点信息就调用一次 input_sync函数,也就是发送一个SYN_REPORT事件

Type B触摸点信息上报时序

对于Type B类型的设备,发送触摸点信息的时序如下所示,这里以2个触摸点为例

Type B触摸点数据上报时序

第1行,上报ABS_MT_SLOT事件,也就是触摸点对应的SLOT。每次上报一个触摸点坐标之前要先使用input_mt_slot函数上报当前触摸点SLOT,触摸点的SLOT其实就是触摸点ID,需要由触摸IC提供

第2行,根据Type B的要求,每个SLOT必须关联一个ABS_MT_TRACKING_ID,通过修改SLOT关联的ABS_MT_TRACKING_ID来完成对触摸点的添加、替换或删除。具体用到的函数就是input_mt_report_slot_state,如果是添加一个新的触摸点,那么此函数的第三个参数active要设置为truelinux内核会自动分配一个ABS_MT_TRACKING_ID值,不需要用户去指定具体的ABS_MT_TRACKING_ID值。

第3行,上报触摸点0的X轴坐标,使用函数input_report_abs来完成。

第4行,上报触摸点0的Y轴坐标,使用函数input_report_abs来完成。

第5-8行,和第1-4行类似,只是换成了上报触摸点 1的(X,Y)坐标信息。

第9行,当所有的触摸点坐标都上传完毕以后就得发送SYN_REPORT事件,使用input_sync函数来完成

当一个触摸点移除以后,同样需要通过SLOT关联的ABS_MT_TRACKING_ID来处理,时序如下所示:

Type B触摸点移除时序

第1行,当一个触摸点(SLOT)移除以后,需要通过ABS_MT_TRACKING_ID事件发送一个-1给内核。方法很简单,同样使用input_mt_report_slot_state函数来完成,只需要将此函数的第三个参数active设置为false即可,不需要用户手动去设置-1。

第2行,当所有的触摸点坐标都上传完毕以后就得发送SYN_REPORT事件。当要编写Type B类型的多点触摸驱动的时候就需要按照示例代码47.2.3.1中的时序上报坐标信息。

Linux内核里面有大量的Type B类型的多点触摸驱动程序,可以参考这些现成的驱动程序来编写自己的驱动代码。这里就以ili210x这个触摸驱动IC为例,看看是Type B类型是如何上报触摸点坐标信息的。找到ili210x.c这个驱动文件,路径为drivers/input/touchscreen/ili210x.c,找到ili210x_report_events函数,此函数就是用于上报ili210x触摸坐标信息的,函数内容如下所示:

ili210x_report_events函数代码段

第194-330行,使用for循环实现上报所有的触摸点坐标,第202-266行从触摸芯片中读取触摸坐标数据。第282行调用input_mt_slot函数上报ABS_MT_SLOT事件。第290行调用input_mt_report_slot_state函数上报ABS_MT_TRACKING_ID事件,也就是给SLOT关联一个ABS_MT_TRACKING_ID。第314行使用touchscreen_report_pos函数上报触摸点对应的(X,Y)坐标值,touchscreen_report_pos 函数定义在of_touchscreen.c文件中,此函数最终通过input_report_abs来上报坐标值。

第354行,使用input_sync函数上报SYN_REPORT事件。

MT其他事件使用

在示例代码47.2.1.1中给出了Linux所支持的所有ABS_MT事件,可以根据实际需求将这些事件组成各种事件组合。最简单的组合就是ABS_MT_POSITION_X和ABS_MT_POSITION_Y,可以通过在这两个事件上报触摸点,如果设备支持的话,还可以使用ABS_MT_TOUCH_MAJOR和ABS_MT_WIDTH_MAJOR这两个消息上报触摸面积信息,关于其他ABS_MT事件的具体含义可以查看Linux内核中的multi-touch-protocol.txt文档,这里重点补充一下ABS_MT_TOOL_TYPE事件。

ABS_MT_TOOL_TYPE事件用于上报触摸工具类型,很多内核驱动都不能区分出触摸设备类型是手指还是触摸笔?这种情况下,这个事件可以忽略掉。目前的协议支持MT_TOOL_FINGER(手指)、MT_TOOL_PEN(笔)和 MT_TOOL_PALM(手掌)这三种触摸设备类型,对于Type B类型,此事件由input子系统内核处理。如果驱动程序需要上报ABS_MT_TOOL_TYPE事件,那么可以使用input_mt_report_slot_state函数来完成此工作

关于Linux系统下的多点触摸(MT)协议就讲解到这里,简单总结一下,MT协议隶属于linux的input子系统,驱动通过大量的ABS_MT事件向linux内核上报多点触摸坐标数据。根据触摸IC的不同,分为Type A和Type B两种类型,不同的类型其上报时序不同,目前使用最多的是Type B类型。接下来就根据前面学习过的MT协议来编写一个多点电容触摸驱动程序,本章节所使用的触摸屏是正点原子的ATK7084(7寸800*480)和ATK7016(7寸1024*600)这两款触摸屏,这两款触摸屏都使用FT5426这款触摸IC,因此驱动程序是完全通用的。

多点触摸所使用的API函数

根据前面的讲解,学习到linux下的多点触摸协议其实就是通过不同的事件来上报触摸点坐标信息,这些事件都是通过Linux内核提供的对应API函数实现的,本小节来看一下一些常见的API函数。

input_mt_init_slots函数

input_mt_init_slots函数用于初始化MT的输入 slots,编写MT驱动的时候必须先调用此函
数初始化slots,此函数定义在文件drivers/input/input-mt.c中,函数原型如下所示:

int input_mt_init_slots(struct input_dev *dev, 
						unsigned int num_slots, 
						unsigned int flags)

函数参数和返回值含义如下:

  • dev:MT设备对应的input_dev,因为MT设备隶属于input_dev。
  • num_slots:设备要使用的SLOT数量,也就是触摸点的数量。
  • flags:其他一些flags信息,可设置的flags如下所示:

flags选项

可以采用‘|’运算来同时设置多个flags标识。

  • 返回值:0,成功;负值,失败。

input_my_slot函数

此函数用于Type B类型,产生ABS_MT_SLOT事件,告诉内核当前上报的是哪个触摸点
的坐标数据,此函数定义在文件include/linux/input/mt.h中,函数原型如下所示:

void input_mt_slot(struct input_dev *dev, 
				   int slot)

函数参数和返回值含义如下:

  • dev:MT设备对应的input_dev。
  • slot:当前发送的是哪个slot的坐标信息,也就是哪个触摸点。
  • 返回值:无。

input_mt_report_slot_state函数

此函数用于Type B类型,用于产生ABS_MT_TRACKING_ID和ABS_MT_TOOL_TYPE事件,ABS_MT_TRACKING_ID事件给slot关联一个ABS_MT_TRACKING_ID,ABS_MT_TOOL_TYPE事件指定触摸类型(是笔还是手指等)。此函数定义在文件drivers/input/input-mt.c中,此函数原型如下所示:

bool input_mt_report_slot_state(struct input_dev *dev, 
								unsigned int tool_type, 
								bool active)

函数参数和返回值含义如下:

  • dev:MT设备对应的input_dev。
  • tool_type:触摸类型,可以选择MT_TOOL_FINGER(手指)、 MT_TOOL_PEN(笔)或MT_TOOL_PALM(手掌),对于多点电容触摸屏来说一般都是手指。
  • active:true,连续触摸input子系统内核会自动分配一个ABS_MT_TRACKING_ID给slot;false,触摸点抬起,表示某个触摸点无效了input子系统内核会分配一个-1给slot,表示触摸点溢出。
  • 返回值:触摸有效的话返回true,否则返回false。

input_report_abs函数

Type A和Type B类型都使用此函数上报触摸点坐标信息,通过ABS_MT_POSITION_X和ABS_MT_POSITION_Y事件实现X和Y轴坐标信息上报。此函数定义在文件include/linux/input.h中,函数原型如下所示:

void input_report_abs(struct input_dev *dev,
					  unsigned int code, 
					  int value)

函数参数和返回值含义如下:

  • dev:MT设备对应的input_dev。
  • code:要上报的是什么数据,可以设置为ABS_MT_POSITION_X或ABS_MT_POSITION_Y,也就是X轴或者Y轴坐标数据。
  • value:具体的X轴或Y轴坐标数据值。
  • 返回值:无。

input_mt_report_pointer_emulation函数

如果追踪到的触摸点数量多于当前上报的数量,驱动程序使用BTN_TOOL_TAP事件来通知用户空间当前追踪到的触摸点总数量,然后调用input_mt_report_pointer_emulation函数将use_count参数设置为false。否则的话将use_count参数设置为true,表示当前的触摸点数量(此函数会获取到具体的触摸点数量,不需要用户给出),此函数定义在文件drivers/input/input-mt.c中,函数原型如下:

void input_mt_report_pointer_emulation(struct input_dev *dev, 
									   bool use_count)

函数参数和返回值含义如下:

  1. dev:MT设备对应的input_dev。
  2. use_count:true,有效的触摸点数量false,追踪到的触摸点数量多于当前上报的数量。
  3. 返回值:无。

多点电容触摸驱动框架

前面几小节已经详细的讲解了linux下多点触摸屏驱动原理,本小节来梳理一下linux下多点电容触摸驱动的编写框架和步骤。首先确定驱动需要用到哪些知识点,哪些框架?根据前面的分析,在编写驱动的时候需要注意一下几点:

  1. 多点电容触摸芯片的接口,一般都为I2C接口,因此驱动主框架肯定是I2C。
  2. linux里面一般都是通过中断来上报触摸点坐标信息,因此需要用到中断框架。
  3. 多点电容触摸属于input子系统,因此还要用到input子系统框架。
  4. 在中断处理程序中按照linux的MT协议上报坐标信息。

根据上面的分析,多点电容触摸驱动编写框架以及步骤如下:

I2C驱动框架

驱动总体采用I2C框架,参考框架代码如下所示:

示例代码 47.2.6.1 多点电容触摸驱动 I2C 驱动框架
1  /* 设备树匹配表 */
2  static const struct i2c_device_id xxx_ts_id[] = {
3      { "xxx", 0, },
4      { /* sentinel */ }
5  };
6
7  /* 设备树匹配表 */
8  static const struct of_device_id xxx_of_match[] = {
9      { .compatible = "xxx", },
10     { /* sentinel */ }
11 };
12
13 /* i2c 驱动结构体 */
14 static struct i2c_driver ft5x06_ts_driver = {
15     .driver = {
16         .owner = THIS_MODULE,
17         .name = "edt_ft5x06",
18         .of_match_table = of_match_ptr(xxx_of_match),
19     },
20     .id_table = xxx_ts_id,
21     .probe = xxx_ts_probe,
22     .remove = xxx_ts_remove,
23 };
24
25 /*
26  * @description : 驱动入口函数
27  * @param : 无
28  * @return : 无
29  */
30 static int __init xxx_init(void)
31 {
32     int ret = 0;
33
34     ret = i2c_add_driver(&xxx_ts_driver);
35
36     return ret;
37 }
38
39 /*
40  * @description : 驱动出口函数
41  * @param : 无
42  * @return : 无
43  */
44 static void __exit xxx_exit(void)
45 {
46     i2c_del_driver(&ft5x06_ts_driver);
47 }
48
49 module_init(xxx_init);
50 module_exit(xxx_exit);
51 MODULE_LICENSE("GPL");
52 MODULE_AUTHOR("zuozhongkai");

I2C驱动框架已经在之前的I2C驱动中已经进行了详细的讲解,这里就不再赘述了。当设备树中触摸IC的设备节点和驱动匹配以后,示例代码47.2.6.1中第21行的xxx_ts_probe函数就会执行,可以在此函数中初始化触摸IC,中断和input子系统等

初始化触摸IC、中断和input子系统

初始化操作都是在xxx_ts_probe函数中完成,参考框架如下所示(以下代码中步骤顺序可以
自行调整,不一定按照示例框架来):

示例代码 47.2.6.2 xxx_ts_probe 驱动框架
1  static int xxx_ts_probe(struct i2c_client *client, const struct i2c_device_id *id)
2  {
3      struct input_dev *input;
4
5      /* 1 、初始化 I2C */
6      ......
7
8      /* 2 、申请中断 */
9      devm_request_threaded_irq(&client->dev, client->irq, NULL,
10         xxx_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
11         client->name, &xxx);
12     ......
13
14     /* 3 、input 设备申请与初始化 */
15     input = devm_input_allocate_device(&client->dev);
16
17     input->name = client->name;
18     input->id.bustype = BUS_I2C;
19     input->dev.parent = &client->dev;
20     ......
21
22     /* 4 、初始化 input 和 MT */
23     __set_bit(EV_ABS, input->evbit);
24     __set_bit(BTN_TOUCH, input->keybit);
25
26     input_set_abs_params(input, ABS_X, 0, width, 0, 0);
27     input_set_abs_params(input, ABS_Y, 0, height, 0, 0);
28     input_set_abs_params(input, ABS_MT_POSITION_X, 0, width, 0, 0);
29     input_set_abs_params(input, ABS_MT_POSITION_Y, 0, height, 0, 0);
30     input_mt_init_slots(input, MAX_SUPPORT_POINTS, 0);
31     ......
32
33     /* 5 、注册 input_dev */
34     input_register_device(input);
35     ......
36 }

第5-7行,首先肯定是初始化触摸芯片,包括芯片的相关IO,比如复位、中断等IO引脚,然后就是芯片本身的初始化,也就是配置触摸芯片的相关寄存器

第9行,因为一般触摸芯片都是通过中断来向系统上报触摸点坐标信息的,因此需要初始化中断,这里又和之前Linux中断内容结合起来了。可能会发现第9行并没有使用request_irq函数申请中断,而是采用了devm_request_threaded_irq这个函数,为什么使用这个函数呢?是不是request_irq函数不能使用?答案肯定不是的,这里用request_irq函数是绝对没问题的。那为何要用devm_request_threaded_irq呢?这里就简单的介绍一下这个API函数,devm_request_threaded_irq函数特点如下:

  1. 用于申请中断,作用和request_irq函数类似。
  2. 此函数的作用是中断线程化,如果直接在网上搜索“devm_request_threaded_irq”会发现相关解释很少。但是去搜索request_threaded_irq函数就会有很多讲解的博客和帖子,这两个函数在名字上的差别就是前者比后者多了个“devm_”前缀 “devm_”前缀稍后讲解。应该注意到了“request_threaded_irq”相比“request_irq”多了个threaded函数,也就是线程的意思。那么为什么要中断线程化呢?都是知道硬件中断具有最高优先级,不论什么时候只要硬件中断发生,那么内核都会终止当前正在执行的操作,转而去执行中断处理程序(不考虑关闭中断和中断优先级的情况),如果中断非常频繁的话那么内核将会频繁的执行中断处理程序,导致任务得不到及时的处理中断线程化以后中断将作为内核线程运行,而且也可以被赋予不同的优先级,任务的优先级可能比中断线程的优先级高,这样做的目的就是保证高优先级的任务能被优先处理。虽然下半部可以被延迟处理,但是依旧先于线程执行,中断线程化可以让这些比较耗时的下半部与进程进行公平竞争
    要注意,并不是所有的中断都可以被线程化,重要的中断就不能这么操作。对于触摸屏而言只要手指放到屏幕上,它可能就会一直产生中断(视具体芯片而定,FT5426是这样的),中断处理程序里面需要通过I2C读取触摸信息并上报给内核,I2C的速度最大只有400KHz,算是低速外设。不断的产生中断、读取触摸信息、上报信息会导致处理器在触摸中断上花费大量的时间,但是触摸相对来说不是那么重要的事件,因此可以将触摸中断线程化。如果你觉得触摸中断很重要,那么就可以不将其进行线程化处理。总之,要不要将一个中断进行线程化处理是需要自己根据实际情况去衡量的。linux内核自带的goodix.c(汇顶科技)、mms114.c(MELFAS公司)、zforce_ts.c(zForce公司)等多点电容触摸IC驱动程序都采用了中断线程化,当然也有一些驱动没有采用中断线程化。
  3. 最后来看一下“devm_”前缀,在linux内核中有很多的申请资源类的API函数都有对应的“devm_”前缀版本。比如devm_request_irq和request_irq这两个函数,这两个函数都是申请中断的,使用request_irq函数申请中断的时候,如果驱动初始化失败的话就要调用free_irq函数对申请成功的irq进行释放,卸载驱动的时候也需要手动调用free_irq来释放irq。假如驱动里面申请了很多资源,比如:gpio、irq、input_dev,那么就需要添加很多goto语句对其做处理,当这样的标签多了以后代码看起来就不整洁了。“devm_”函数就是为了处理这种情况而诞生的,“devm_”函数最大的作用就是:使用“devm_”前缀的函数申请到的资源可以由系统自动释放,不需要手动处理。如果使用devm_request_threaded_irq函数来申请中断,那么就不需要再调用free_irq函数对其进行释放。带有“devm_”前缀的都是一些和设备资源管理有关的函数

第15行,接下来就是申请input_dev,因为多点电容触摸属于input子系统。这里同样使用devm_input_allocate_device函数来申请input_dev,也就是前面讲解的input_allocate_device函数加“devm_”前缀版本。申请到input_dev以后还需要对其进行初始化操作

第23-24行,设置input_dev需要上报的事件为EV_ABS和BTN_TOUCH,因为多点电容屏的触摸坐标为绝对值,因此需要上报EV_ABS事件。触摸屏有按下和抬起之分,因此需要上报BTN_TOUCH按键。

第26-29行,调用input_set_abs_params函数设置EV_ABS事件需要上报ABS_X、ABS_Y、ABS_MT_POSITION_X和ABS_MT_POSITION_Y。单点触摸需要上报ABS_X和ABS_Y,对于多点触摸需要上报ABS_MT_POSITION_X和ABS_MT_POSITION_Y。

第30行,调用input_mt_init_slots函数初始化多点电容触摸的slots

第34行,调用input_register_device函数系统注册前面申请到的input_dev

上报坐标信息

最后就是在中断服务程序中上报读取到的坐标信息,根据所使用的多点电容触摸设备类型选择使用Type A还是Type B时序。由于大多数的设备都是Type B类型,因此这里就以Type B类型为例讲解一下上报过程,参考驱动框架如下所示:

示例代码 47.2.6.3 xxx_handler 中断处理程序
1  static irqreturn_t xxx_handler(int irq, void *dev_id)
2  {
3  
4      int num; /* 触摸点数量 */
5      int x[n], y[n]; /* 保存坐标值 */
6
7      /* 1 、从触摸芯片获取各个触摸点坐标值 */
8      ......
9
10     /* 2 、上报每一个触摸点坐标 */
11     for (i = 0; i < num; i++) {
12         input_mt_slot(input, id);
13         input_mt_report_slot_state(input, MT_TOOL_FINGER, true);
14         input_report_abs(input, ABS_MT_POSITION_X, x[i]);
15         input_report_abs =(input, ABS_MT_POSITION_Y, y[i]);
16     }
17     ......
18
19     input_sync(input);
20     ......
21
22     return IRQ_HANDLED;
23 }

进入中断处理程序以后首先肯定是从触摸IC里面读取触摸坐标以及触摸点数量,假设触摸点数量保存到num变量,触摸点坐标存放到x,y数组里面。

第11-16行,循环上报每一个触摸点坐标,一定要按照Type B类型的时序进行。

第19行,每一轮触摸点坐标上报完毕以后就调用一次input_sync函数发送一个SYN_REPORT事件

关于多点电容触摸驱动框架就讲解到这里,接下来就实际编写一个多点电容触摸驱动程序。

硬件原理图分析

触摸屏原理图一共涉及到4个引脚,如下图所示:

触摸原理图

I2C接口引脚I2C2_SCL和I2C2_SDA,触摸IC 复位引脚CT_RST以及触摸IC中断引脚CT_INT。 I2C2_SCL和 I2C2_SDA对应的引脚为PH4和PH5。CT_INT对应的引脚为PI1,CT_RST对应的引脚为PH15

对于中断触发模式,当FT5426 检测到有触摸事件发生时,会将中断信号拉低,所以对主机控制器来说,它是一个下降沿中断触发方式;当一直按着触摸屏不松开,则会一直触发中断,现在大部分的触摸IC都是这样设计的。一般要专门使用一个定时器来检测、处理触摸屏长按的情况,但对于FT5426这类IC来说,不需要这样做

实验程序编写

添加FT5426设备节点

添加引脚节点

FT5426用到了I2C2接口以及PI1和PH15这两个IO,I2C2的引脚前面实验已经修改好了,本实验就不需要再修改。本实验重点只需要配置PI1以及PH15这两个引脚,打开stm32mp15-pinctrl.dtsi文件,添加如下引脚配置信息:

示例代码47.4.1.1 ft5426中断以及复位引脚配置 
1 ft5426int_reset_pins_a: ft5426int_reset_pins-0 { 
2     pins1 {
3         pinmux = <STM32_PINMUX('I', 1, GPIO)>, /* ft5426 INT */ 
4                 <STM32_PINMUX('H', 15, GPIO)>; /* ft5426 RESET */ 
5         bias-pull-up; 
6         slew-rate = <0>; 
7     }; 
8 };

FT5426节点配置

FT5426这个触摸IC挂载在STM32MP1的I2C2总线接口上,因此需要向I2C2节点下添加一个子节点,此子节点用于描述FT5426,添加完成以后的I2C2节点内容如下所示(省略掉其他挂载到I2C2下的设备):

示例代码 47. 4.1.2 ft 5426 节点信息
1  &i2c2 {
2      pinctrl-names = "default", "sleep";
3      pinctrl-0 = <&i2c2_pins_a>;
4      pinctrl-1 = <&i2c2_pins_sleep_a>;
5      status = "okay";
6
7      ft5426: ft5426@38 {
8          compatible = "edt,edt-ft5426";
9          pinctrl-0 = <&ft5426int_reset_pins_a>;
10         reg = <0x38>;
11         irq-gpios = <&gpioi 1 GPIO_ACTIVE_LOW>;
12         reset-gpios = <&gpioh 15 GPIO_ACTIVE_LOW>;
13         status = "okay";
14     };
15 };

第7行,触摸屏所使用的FT5426 芯片节点,挂载I2C2 节点下,FT5426 的器件地址为0X38。

第10行,reg属性描述FT5426的器件地址为0x38。

第11行,irq-gpios 属性描述中断IO对应的GPIO为PI1。

第12行,reset-gpios属性描述复位IO对应的GPIO 为PH15。

编写多点电容触摸驱动

首先要先进行FT5426芯片的相关寄存器宏定义。之后还要define一下对应的按下、抬起、接触以及保留的宏指令。

然后编写ft5426的设备结构体,里面要放进去之前说过的四个内容,也就是i2c_client,input_dev,还有两个int变量reset_gpio,以及irq_gpio。

之后编写write函数,通过定义struct i2c_msg的msg变量,来获取flags,addr,buf以及len(msg.len=len+1)来完成i2c的写操作,然后i2c_transfer把消息发出去。

read也是类似,只不过这里跟spi不一样有收发的区别,写只要msg,读就需要i2c_mas msg[2];然后msg[0]就是写操作,msg[1]就是读操作,写的len就是1,读的msg[1].len=len。然后同样是通过i2c_transfer发消息。

然后编写reset函数,这里就要通过of_get_named_gpio获取设备树中的复位GPIO引脚,然后通过devm_gpio_request_one来申请使用引脚,这里通过msleep进行延时拉高拉低引脚,先gpio_set_value_cansleep拉低,然后gpio_set_value_cansleep拉高即可标识复位。

然后编写isr函数,用于作为触摸屏中断服务函数,来完成坐标上报。首先通过刚才自己编写的edt_ft5426_ts_read函数,从0x02寄存器(自己宏定义过的),连续读取29个寄存器,然后在for循环里面来读取每一个触点,把连续读取的rdbuf通过6*i这个规律读到u8的buf数组中。然后在触点类型type中保存buf[0]>>6获取Event Flag,坐标的获取则是要看屏幕和芯片自己的定义来获取,然后触摸id通过(buf[2]>>4)&0x0f来获取,来看是哪一个触摸点,之后通过input_mt_slot和input_mt_report_slot_state完成当前触摸点事件上报;之后通过input_report_abs把当前的x,y更新上报。把所有点都上报之后(跳出for循环),调用input_mt_report_pointer_emulation并把第二个参数置true,最后调用input_sync完成触点坐标的获取。

之后编写irq函数,用于初始化中断引脚。老样子通过of_get_named_gpio来获取中断引脚,然后通过devm_gpio_request_one申请使用引脚,之后通过devm_request_threaded_irq注册中断服务函数,就是刚写的那个isr函数。

之后编写probe函数,这里就比较常规了,显示devm_kzalloc实例化设备,然后edt_ft5426_ts_reset先复位芯片,msleep之后初始化芯片(根据芯片手册要求通过write函数对寄存器写入对应值),然后edt_ft5426_ts_irq申请注册中断服务函数,之后devm_input_allocate_device注册input设备,设定是BUS_I2C通讯,然后input_set_abs_params初始化屏幕范围,最后input_mt_init_slots初始化一下slot。最后注册一下input设备,通过input_register_device完成,然后i2c_set_clientdata把input绑定到client。

之后编写remove函数,这里就是通过之前已经保存的i2c的clientdata,由i2c_get_clientdata获取设备之后,直接input_unregister_device注销设备就可以了。

然后是of_device_id的数组,设置一下跟设备树吻合的.compatible就好。

之后是写一个i2c_driver的函数,里面要完成.driver,保存.owner,.name以及.of_match_table,之后绑定.probe和.remove就好了。

最后就是module_i2c_driver把刚才的i2c_driver加入,然后添加MODULE_LICENSE,MODULE_AUTHOR以及MODULE_INFO就可以了。

运行测试

编译驱动程序

把Makefile里面的obj-m改成ft5x06.o然后“make”就可以了。

运行测试

编译设备树,然后使用新的设备树启动linux内核。

多点电容触摸屏测试不需要编写专门的APP,将上一小节编译出来ft5x06.ko拷贝到rootfs/lib/modules/5.3.41目录中,启动开发板,进入到目录lib/modules/5.3.41中,输入如下命令加载ft5x06.ko这个驱动模块:

depmod //第一次加载驱动的时候需要运行此命令
modprobe ft5x06.ko //加载驱动模块

驱动加载完成后会有如下图所示的信息输入:

电容屏对应event设备

不同的平台event序号不同,也可能是event3event4等,一切以实际情况为准!输入如下命令查看event1,也就是多点电容触摸屏上报的原始数据

hexdump /dev/input/event1

现在用一根手指触摸屏幕的右上角,然后再抬起,理论坐标值为(1023,0),但是由于触摸误差的原因,大概率不会是绝对的(1023,0),应该是在此值附近的一个触摸坐标值,实际的上报数据如下图所示:

上报的原始数据

上图中上报的信息是按照input_event类型呈现的,这个同样在之前的input子系统中已经做了详细的介绍,这里重点来分析一下,在多点电容触摸屏上其所代表的具体含义,将上图中的数据进行整理,结果如下所示:

Makefile文件

第1行,type为0x3,说明是一个EV_ABS事件,code为0x39,为ABS_MT_TRACKING_ID。因此这一行就是input_mt_slot函数上报的ABS_MT_TRACKING_ID事件。value=0,说明屏被按下。

第2行,type为0x3,是一个EV_ABS事件,code为0x35,为ABS_MT_POSITION_X。这一行就是input_report_abs函数上报的ABS_MT_POSITION_X事件,也就是触摸点的X轴坐标。value=0x3fc=1020,说明触摸点X轴坐标为1020,属于屏幕右边区域。

第3行,type为0x3,是一个EV_ABS事件,code为0x36,为ABS_MT_POSITION_Y。这一行就是input_mt_report_slot_state函数上报的ABS_MT_POSITION_Y事件,也就是触摸点
的Y轴坐标。value=0x10=16,说明Y轴坐标为16,由此可以看出本次触摸的坐标为(1020,16),处于屏幕右上角区域。

第4行,type为0x1,是一个EV_KEY事件,code=0x14a,为BTN_TOUCH value=0x1表
示触摸屏被按下。

第5行,type为0x3,是一个EV_ABS事件,code为0x0,为ABS_X,用于单点触摸的时候上报X轴坐标。在这里和ABS_MT_POSITION_X相同,value也为0x3fc=1020。ABS_X是由input_mt_report_pointer_emulation函数上报的。

第6行,type为0x3,是一个EV_ABS事件,code为0x1,为ABS_Y,用于单点触摸的时候上报Y轴坐标。在这里和ABS_MT_POSITION_Y相同,value也为0x10=16。ABS_Y是由input_mt_report_pointer_emulation函数上报的。

第7行,type为0x0,是一个EV_SYN事件,由input_sync函数上报。

第8行,type为0x3,是一个EV_ABS事件,code为0x39,也就是ABS_MT_TRACKING_ID,value=0xffffffff=-1,说明触摸点离开了屏幕。

第9行,type为0x1,是一个EV_KEY事件,code=0x14a,为BTN_TOUCH,value=0x0表示手指离开触摸屏,也就是触摸屏没有被按下了。

第10行,type为0x0,是一个EV_SYN事件,由input_sync函数上报。

以上就是一个触摸点的坐标上报过程,和前面讲解的Type B类型设备一致。

将驱动添加到内核中

前面一直将触摸驱动编译为模块,每次系统启动以后在手动加载驱动模块,这样很不方便。当把驱动调试成功以后一般都会将其编译到内核中,这样内核启动以后就会自动加载驱动,不需要再手动modprobe了。本节就来学习一下如何将ft5x06.c添加到linux内核里面,步骤如下所示:

将驱动文件放到合适位置

首先肯定是在内核源码中找个合适的位置将ft5x06.c放进去,ft5x06.c是个触摸屏驱动,因
此需要查找一下linux内核里面触摸屏驱动放到了哪个目录下。linux内核里面将触摸屏驱动放到了drivers/input/touchscreen目录下,因此要将 ft5x06.c拷贝到此目录下,命令如下:

cp ft5x06.c (内核源码目录 )/drivers/input/touchscreen/ -f

修改对应的Makefile

修改drivers/input/touchscreen目录下的Makefile,在最下面添加下面一行:

obj-y += ft5x06.o

完成后,重新编译linux内核,然后用新的uImage启动开发板。如果驱动添加成功会输出如下图所示信息:

启动信息

从上图可以看出,触摸屏驱动已经启动了,这个时候就会自动生成/dev/input/evenvtX。在本实验中将触摸屏驱动添加到linux内核里面以后触摸屏对应的是event0,而不是前面编译为模块对应的event1,这一点一定要注意。输入如下命令,查看驱动工作是否正常:

hexdump /dev/input/event0 //查看触摸屏原始数据上报信息

结果如下图所示:

上报原始数据

可以看出,坐标数据的上报正常,说明驱动没有问题。

tslib移植与使用

tslib移植

tslib是一个开源的第三方库,用于触摸屏性能调试,使用电阻屏的时候一般使用tslib进行
校准。虽然电容屏不需要校准,但是由于电容屏加工的原因,有的时候其不一定精准,因此有时候也需要进行校准。最主要的是tslib提供了一些其他软件,可以通过这些软件来测试触摸屏工作是否正常。最新版本的tslib已经支持了多点电容触摸屏,因此可以通过tslib来直观的测试多点电容触摸屏驱动,这个要比观看eventX原始数据方便的多。

tslib的移植很简单,使用的文件系统为buildroot,只要打开图形化配置界面即可tilib的配置,进入buildroot的源码目录下,通过“make menuconfig”打开配置界面后,配置路径如下:

-> Target packages
-> Libraries
-> Hardware handling
-> [*] tslib

配置如下图所示;

tslib库的配置

保存配置在重新编译文件系统记得ubuntu要连接网络。编译完成后,进入output/images目录,运行以下命令把文件系统替换进去:

cd output/images/ //进入到 output/images目录
sudo tar -axvf rootfs.tar -C /home/zuozhongkai/linux/nfs/rootfs //解压到 nfsroot目录

上述命令将buildroot中output/images/rootfs.tar这个压缩包解压到/home/zuozhongkai/linux/nfs/rootfs这个目录中,这个目录就是教程中当前nfsroot目录,根据自己的实际情况解压到对应的目录文件中。

完成以后重启开发板!然后就可以进行测试了。

tslib测试

电容屏可以不用校准,如果是电阻屏就要先进行校准!校准的话输入如下命令:

ts_calibrate

校准完成后,如果不满意或者不小心对电容屏进行了校准,可以直接删除掉/etc/pointercal文件。

最后使用ts_test_mt这个软件来测试触摸屏工作是否正常,以及多点触摸是否有效,执行如下所示命令:

ts_test_mt

此命令会打开一个触摸测试界面,如下图所示:

ts_test_mt界面

在上图上有三个按钮“Drag”、“Draw”和“Quit”,这三个按钮的功能如下:

  • Drag:拖拽按钮,默认就是此功能,可以看到屏幕中间有一个十字光标,可以通过触摸屏幕来拖拽此光标。一个触摸点一个十字光标,对于5点电容触摸屏,如果5个手指都放到屏幕上,那么就有5个光标,一个手指一个。
  • Draw:绘制按钮,按下此按钮就可以在屏幕上进行简单的绘制,可以通过此功能检测多点触摸工作是否正常。
  • Quit:退出按钮,退出ts_test_mt测试软件。

点击“Draw”按钮,使用绘制功能5个手指一起划过屏幕,如果多点电容屏工作正常的话就会在屏幕上留下5条线。

使用内核自带的驱动

Linux内核已经集成了很多电容触摸IC的驱动文件,比如本章实验所使用FT5426。本节就来学习一下,如何使用Linux内核自带的多点电容触摸驱动。在使用之前要先将前面自己添加到内核的ft5x06.c这个文件从内核中去除掉。

内核自带的FT5426的驱动文件为drivers/input/touchscreen/edt-ft5x06.c,此驱动文件不仅仅能够驱动FT5426,FT5206、FT5406这些都可以驱动。按照如下步骤来操作,学习如何使用此驱动。

使能内核自带的FT5X06驱动

edt-ft5x06.c这个驱动默认是使能的,还是要教一下如何配置Linux内核,使能此驱动,通过图形化配置界面即可完成配置。配置路径如下:

-> Device Drivers
-> Input device support
-> Generic input layer (needed for keyboard, mouse, ...) (INPUT [=y])
-> Touchscreens (INPUT_TOUCHSCREEN [=y])
-> <*> EDT FocalTech FT5x06 I2C Touchscreen support

配置如下图所示:

使能内核自带的FT5X06驱动

修改设备树

修改之前编写的ft5426这个设备节点,需要在里面添加compatible属性,添加的内容就要参考edt-ft5x06.c文件了,edt-ft5x06.c所支持的 ompatible属性列表如下所示:

edt-ft5x06.c的compatible属性列表

可以看出,edt-ft5x06.c文件默认支持的compatible属性只有六个“edt,edt-ft5206”、“edt,edt-ft5306”和 “edt,edt-ft5406”等等。因此需要修改设备树中的ft5426节点,修改以后的节点内容如下所示:

示例代码 47.7.2 ft5426 节点内容
1  ft5246: ft5426@38 {
2      compatible = "edt,edt ft5406";
3      pinctrl-0 = <&ft5426int_reset_pins_a>;
4      reg = <0x38>;
5      interrupt-parent = <&gpioi>;
6      interrupts = <1 IRQ_TYPE_EDGE_RISING>;
8      reset-gpios = <&gpioh 15 GPIO_ACTIVE_LOW>;
9      status = "okay";
10 };

第2行,添加一条“edt,edt-ft5406”兼容性值。

修改完成以后重新编译设备树,然后使用新得到的.dtb启动linux内核。如果一切正常的话系统启动的时候就会输出如下图所示信息:

触摸屏log信息

直接运行ts_test_mt来测试触摸屏是否可以使用。如果触摸点不对可运行ts_calibrate进行校准。至此,关于Linux下的多点电容触摸驱动就结束了,重点就是掌握linux下的触摸屏上报时序,大多数都是Type B类型。

4.3寸屏触摸驱动实验

正点原子有两款4.3寸电容触摸屏,分辨率分别为800480和480272,这两款电容触摸屏的触摸驱动IC都是GT9147,因此本质上就是编写 GT9147驱动。原理和方法基本和前面讲的7寸屏所使用的FT5426一样,这里只简单讲解一下GT9147的驱动编写步骤。

在设备树的i2c2节点下添加gt9147子节点

第一肯定是修改设备树,在i2c2的节点下添加一个gt9147的子节点。添加内容如下:

示例代码 47.8.1 gt9147 子节点内容
1 gt9147: gt9147@14 {
2     compatible = "atk-gt9147";
3     reg = <0x14>;
4     interrupt-parent = <&gpioi>;
5     interrupts = <1 IRQ_TYPE_EDGE_RISING>;
6     interrupt-gpios = <&gpioi 1 GPIO_ACTIVE_LOW>;
7     reset-gpios = <&gpioh 15 GPIO_ACTIVE_LOW>;
8     status = "okay";
9 };

在使用的ST官方的内核配置,已经把gt9147这个驱动编译进内核了,所以compatible的属性值为“atk-gt9147”,免得使用内核的gt9147驱动代码。

添加屏幕参数

不同的屏幕其配置参数也不同,因此需要在drivers/gpu/drm/panel/panel-simple.c文件下,添加对应屏幕的参数,这里就根据之前LCD章节里面的学习添加就好了,我的屏幕是7寸的,这里就没怎么看。

编译GT9147驱动文件

这里的方法跟之前是一样的,可以编译拷贝过去之后,使用tslib进行测试。

如果有如下的错误:

tslib执行报错

上图中使用ts_test_mt来测试多点电容触摸,但是此时提示ts_setup错误。这个时候先用hexdump查看一下原始触摸数据有没有。如果有的话就是tslib配置错误,没有指定对触摸设备。打开/etc/profile文件,输入如下这行即可:

export TSLIB_TSDEVICE=/dev/input/event1

其中LIB_TSDEVICE表示触摸屏对应的设备,这里设置为/dev/input/event1,要根据自己的实际情况来设置。

注意,gt9147.c里面的驱动是单点触摸的,因为GT9147没有硬件检测每个触摸点的按下和抬起,因此在上报数据的时候不好处理。尝试过一些其他的处理方法,但是效果都不理想,因此改为了单点触摸。如果想直接使用Linux自带的gt9147驱动,直接修改设备树的compatible属性值为“goodix,gt9147”即可。

总结

这一章节主要就是学习了触摸屏的驱动。主要就是对之前的I2C通讯以及INPUT子系统拼接在一起,来完成驱动的编写

这里要注意的是,出教程的时候正点原子的7寸屏是FT5426的芯片,现在买的话都已经是跟4.3寸屏一样的GT9147了。

主要记住的是,咱们都是Type B的时序,会用到slot的相关函数,具体的编写都要看具体芯片的寄存器读写时序,但是大的框架都是在中断服务函数里面Type B的slot函数来更新上报坐标的。具体的x和y坐标则是要查询相关的芯片数据手册来看读写时序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值