正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)
正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)
2024-06-01 10:34:50  作者:寒颜冷若霜  网址:https://m.xinb2b.cn/tech/ork271166.html
输入设备应用编程

本章学习输入设备的应用编程,首先要知道什么是输入设备?输入设备其实就是能够产生输入事件的设备就称为输入设备,常见的输入设备包括鼠标、键盘、触摸屏、按钮等等,它们都能够产生输入事件,产生输入数据给计算机系统。

对于输入设备的应用编程其主要是获取输入设备上报的数据、输入设备当前状态等,譬如获取触摸屏当前触摸点的X、Y轴位置信息以及触摸屏当前处于按下还是松开状态。

本章将会讨论如下主题内容。

什么是输入设备;如何读取输入设备的数据;如何解析从输入设备中获取到的数据;按键、触摸屏设备如何解析数据、应用编程。输入类设备编程介绍什么是输入设备

先来了解什么是输入设备(也称为input设备),常见的输入设备有鼠标、键盘、触摸屏、遥控器、电脑画图板等,用户通过输入设备与系统进行交互。

input子系统

由上面的介绍可知,输入设备种类非常多,每种设备上报的数据类型又不一样,那么linux系统如何管理呢?Linux系统为了统一管理这些输入设备,实现了一套能够兼容所有输入设备的框架,那么这个框架就是input子系统。驱动开发人员基于input子系统开发输入设备的驱动程序,input子系统可以屏蔽硬件的差异,向应用层提供一套统一的接口。

基于input子系统注册成功的输入设备,都会在/dev/input目录下生成对应的设备节点(设备文件),设备节点名称通常为eventX(X表示一个数字编号0、1、2、3等),譬如/dev/input/event0、/dev/input/event1、/dev/input/event2等,通过读取这些设备节点可以获取输入设备上报的数据。

读取数据的流程

如果我们要读取触摸屏的数据,假设触摸屏设备对应的设备节点为/dev/input/event0,那么数据读取流程如下:

①、应用程序打开/dev/input/event0设备文件;

②、应用程序发起读操作(譬如调用read),如果没有数据可读则会进入休眠(阻塞I/O情况下);

③、当有数据可读时,应用程序会被唤醒,读操作获取到数据返回;

④、应用程序对读取到的数据进行解析。

当无数据可读时,程序会进入休眠状态(也就是阻塞),譬如应用程序读触摸屏数据,如果当前并没有去触碰触摸屏,自然是无数据可读;当我们用手指触摸触摸屏或者在屏上滑动时,此时就会产生触摸数据、应用程序就有数据可读了,应用程序会被唤醒,成功读取到数据。那么对于其它输入设备亦是如此,无数据可读时应用程序会进入休眠状态(阻塞式I/O方式下),当有数据可读时才会被唤醒。

应用程序如何解析数据

首先我们要知道,应用程序打开输入设备对应的设备文件,向其发起读操作,那么这个读操作获取到的是什么样的数据呢?其实每一次read操作获取的都是一个struct input_event结构体类型数据,该结构体定义在<linux/input.h>头文件中,它的定义如下:

示例代码 18.1.1 struct input_event结构体struct input_event {struct timeval time;__u16 type;__u16 code;__s32 value;};

结构体中的time成员变量是一个struct timeval类型的变量,该结构体在前面给大家介绍过,内核会记录每个上报的事件其发生的时间,并通过变量time返回给应用程序。时间参数通常不是那么重要,而其它3个成员变量type、code、value更为重要。

type:type用于描述发生了哪一种类型的事件(对事件的分类),Linux系统所支持的输入事件类型如下所示:

#define EV_SYN 0x00 //同步类事件,用于同步事件#define EV_KEY 0x01 //按键类事件#define EV_REL 0x02 //相对位移类事件(譬如鼠标)#define EV_ABS 0x03 //绝对位移类事件(譬如触摸屏)#define EV_MSC 0x04 //其它杂类事件#define EV_SW 0x05#define EV_LED 0x11#define EV_SND 0x12#define EV_REP 0x14#define EV_FF 0x15#define EV_PWR 0x16#define EV_FF_STATUS 0x17#define EV_MAX 0x1f#define EV_CNT (EV_MAX 1)

以上这些宏定义也是在<linux/input.h>头文件中,所以在应用程序中需要包含该头文件;一种输入设备通常可以产生多种不同类型的事件,譬如点击鼠标按键(左键、右键,或鼠标上的其它按键)时会上报按键类事件,移动鼠标时则会上报相对位移类事件。

code:code表示该类事件中的哪一个具体事件,以上列举的每一种事件类型中都包含了一系列具体事件,譬如一个键盘上通常有很多按键,譬如字母A、B、C、D或者数字1、2、3、4等,而code变量则告知应用程序是哪一个按键发生了输入事件。每一种事件类型都包含多种不同的事件,譬如按键类事件:

#define KEY_RESERVED 0#define KEY_ESC 1 //ESC键#define KEY_1 2 //数字1键#define KEY_2 3 //数字2键#define KEY_TAB 15 //TAB键#define KEY_Q 16 //字母Q键#define KEY_W 17 //字母W键#define KEY_E 18 //字母E键#define KEY_R 19 //字母R键……相对位移事件#define REL_X 0x00 //X轴#define REL_Y 0x01 //Y轴#define REL_Z 0x02 //Z轴#define REL_RX 0x03#define REL_RY 0x04#define REL_RZ 0x05#define REL_HWHEEL 0x06#define REL_DIAL 0x07#define REL_WHEEL 0x08#define REL_MISC 0x09#define REL_MAX 0x0f#define REL_CNT (REL_MAX 1)

绝对位移事件

触摸屏设备是一种绝对位移设备,它能够产生绝对位移事件;譬如对于触摸屏来说,一个触摸点所包含的信息可能有多种,譬如触摸点的X轴坐标、Y轴坐标、Z轴坐标、按压力大小以及接触面积等,所以code变量告知应用程序当前上报的是触摸点的哪一种信息(X坐标还是Y坐标、亦或者其它);绝对位移事件如下:

#define ABS_X 0x00 //X轴#define ABS_Y 0x01 //Y轴#define ABS_Z 0x02 //Z轴#define ABS_RX 0x03#define ABS_RY 0x04#define ABS_RZ 0x05#define ABS_THROTTLE 0x06#define ABS_RUDDER 0x07#define ABS_WHEEL 0x08#define ABS_GAS 0x09#define ABS_BRAKE 0x0a#define ABS_HAT0X 0x10#define ABS_HAT0Y 0x11#define ABS_HAT1X 0x12#define ABS_HAT1Y 0x13#define ABS_HAT2X 0x14#define ABS_HAT2Y 0x15#define ABS_HAT3X 0x16#define ABS_HAT3Y 0x17#define ABS_PRESSURE 0x18#define ABS_DISTANCE 0x19#define ABS_TILT_X 0x1a#define ABS_TILT_Y 0x1b#define ABS_TOOL_WIDTH 0x1c......

除了以上列举出来的之外,还有很多,大家可以自己浏览<linux/input.h>头文件(这些宏其实是定义在input-event-codes.h头文件中,该头文件被<linux/input.h>所包含了),关于这些具体的事件,后面再给大家进行介绍。

value:内核每次上报事件都会向应用层发送一个数据value,对value值的解释随着code的变化而变化。譬如对于按键事件(type=1)来说,如果code=2(键盘上的数字键1,也就是KEY_1),那么如果value等于1,则表示KEY_1键按下;value等于0表示KEY_1键松开,如果value等于2则表示KEY_1键长按。再比如,在绝对位移事件中(type=3),如果code=0(触摸点X坐标ABS_X),那么value值就等于触摸点的X轴坐标值;同理,如果code=1(触摸点Y坐标ABS_Y),此时value值便等于触摸点的Y轴坐标值;所以对value值的解释需要根据不同的code值而定!

数据同步

上面我们提到了同步事件类型EV_SYN,同步事件用于实现同步操作、告知接收者本轮上报的数据已经完整。应用程序读取输入设备上报的数据时,一次read操作只能读取一个struct input_event类型数据,譬如对于触摸屏来说,一个触摸点的信息包含了X坐标、Y坐标以及其它信息,对于这样情况,应用程序需要执行多次read操作才能把一个触摸点的信息全部读取出来,这样才能得到触摸点的完整信息。

那么应用程序如何得知本轮已经读取到完整的数据了呢?其实这就是通过同步事件来实现的,内核将本轮需要上报、发送给接收者的数据全部上报完毕后,接着会上报一个同步事件,以告知应用程序本轮数据已经完整、可以进行同步了。

同步类事件中也包含了多种不同的事件,如下所示:

#define SYN_REPORT 0#define SYN_CONFIG 1#define SYN_MT_REPORT 2#define SYN_DROPPED 3#define SYN_MAX 0xf#define SYN_CNT (SYN_MAX 1)

所以的输入设备都需要上报同步事件,上报的同步事件通常是SYN_REPORT,而value值通常为0。

读取struct input_event数据

根据前面的介绍可知,对输入设备调用read()会读取到一个struct input_event类型数据,本小节编写一个简单地应用程序,将读取到的struct input_event类型数据中的每一个元素打印出来、并对它们进行解析。

本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->18_input->read_input.c。

示例代码 18.2.1 读取struct input_event类型数据#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <linux/input.h>int main(int argc, char *argv[]){struct input_event in_ev = {0};int fd = -1;if (2 != argc) {fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);exit(-1);}if (0 > (fd = open(argv[1], O_RDONLY))) {perror("open error");exit(-1);}for ( ; ; ) {if (sizeof(struct input_event) !=read(fd, &in_ev, sizeof(struct input_event))) {perror("read error");exit(-1);}printf("type:%d code:%d value:%d\n",in_ev.type, in_ev.code, in_ev.value);}}

执行程序时需要传入参数,这个参数就是对应的输入设备的设备节点(设备文件),程序中会对传参进行校验。程序中首先调用open()函数打开设备文件,之后在for循环中调用read()函数读取文件,将读取到的数据存放在struct input_event结构体对象中,之后将结构体对象中的各个成员变量打印出来。注意,程序中使用了阻塞式I/O方式读取设备文件,所以当无数据可读时read调用会被阻塞,知道有数据可读时才会被唤醒!

Tips:设备文件不同于普通文件,读写设备文件之前无需设置读写位置偏移量。

使用交叉编译工具编译上述代码得到可执行文件testApp:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(1)

图 18.2.1 编译示例代码

在开发板上验证

ALPHA和Mini开发板上都有一个用户按键KEY0,它就是一个典型的输入设备,如下图所示:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(2)

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(3)

图 18.3.1 用户按键KEY0(左图为ALPHA、右图为Mini)

该按键是提供给用户使用的一个GPIO按键,在出厂系统中,该按键驱动基于input子系统而实现,所以在/dev/input目录下存在KEY0的设备节点,具体是哪个设备节点,可以使用14.1.1小节介绍的方法进行判断,这里不再重述!也可以通过查看/proc/bus/input/devices文件得知,查看该文件可以获取到系统中注册的所有输入设备相关的信息,如下所示:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(4)

图 18.3.2 查看/proc/bus/input/devices文件

接下来我们使用这个按键进行测试,执行下面的命令:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(5)

图 18.3.3 测试

程序运行后,执行按下KEY0、松开KEY0等操作,终端将会打印出相应的信息,如上图所示。

第一行中type等于1,表示上报的是按键事件EV_KEY,code=114,打开input-event-codes.h头文件进行查找,可以发现cpde=114对应的是键盘上的KEY_VOLUMEDOWN按键,这个是ALPHA/Mini开发板出厂系统已经配置好的。而value=1表示按键按下,所以整个第一行的意思就是按键KEY_VOLUMEDOWN被按下。

第二行,表示上报了EV_SYN同步类事件(type=0)中的SYN_REPORT事件(code=0),表示本轮数据已经完整、报告同步。

第三行,type等于1,表示按键类事件,code等于114、value等于0,所以表示按键KEY_VOLUMEDOWN被松开。

第四行,又上报了同步事件。

所以整个上面4行的打印信息就是开发板上的KEY0按键被按下以及松开这个过程,内核所上报的事件以及发送给应用层的数据value。

我们试试长按按键KEY0,按住不放,如下所示:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(6)

图 18.3.4 长按按键KEY0

可以看到上报按键事件时,对应的value等于2,表示长按状态。

按键应用编程

本小节编写一个应用程序,获取按键状态,判断按键当前是按下、松开或长按状态。从上面打印的信息可知,对于按键来说,它的事件上报流程如下所示:

# 以字母A键为例KEY_A //上报KEY_A事件SYN_REPORT //同步

如果是按下,则上报KEY_A事件时,value=1;如果是松开,则value=0;如果是长按,则value=2。

接下来编写按键应用程序,读取按键状态并将结果打印出来,代码如下所示:

本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->18_input->read_key.c。

示例代码 18.4.1 按键应用编程#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <linux/input.h>int main(int argc, char *argv[]){struct input_event in_ev = {0};int fd = -1;int value = -1;if (2 != argc) {fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);exit(-1);}if (0 > (fd = open(argv[1], O_RDONLY))) {perror("open error");exit(-1);}for ( ; ; ) {if (sizeof(struct input_event) !=read(fd, &in_ev, sizeof(struct input_event))) {perror("read error");exit(-1);}if (EV_KEY == in_ev.type) { //按键事件switch (in_ev.value) {case 0:printf("code<%d>: 松开\n", in_ev.code);break;case 1:printf("code<%d>: 按下\n", in_ev.code);break;case 2:printf("code<%d>: 长按\n", in_ev.code);break;}}}}

在for循环中,调用read()读取输入设备上报的数据,当按键按下或松开(以及长按)动作发生时,read()会读取到输入设备上报的数据,首先判断此次上报的事件是否是按键类事件(EV_KEY),如果是按键类事件、接着根据value值来判断按键当前的状态是松开、按下还是长按。

将上述代码进行编译:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(7)

图 18.4.1 编译示例代码

将编译得到的可执行文件拷贝开发板Linux系统的/home/root目录下。

首先我们来测试开发板的KEY0按键,执行应用程序:

./testApp /dev/input/event2 # 测试开发板上的KEY0

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(8)

图 18.4.2 测试KEY0

运行程序之后,按下KEY0或松开KEY0以及长按情况下,终端会打印出相应的信息,如上图所示。code=114(KEY_VOLUMEDOWN按键)。

除了测试开发板上的KEY0按键之外,我们还可以测试键盘上的按键,首先找到一个USB键盘连接到开发板的USB HOST接口上,当键盘插入之后,终端将会打印出相应的驱动加载信息:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(9)

图 18.4.3 插入USB键盘终端打印信息

驱动加载成功之后,可以查看下该键盘设备对应的设备节点,使用命令"cat /proc/bus/input/devices",在打印信息中找到键盘设备的信息:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(10)

图 18.4.4 USB键盘设备信息

譬如笔者使用的是一个罗技的USB键盘"Logitech USB Keyboard",对应的设备节点为/dev/input/event3,运行测试程序并按下、松开键盘上的按键:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(11)

图 18.4.5 测试USB键盘

大家可以根据code值查询到对应的按键(通过input-event-codes.h头文件),譬如code=30对应的是键盘上的字母A键,code=48对应的是字母B键。

触摸屏应用编程

本小节编写触摸屏应用程序,获取触摸屏的坐标信息并将其打印出来。

解析触摸屏设备上报的数据

触摸屏设备是一个绝对位移设备,可以上报绝对位移事件,绝对位移事件如下:

#define ABS_X 0x00 //X轴坐标#define ABS_Y 0x01 //Y轴坐标#define ABS_Z 0x02 //Z轴坐标#define ABS_RX 0x03#define ABS_RY 0x04#define ABS_RZ 0x05#define ABS_THROTTLE 0x06#define ABS_RUDDER 0x07#define ABS_WHEEL 0x08#define ABS_GAS 0x09#define ABS_BRAKE 0x0a#define ABS_HAT0X 0x10#define ABS_HAT0Y 0x11#define ABS_HAT1X 0x12#define ABS_HAT1Y 0x13#define ABS_HAT2X 0x14#define ABS_HAT2Y 0x15#define ABS_HAT3X 0x16#define ABS_HAT3Y 0x17#define ABS_PRESSURE 0x18 //按压力#define ABS_DISTANCE 0x19#define ABS_TILT_X 0x1a#define ABS_TILT_Y 0x1b#define ABS_TOOL_WIDTH 0x1c#define ABS_VOLUME 0x20#define ABS_MISC 0x28#define ABS_MT_SLOT 0x2f #define ABS_MT_TOUCH_MAJOR 0x30 #define ABS_MT_TOUCH_MINOR 0x31 #define ABS_MT_WIDTH_MAJOR 0x32 #define ABS_MT_WIDTH_MINOR 0x33 #define ABS_MT_ORIENTATION 0x34 #define ABS_MT_POSITION_X 0x35 //X轴坐标#define ABS_MT_POSITION_Y 0x36 //Y轴坐标#define ABS_MT_TOOL_TYPE 0x37 #define ABS_MT_BLOB_ID 0x38 #define ABS_MT_TRACKING_ID 0x39 ID#define ABS_MT_PRESSURE 0x3a //按压力#define ABS_MT_DISTANCE 0x3b #define ABS_MT_TOOL_X 0x3c #define ABS_MT_TOOL_Y 0x3d #define ABS_MAX 0x3f#define ABS_CNT (ABS_MAX 1)

单点触摸和多点触摸

触摸屏分为多点触摸设备和单点触摸设备。单点触摸设备只支持单点触摸,一轮(笔者把一个同步事件称为一轮)完整的数据只包含一个触摸点信息;单点触摸设备以ABS_XXX事件承载、上报触摸点的信息,譬如ABS_X(value值对应的是X轴坐标值)、ABS_Y(value值对应的是Y轴坐标值)等绝对位移事件,而有些设备可能还支持Z轴坐标(通过ABS_Z事件上报、value值对应的便是Z轴坐标值)、按压力大小(通过ABS_PRESSURE事件上报、value值对应的便是按压力大小)以及接触面积等属性。大部分的单点触摸设备都会上报ABS_X和ABS_Y事件,而其它绝对位移事件则根据具体的设备以及驱动的实现而定!

多点触摸设备可支持多点触摸,譬如ALPHA/Mini开发板配套使用的4.3寸、7寸等触摸屏均支持多点触摸,对于多点触摸设备,一轮完整的数据可能包含多个触摸点信息。多点触摸设备则是以ABS_MT_XXX(MT是Multi-touch,意思为:多点触摸)事件承载、上报触摸点的信息,如ABS_MT_POSITION_X(X轴坐标)、ABS_MT_POSITION_Y(Y轴坐标)等绝对位移事件。

触摸屏设备除了上报绝对位移事件之外,还可以上报按键类事件和同步类事件。同步事件很好理解,因为几乎每一个输入设备都会上报同步事件、告知应用层本轮数据是否完整;当手指点击触摸屏或手指从触摸屏离开时,此时就会上报按键类事件,用于描述按下触摸屏和松开触摸屏;具体的按键事件为BTN_TOUCH(code=0x14a,也就是330),当然,手指在触摸屏上滑动不会上报BTN_TOUCH事件。

Tips:BTN_TOUCH事件不支持长按状态,故其value不会等于2。对于多点触摸设备来说,只有第一个点按下时上报BTN_TOUCH事件value=1、当最后一个点离开触摸屏时上报BTN_TOUCH事件value=0。

单点触摸设备--事件上报的顺序

通过上面的介绍可知,单点触摸设备事件上报的流程大概如下所示:

# 点击触摸屏时BTN_TOUCHABS_XABS_YSYN_REPORT# 滑动ABS_XABS_YSYN_REPORT# 松开BTN_TOUCHSYN_REPORT

以上列举出只是一个大致流程,实际上对于不同的触摸屏设备,能够获取到的信息量大小是不相同的,譬如某设备只能读取到触摸点的X和Y坐标、而另一设备却能读取X、Y坐标以及按压力大小、触点面积等信息,总之这些数据都会在SYN_REPORT同步事件之前上报给应用层。

当手指点击触摸屏时,首先上报BTN_TOUCH事件,此时value=1,表示按下;接着上报ABS_X、ABS_Y事件将X、Y轴坐标数据发送给应用层;数据上报完成接着上报一个同步事件SYN_REPORT,表示此次触摸点信息已经完整。

当手指在触摸屏上滑动时,并不会上报BTN_TOUCH事件,因为滑动过程并未发生按下、松开这种动作。

当松开时,首先上报了BTN_TOUCH事件,此时value=0,表示手指已经松开了触摸屏,接着上报一个同步事件SYN_REPORT。

以上就是单点触摸设备事件上报的一个大致流程,接下来看看多点触摸设备。

多点触摸设备--事件上报的顺序

多点触摸设备上报的一轮完整数据中可能包含多个触摸点的信息,譬如5点触摸设备,如果5个手指同时在触摸屏上滑动,那么硬件就会更新5个触摸点的信息,内核需要把这5个触摸点的信息上报给应用层。

在Linux内核中,多点触摸设备使用多点触摸(MT)协议上报各个触摸点的数据,MT协议分为两种类型:Type A和Type B,Type A协议实际使用中用的比较少,几乎处于淘汰的边缘,这里就不再给大家介绍了,我们重点来看看Type B协议。

MT协议之Type B协议

Type B协议适用于能够追踪并区分触摸点的设备,开发板配套使用的触摸屏都属于这类设备。Type B协议的重点是通过ABS_MT_SLOT事件上报各个触摸点信息的更新!

能够追踪并区分触摸点的设备通常在硬件上能够区分不同的触摸点,譬如对于一个5点触摸设备来说,硬件能够为每一个识别到的触摸点与一个slot进行关联,这个slot就是一个编号,触摸点0、触摸点1、触摸点2等。底层驱动向应用层上报ABS_MT_SLOT事件,此事件会告诉接收者当前正在更新的是哪个触摸点的数据,ABS_MT_SLOT事件中对应的value数据存放的便是一个slot、以告知应用层当前正在更新slot关联的触摸点对应的信息。

每个识别出来的触摸点分配一个slot,与该slot关联起来,利用这个slot来传递对应触点的变化。除了ABS_MT_SLOT事件之外,Type B协议还会使用到ABS_MT_TRACTKING_ID事件,ABS_MT_TRACTKING_ID事件则用于触摸点的创建、替换和销毁工作,ABS_MT_TRACTKING_ID事件携带的数据value表示一个ID,一个非负数的ID(ID>=0)表示一个有效的触摸点,如果ID等于-1表示该触摸点已经不存在、被移除了;一个以前不存在的ID表示这是一个新的触摸点。

Type B协议可以减少发送到用户空间的数据,只有发生了变更的数据才会上报,譬如某个触摸点发生了移动,但仅仅只改变了X轴坐标、而未改变Y轴坐标,那么内核只会将改变后的X坐标值通过ABS_MT_POSITION_X事件发送给应用层。

以上关于Type B协议就给大家介绍这么多,为了帮助大家理解,笔者把Type B协议下多点触摸设备上报数据的流程列举如下:

ABS_MT_SLOT 0ABS_MT_TRACKING_ID 10ABS_MT_POSITION_XABS_MT_POSITION_YABS_MT_SLOT 1ABS_MT_TRACKING_ID 11ABS_MT_POSITION_XABS_MT_POSITION_YSYN_REPORT

单看这个可能大家看不懂,接下来我们打印触摸屏的数据一个一个进行分析。

Tips:大家可能会对slot和ID这两个概念有点混乱,这里笔者将自己的理解告知大家;slot是硬件上的一个概念、而ID则可认为是软件上的一个概念;对于一个多点触摸设备来说,它最大支持的触摸点数是确定的,譬如5个触摸设备,最多支持5个触摸点;每一个触摸点在硬件上它有一个区分的编号,譬如触摸点0、触摸点1、触摸点2等,这个编号就是一个slot(通常从0开始);如何给识别到的触点分配一个slot呢(触点与slot关联)?通常是按照时间先后顺序来的,譬如第一根手指先触碰到触摸屏,那第一根手指就对应触摸点0(slot=0),接着第二根手指触碰到触摸屏则对应触摸点1(slot=1)以此类推!这个通常是硬件所支持的。

而ID可认为是软件上的一个概念,它也用于区分不同的触摸点,但是它跟slot不同,不是同一层级的概念;举个例子,譬如一根手指触碰到触摸屏之后拿开,然后再次触碰触摸屏,这个过程中,假设只有这一根手指进行触碰操作,那么两次触碰对应都是触摸点0(slot=0),这个无疑义!但从触摸点的生命周期来看,它们是同一个触摸点吗?答案肯定不是,为啥呢?手指从触摸屏上离开后,该触摸点就消失了、被删除了,该触摸点的生命周期也就到此结束了,所以它们自然是不同的触摸点,所以它们的ID是不同的。

触摸屏上报数据分析

首先在测试触摸屏之前,需要保证开发板上已经连接了LCD屏,ALPHA/Mini I.MX6U开发板出厂系统配套支持多种不同分辨率的LCD屏,包括4.3寸480*272、4.3寸800*480、7寸800*480、7寸1024*600以及10.1寸1280*800,在启动开发板之前需要将LCD屏通过软排线连接到开发板的LCD接口,开发板连接好LCD屏之后上电启动开发板、运行出厂系统。

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(12)

图 18.5.1 连接LCD屏(ALPHA板)

触摸屏与LCD液晶屏面板是粘合在一起的,也就是说触摸屏是直接贴在了LCD液晶屏上面,直接在LCD屏上触摸、滑动操作即可。为了测试方便,可以将出厂系统的GUI应用程序退出,如何退出呢?点击屏幕进入设置页面,可以看到在该页面下有一个退出按钮选项,直接点击即可!

使用命令"cat /proc/bus/input/devices",确定触摸屏对应的设备节点,如下所示:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(13)

图 18.5.2 触摸屏输入设备的信息(这里是以4.3寸800*480屏为例)

笔者使用的是开发板配套的4.3寸800*480 LCD屏,如果各位读者使用的是其它屏,那么查看到的名称可能不是"goodix-ts"。执行示例代码 18.2.1对应的可执行文件,一个手指点击触摸屏先不松开,终端将会打印如下信息:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(14)

图 18.5.3 单击不放

首先第一行上报了绝对位移事件EV_ABS(type=3)中的ABS_MT_TRACKING_ID(code=57)事件,并且value值等于78,也就是ID,这个ID是一个非负数,所以表示这是一个新的触摸点被创建,也就意味着触摸屏上产生了一个新的触摸点(手指按下)。

第二行上报了绝对位移事件EV_ABS(type=3)中的ABS_MT_POSITION_X(code=53)事件,其value对应的便是触摸点的X坐标;第三行上报了ABS_MT_POSITION_Y(code=54)事件,其value值对应的便是触摸点Y坐标,所以由此可知该触摸点的坐标为(372, 381)。

第四行上报了按键类事件EV_KEY(type=1)中的BTN_TOUCH(code=330),value值等于1,表示这是触摸屏上最先产生的触摸点(slot=0、也就是触摸点0)。

第五行和第六行分别上报了绝对位移事件EV_ABS(type=3)中的ABS_X(code=0)和ABS_Y(code=1),其value分别对应的是触摸点的X坐标和Y坐标。多点触摸设备也会通过ABS_X、ABS_Y事件上报触摸点的X、Y坐标,但通常只有触摸点0支持,所以可以把多点触摸设备当成单点触摸设备来使用。

最后一行上报了同步类事件EV_SYN(type=0)中的SYN_REPORT(code=0)事件,表示此次触摸点的信息全部上报完毕。

在第一个触摸点的基础上,增加第二个触摸点,打印信息如下所示:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(15)

图 18.5.4 增加触摸点

1~7行不再解释,第八行上报了绝对位移事件EV_ABS(type=3)中的ABS_MT_SLOT事件(code=47),表示目前要更新slot=1所关联的触摸点(也就是触摸点1)对应的信息。

第九行上报了绝对位移事件EV_ABS(type=3)中的ABS_MT_TRACKING_ID事件(code=57),ID=79,这是之前没有出现过的ID,表示这是一个新的触摸点。

第十、十一行分别上报了ABS_MT_POSITION_X和ABS_MT_POSITION_Y事件。

最后一行上报同步事件(type=0、code=0),告知应用层数据完整。

当手指松开时,触摸点就会被销毁,上报ABS_MT_TRACKING_ID事件,并将value设置为-1(ID),如下所示:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(16)

图 18.5.5 触摸点被销毁

关于触摸屏数据的分析就给大家介绍这么多,不管是键盘也好、或者是鼠标、触摸屏,都可以像上面那样将输入设备的数据直接打印出来,然后自己再去分析,确定该输入设备上报事件的规则和流程,把这些弄懂之后再去编写程序验证结果。下面我们将自己动手编写单点触摸以及多点触摸应用程序,读取触摸点的坐标信息。

获取触摸屏的信息

本小节介绍如何获取触摸屏设备的信息,譬如触摸屏支持的最大触摸点数、触摸屏X、Y坐标的范围等。通过ioctl()函数可以获取到这些信息,3.10.2小节给大家介绍过该函数,ioctl()是一个文件I/O操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或设备文件,为了方便讲解,再次把ioctl()函数的原型列出:

#include <sys/ioctl.h>int ioctl(int fd, unsigned long request, ...);

第一个参数fd对应文件描述符;第二个参数request与具体要操作的对象有关,没有统一值,表示向文件描述符请求相应的操作,也就是请求指令;此函数是一个可变参函数,第三个参数需要根据request参数来决定,配合request来使用。

先来看下输入设备的ioctl()该怎么用,在input.h头文件有这样一些宏定义,如下所示:

示例代码 18.5.1 EVIOC相关的宏定义#define EVIOCGVERSION _IOR('E', 0x01, int) #define EVIOCGID _IOR('E', 0x02, struct input_id) #define EVIOCGREP _IOR('E', 0x03, unsigned int[2]) #define EVIOCSREP _IOW('E', 0x03, unsigned int[2]) #define EVIOCGKEYCODE _IOR('E', 0x04, unsigned int[2]) #define EVIOCGKEYCODE_V2 _IOR('E', 0x04, struct input_keymap_entry)#define EVIOCSKEYCODE _IOW('E', 0x04, unsigned int[2]) #define EVIOCSKEYCODE_V2 _IOW('E', 0x04, struct input_keymap_entry)#define EVIOCGNAME(len) _IOC(_IOC_READ, 'E', 0x06, len) #define EVIOCGPHYS(len) _IOC(_IOC_READ, 'E', 0x07, len) #define EVIOCGUNIQ(len) _IOC(_IOC_READ, 'E', 0x08, len) #define EVIOCGPROP(len) _IOC(_IOC_READ, 'E', 0x09, len) #define EVIOCGMTSLOTS(len) _IOC(_IOC_READ, 'E', 0x0a, len)#define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len) #define EVIOCGLED(len) _IOC(_IOC_READ, 'E', 0x19, len) #define EVIOCGSND(len) _IOC(_IOC_READ, 'E', 0x1a, len) #define EVIOCGSW(len) _IOC(_IOC_READ, 'E', 0x1b, len) #define EVIOCGBIT(ev,len) _IOC(_IOC_READ, 'E', 0x20 (ev), len) #define EVIOCGABS(abs) _IOR('E', 0x40 (abs), struct input_absinfo) #define EVIOCSABS(abs) _IOW('E', 0xc0 (abs), struct input_absinfo) #define EVIOCSFF _IOW('E', 0x80, struct ff_effect) #define EVIOCRMFF _IOW('E', 0x81, int) #define EVIOCGEFFECTS _IOR('E', 0x84, int) #define EVIOCGRAB _IOW('E', 0x90, int) #define EVIOCREVOKE _IOW('E', 0x91, int)

每一个宏定义后面都有相应的注释,对于input输入设备,对其执行ioctl()操作需要使用这些宏,不同的宏表示不同请求指令;譬如使用EVIOCGNAME宏获取设备名称,使用方式如下:

char name[100];ioctl(fd, EVIOCGNAME(sizeof(name)), name);

EVIOCGNAME(len)就表示用于接收字符串数据的缓冲区大小,而此时ioctl()函数的第三个参数需要传入一个缓冲区的地址,该缓冲区用于存放设备名称对应的字符串数据。

EVIOCG(get)开头的表示获取信息,EVIOCS(set)开头表示设置;这里暂且不管其它宏,重点来看看EVIOCGABS(abs)宏,这个宏也是通常使用最多的,如下所示:

#define EVIOCGABS(abs) _IOR('E', 0x40 (abs), struct input_absinfo)

通过这个宏可以获取到触摸屏slot(slot<0>表示触摸点0、slot<1>表示触摸点1、slot<2>表示触摸点2,以此类推!)的取值范围,可以看到使用该宏需要传入一个abs参数,该参数表示为一个ABS_XXX绝对位移事件,譬如EVIOCGABS(ABS_MT_SLOT)表示获取触摸屏的slot信息,此时ioctl()函数的第三个参数是一个struct input_absinfo *的指针,指向一个struct input_absinfo对象,调用ioctl()会将获取到的信息写入到struct input_absinfo对象中。struct input_absinfo结构体如下所示:

示例代码 18.5.2 struct input_absinfo结构体struct input_absinfo {__s32 value; //最新的报告值__s32 minimum; //最小值__s32 maximum; //最大值__s32 fuzz;__s32 flat;__s32 resolution;};

获取触摸屏支持的最大触摸点数:

struct input_absinfo info;int max_slots; //最大触摸点数if (0 > ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &info))perror("ioctl error");max_slots = info.maximum 1 - info.minimum;

其它宏定义不再介绍,读者可以自行测试。

获取触摸屏支持的最大触摸点数

本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->18_input->read_slot.c。

示例代码 18.5.3 读取触摸屏支持的最大触摸点数#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <linux/input.h>int main(int argc, char *argv[]){struct input_absinfo info;int fd = -1;int max_slots;if (2 != argc) {fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);exit(EXIT_FAILURE);}if (0 > (fd = open(argv[1], O_RDONLY))) {perror("open error");exit(EXIT_FAILURE);}if (0 > ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &info)) {perror("ioctl error");close(fd);exit(EXIT_FAILURE);}max_slots = info.maximum 1 - info.minimum;printf("max_slots: %d\n", max_slots);close(fd);exit(EXIT_SUCCESS);}

编译示例代码,将其拷贝到开发板Linux系统的用户家目录下,执行该应用程序:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(17)

图 18.5.6 运行结果

所以从打印结果可知,我们这个屏是一个5点触摸屏。

单点触摸应用程序

通过上面的详细介绍,详细大家应该知道如何去编写一个触摸屏的应用程序了,本小节我们编写一个单点触摸应用程序,获取一个触摸点的坐标信息,并将其打印出来。

ALPHA/Mini开发板配套使用的触摸屏均支持多点触摸,这里我们把它当成单点触摸设备来使用,编写一个程序读取一个触摸点,示例代码如下所示:

本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->18_input->read_ts.c。

示例代码 18.5.4 单点触摸应用程序#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <linux/input.h>int main(int argc, char *argv[]){struct input_event in_ev;int x, y; //触摸点x和y坐标int down; //用于记录BTN_TOUCH事件的value,1表示按下,0表示松开,-1表示移动int valid; //用于记录数据是否有效(我们关注的信息发生更新表示有效,1表示有效,0表示无效)int fd = -1;if (2 != argc) {fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);exit(EXIT_FAILURE);}if (0 > (fd = open(argv[1], O_RDONLY))) {perror("open error");exit(EXIT_FAILURE);}x = y = 0; //初始化x和y坐标值down = -1; //初始化<移动>valid = 0;//初始化<无效>for ( ; ; ) {if (sizeof(struct input_event) !=read(fd, &in_ev, sizeof(struct input_event))) {perror("read error");exit(EXIT_FAILURE);}switch (in_ev.type) {case EV_KEY: //按键事件if (BTN_TOUCH == in_ev.code) {down = in_ev.value;valid = 1;}break;case EV_ABS: //绝对位移事件switch (in_ev.code) {case ABS_X: //X坐标x = in_ev.value;valid = 1;break;case ABS_Y: //Y坐标y = in_ev.value;valid = 1;break;}break;case EV_SYN: //同步事件if (SYN_REPORT == in_ev.code) {if (valid) {//判断是否有效switch (down) {//判断状态case 1:printf("按下(%d, %d)\n", x, y);break;case 0:printf("松开\n");break;case -1:printf("移动(%d, %d)\n", x, y);break;}valid = 0; //重置validdown = -1; //重置down}}break;}}}

程序中首先校验传参,通过传参的方式将触摸屏设备文件路径传入到程序中,main()函数中定义了4个变量:

⑴、变量x表示触摸点的X坐标;

⑵、变量y表示触摸点的Y坐标;

⑶、变量down表示手指状态时候按下、松开还是滑动,down=1表示手指按下、down=0表示手指松开、down=-1表示手指滑动;

⑷、变量valid表示数据是否有效,valid=1表示有效、valid=0表示无效;有效指的是我们检测的信息发生了更改,譬如程序中只检测了手指的按下、松开动作以及坐标值的变化。

接着调用open()打开触摸屏设备文件得到文件描述符fd;在for循环之前,首先对x、y、down、valid这4个变量进行初始化操作。在for循环读取触摸屏上报的数据,将读取到的数据存放在struct input_event数据结构中。在switch…case语句中对读取到的数据进行解析,获取BTN_TOUCH事件的value数据,判断触摸屏是按下还是松开状态,获取ABS_X和ABS_Y事件的value变量,得到触摸点的X轴坐标和Y轴坐标。

当上报同步事件时,表示数据已经完整,接着对我们得到的数据进行分析、打印坐标信息。

编译应用程序:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(18)

图 18.5.7 编译应用程序

将编译得到的可执行文件拷贝到开发板Linux系统的用户家目录下,准备进行测试。

笔者使用的是4.3寸800*480 LCD屏,执行单点触摸应用程序,程序执行之后,接着用一个手指按下触摸屏、松开以及滑动操作,串口终端将会打印出相应的信息:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(19)

图 18.5.8 单点触摸应用程序测试结果

当手指点击触摸屏时会打印"按下(X, Y)",松开时会打印"松开",手指在触摸屏上滑动时会打印"移动(X, Y)"等信息。大家可以自己动手测试,对代码不理解的,可以对照测试结果进行对比。

多点触摸应用程序

介绍完单点触摸应用程序之后,再来看看多点触摸应用程序该如何编写,前面已经详细给大家介绍了多点触摸设备的事件上报流程。

本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->18_input->read_mt.c。

示例代码 18.5.5 多点触摸应用程序#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <sys/ioctl.h>#include <unistd.h>#include <string.h>#include <linux/input.h>struct ts_mt {int x; //X坐标int y; //Y坐标int id; //对应ABS_MT_TRACKING_IDint valid; //数据有效标志位(=1表示触摸点信息发生更新)};struct tp_xy {int x;int y;};static int ts_read(const int fd, const int max_slots,struct ts_mt *mt){struct input_event in_ev;static int slot = 0;//用于保存上一个slotstatic struct tp_xy xy[12] = {0};//用于保存上一次的x和y坐标值,假设触摸屏支持的最大触摸点数不会超过12int i;memset(mt, 0x0, max_slots * sizeof(struct ts_mt)); //清零for (i = 0; i < max_slots; i )mt[i].id = -2;//将id初始化为-2, id=-1表示触摸点删除, id>=0表示创建for ( ; ; ) {if (sizeof(struct input_event) !=read(fd, &in_ev, sizeof(struct input_event))) {perror("read error");return -1;}switch (in_ev.type) {case EV_ABS:switch (in_ev.code) {case ABS_MT_SLOT:slot = in_ev.value;break;case ABS_MT_POSITION_X:xy[slot].x = in_ev.value;mt[slot].valid = 1;break;case ABS_MT_POSITION_Y:xy[slot].y = in_ev.value;mt[slot].valid = 1;break;case ABS_MT_TRACKING_ID:mt[slot].id = in_ev.value;mt[slot].valid = 1;break;}break;//case EV_KEY://按键事件对单点触摸应用比较有用// break;case EV_SYN:if (SYN_REPORT == in_ev.code) {for (i = 0; i < max_slots; i ) {mt[i].x = xy[i].x;mt[i].y = xy[i].y;}}return 0;}}}int main(int argc, char *argv[]){struct input_absinfo slot;struct ts_mt *mt = NULL;int max_slots;int fd;int i;if (2 != argc) {fprintf(stderr,"usage: %s <input_dev>\n", argv[0]);exit(EXIT_FAILURE);}fd = open(argv[1], O_RDONLY);if (0 > fd) {perror("open error");exit(EXIT_FAILURE);}if (0 > ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &slot)) {perror("ioctl error");close(fd);exit(EXIT_FAILURE);}max_slots = slot.maximum 1 - slot.minimum;printf("max_slots: %d\n", max_slots);mt = calloc(max_slots, sizeof(struct ts_mt));for ( ; ; ) {if (0 > ts_read(fd, max_slots, mt))break;for (i = 0; i < max_slots; i ) {if (mt[i].valid) {//判断每一个触摸点信息是否发生更新(关注的信息发生更新)if (0 <= mt[i].id)printf("slot<%d>, 按下(%d, %d)\n", i, mt[i].x, mt[i].y);else if (-1 == mt[i].id)printf("slot<%d>, 松开\n", i);elseprintf("slot<%d>, 移动(%d, %d)\n", i, mt[i].x, mt[i].y);}}}close(fd);free(mt);exit(EXIT_FAILURE);}

示例代码中申明了struct ts_mt数据结构,用于描述多点触摸情况下每一个触摸点的信息。

首先来看下main()函数,定义了max_slots变量,用于指定触摸屏设备的支持的最大触摸点数,通过:

ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &slot)

获取到触摸屏该信息。

接着根据max_slots变量的值,为mt指针申请内存:

mt = calloc(max_slots, sizeof(struct ts_mt));

for( ; ; )循环中调用ts_read()函数,该函数是自定义函数,用于获取触摸屏上报的数据,第一个参数表示文件描述符fd、第二个参数表示触摸屏支持的最大触摸点数、第三个参数则是struct ts_mt数组,ts_read()函数会将获取到的数据存放在数组中,mt[0]表示slot<0>数据、mt[1]表示slot<1>的数据依次类推!

在内部的for循环中,则对获取到的数据进行分析,判断数据是否有效,并根据id判断手指的动作,在单点触摸应用程序中,我们是通过BTN_TOUCH事件来判断手指的动作;而在多点触摸应用中,我们需要通过id来判断多个手指的动作。

关于自定义函数ts_read()就不再介绍了,代码的注释已经描述很清楚了!

接着编译应用程序,将编译得到的可执行文件拷贝到开发板Linux系统的用户家目录下,执行应用程序,接着可以用多个手指触摸触摸屏、松开、滑动等操作:

正点原子stm32mini开发板入门(正点原子I.MX6U嵌入式Linux)(20)

图 18.5.9 多点触摸应用程序测试结果

每一个不同的slot表示不同的触摸点,譬如slot<0>表示触摸点0、slot<1>表示触摸点1以此类推!

鼠标应用编程

本小节是笔者留给各位读者的一个作业,交给大家去完成,通过本章内容的介绍相信大家可以独立完成,ALPHA/Mini开发板出厂系统支持USB鼠标,直接将一个USB鼠标插入到开发板的USB HOST接口即可,在终端会打印驱动加载信息。

  • 陈丹青骂余秋雨原因(余秋雨显得非常无耻)
  • 2024-06-02余秋雨显得非常无耻在当代文坛,余秋雨也算是占有一席之地的他的"文化散文"过去开启了一代文风,在中国散文史上有着不可磨灭的意义不过,他过于锋芒毕露,这导致他名声不好陈丹青曾在一档节目中炮轰他:"余。
  • 剪刀用英语怎么说(剪刀用英语的写法)
  • 2024-06-02剪刀用英语的写法剪刀[jiǎndāo]的英语是scissors;shears剪刀(jiǎndāo)是切割布、纸、钢板、绳、圆钢等片状或线状物体的双刃工具,两刃交错,可以开合在中国,因纺织业发展,剪子、剪刀业内有区分例。
  • 幸福树的种植方法步骤(幸福树容易长斑吗)
  • 2024-06-02幸福树容易长斑吗合理的浇水幸福树喜欢湿润的环境,但是土壤的水分不可过大,土壤过湿会影响幸福树根部的生长,平时只需要注意土壤湿润松软即可夏季温度高,水分吸收与蒸发较快,需要经常观察土壤水分状况,根据土壤情况及时浇水合理。
  • 极限概念引入的作用(动态定义与静态定义)
  • 2024-06-02动态定义与静态定义本文为“第三届数学文化征文比赛极限定义新讲:动态定义与静态定义作者:李照作品编号:014极限,比如说数列极限,简单讲来说的是“当n越来越大时,数列越来越靠近实数L”,是一种动态过程,而其正式定义,也称。
  • 水肿肥胖如何消除(变胖和水肿哪个更可怕)
  • 2024-06-02变胖和水肿哪个更可怕记得有一期看《向往的生活》,张艺兴因为熬夜做音乐,导致第二天起床双眼皮直接肿成单眼皮,脸也直接大了一圈,整个人水肿得不像样子,被好多网友调侃像“猪头”!好好一张帅气的脸,怎么就会水肿呢?其实,相较于男。
  • 乡村爱情各演员现状 乡村爱情主要演员今昔对比照
  • 2024-06-02乡村爱情各演员现状 乡村爱情主要演员今昔对比照从2005年开始,由赵本山团队制作的《乡村爱情》系列电视剧受到了观众们的喜欢,该系列如今已经拍了八部,十年间,剧情不断变化,剧中主演也有变化长春国贸晒一组主要演员对比照看这些人的变化有多大!看谁变化最。
  • 为什么都说吃水果减肥呢(吃水果可以减肥)
  • 2024-06-02吃水果可以减肥有很多人在减肥的时候都知道自己要控制饮食,尽量多吃一些低脂的食物,因此有一些减肥的人就想通过食用水果来帮助减肥确实,有一些水果能够起到有效减肥的作用,但也要知道在食用水果减肥的时候有一些误区要避免让自。
  • 简短生日搞笑祝福语
  • 2024-06-02简短生日搞笑祝福语五彩缤纷的世界,只有友情最珍贵,在这属于你的日子里,祝你快乐皎洁的月光是我为你点亮的烛光,灿烂的繁星是我送你许愿的流星,无尽的祝福是我送你的生日礼物,请好好收藏在心中,祝你生日快乐,好运势如破竹在这个。
  • 家用家电安全使用年限(家用电器安全使用年限标准发布)
  • 2024-06-02家用电器安全使用年限标准发布中国网科技1月10日讯近日,中国家用电器协会正式发布了《家用电器安全使用年限》系列标准涵盖家用电冰箱、家用电动洗衣机和干衣机、房间空气调节器、家用燃气灶、吸油烟机、储水式电热水器六大品类其中,家用电冰。
  • 绝句的意思是绝句这首诗的意思(绝句的意思及原文)
  • 2024-06-02绝句的意思及原文译文:两只黄鹂在翠绿的柳树间婉转地歌唱,一队整齐的白鹭直冲向蔚蓝的天空我坐在窗前,可以望见西岭上堆积着终年不化的积雪,门前停泊着自万里外的东吴远行而来的船只原文:《绝句》【作者】杜甫【朝代】唐译文对照。