1.3.4 查询方式
APP调用open函数时,传入“O_NONBLOCK”表示“非阻塞”。
APP调用read函数读取数据时,如果驱动程序中有数据,那么APP的read函数会返回数据,否则也会立刻返回错误。
1.3.5 休眠-唤醒方式
APP调用open函数时,不要传入“O_NONBLOCK”。
APP调用read函数读取数据时,如果驱动程序中有数据,那么APP的read函数会返回数据;否则APP就会在内核态休眠,当有数据时驱动程序会把APP唤醒,read函数恢复执行并返回数据给APP。
//打开设备节点ioctl #include <linux/input> /*./01_get_input_info /dev/input/event0 noblock */ #include <sys/types.h> #include <fcntl.h> #include <sys/stat.h> int main(int argc, char **argv) { int fd; int len; unsigned int evbit[2]; char ev_names={ "EV_SYN", "EV_KEY", "EV_REL", "EV_ABS", "EV_MSC", "EV_SW", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL", "EV_LED", "EV_SND", "EV_REP", "EV_FF", "EV_PWR", }; int bit; if(argc<2) { printf("Usage:%s<dev> [noblock]\n,argv[0]");//<>是必须有的 []是可以省略的 return -1; } if(argc==3 && strcmp(argv[2],"noblock")) { fd=open(argv[1],O_RDWR|O_NONBLOCK); } else { fd=open(argv[1],O_RDWR); } //fd=open(argv[1],O_RDWR); if(fd<0) { printf("open %s err\n",argv[1]); return -1; } err = ioctl(fd,EVIOCGID,&id); if(err=0) { printf("bustype = 0x%x\n",id.bustype); printf("vendor = 0x%x\n",id.vendor); printf("product = 0x%x\n",id.product); printf("version = 0x%x\n",id.version); } len =ioctl(fd,EVIOCGBIT(0,sizeof(evbit),&evbit)); if(len > 0&&len <= sizeof(evbit)) { for(i=0;i<len;i++) { byte=((unsigned char *)evbit)[i]; for(bit=0;bit<8;bit++) { if(byte & (1<<bit)){ printf("%s",ev_names[i*8+bit]); } } } printf("support ev_type:"); } while(1) { len=read(fd,&event,sizeof(event)); if(len==sizeof(event)) { printf("get event:type=0x%x,code=0x%x,value=0x%x\n",event.type,event.code,event.value); } else { printf("read err %d\n",len); } } return 0 }
1.3.6 POLL/SELECT方式
1. 功能介绍
POLL机制、SELECT机制是完全一样的,只是APP接口函数不一样。
简单地说,它们就是“定个闹钟”:在调用poll、select函数时可以传入“超时时间”。在这段时间内,条件合适时(比如有数据可读、有空间可写)就会立刻返回,否则等到“超时时间”结束时返回错误。
用法如下。
APP先调用open函数时。
APP不是直接调用read函数,而是先调用poll或select函数,这2个函数中可以传入“超时时间”。它们的作用是:如果驱动程序中有数据,则立刻返回;否则就休眠。在休眠期间,如果有人操作了硬件,驱动程序获得数据后就会把APP唤醒,导致poll或select立刻返回;如果在“超时时间”内无人操作硬件,则时间到后poll或select函数也会返回。APP可以根据函数的返回值判断返回原因:有数据?无数据超时返回?
APP根据poll或select的返回值判断有数据之后,就调用read函数读取数据时,这时就会立刻获得数据。
poll/select函数可以监测多个文件,可以监测多种事件:
在调用poll函数时,要指明:
① 你要监测哪一个文件:哪一个fd
② 你想监测这个文件的哪种事件:是POLLIN、还是POLLOUT
最后,在poll函数返回时,要判断状态。
应用程序代码如下:
struct pollfd fds[1]; int timeout_ms = 5000; int ret; fds[0].fd = fd; fds[0].events = POLLIN; ret = poll(fds, 1, timeout_ms); if ((ret == 1) && (fds[0].revents & POLLIN)) { read(fd, &val, 4); printf("get button : 0x%x\n", val); }
1.3.7 异步通知方式
功能介绍
所谓同步,就是“你慢我等你”。
那么异步就是:你慢那你就自己玩,我做自己的事去了,有情况再通知我。
所谓异步通知,就是APP可以忙自己的事,当驱动程序用数据时它会主动给APP发信号,这会导致APP执行信号处理函数。
仔细想想“发信号”,这只有3个字,却可以引发很多问题:
① 谁发:驱动程序发
② 发什么:信号
③ 发什么信号:SIGIO
④ 怎么发:内核里提供有函数
⑤ 发给谁:APP,APP要把自己告诉驱动
⑥ APP收到后做什么:执行信号处理函数
⑦ 信号处理函数和信号,之间怎么挂钩:APP注册信号处理函数
小孩通知妈妈的事情有很多:饿了、渴了、想找人玩。
Linux系统中也有很多信号,在Linux内核源文件include\uapi\asm-generic\signal.h中,有很多信号的宏定义:
驱动程序通知APP时,它会发出“SIGIO”这个信号,表示有“IO事件”要处理。
就APP而言,你想处理SIGIO信息,那么需要提供信号处理函数,并且要跟SIGIO挂钩。这可以通过一个signal函数来“给某个信号注册处理函数”,用法如下:
除了注册SIGIO的处理函数,APP还要做什么事?想想这几个问题:
① 内核里有那么多驱动,你想让哪一个驱动给你发SIGIO信号?
APP要打开驱动程序的设备节点。
② 驱动程序怎么知道要发信号给你而不是别人?
APP要把自己的进程ID告诉驱动程序。
③ APP有时候想收到信号,有时候又不想收到信号:
应该可以把APP的意愿告诉驱动:设置Flag里面的FASYNC位为1,使能“异步通知”。
应用编程
应用程序要做的事情有这几件:
① 编写信号处理函数:
static void sig_func(int sig) { int val; read(fd, &val, 4); printf("get button : 0x%x\n", val); }
② 注册信号处理函数:
signal(SIGIO, sig_func);
③ 打开驱动:
fd = open(argv[1], O_RDWR);
④ 把进程ID告诉驱动:
fcntl(fd, F_SETOWN, getpid());
⑤ 使能驱动的FASYNC功能:
flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | FASYNC);
1.4 电阻屏和电容屏
触摸屏分为电阻屏、电容屏。电阻屏结构简单,在以前很流行;电容屏支持多点触摸,现在的手机基本都是使用电容屏。
注意:LCD、触摸屏不是一回事,LCD是输出设备,触摸屏是输入设备。制作触摸屏时特意把它的尺寸做得跟LCD一模一样,并且把触摸屏覆盖在LCD上。
1.4.1 电阻屏
1. 复习一下欧姆定律
上图中的电阻假设是均匀的,就是长度和阻值成正比关系。电阻长度为L,阻值为R,在两端施加3.3V电压。在某点测得电压为V,求上图中长度X。
根据欧姆定律:3.3/R = V/Rx,
因为长度和阻值成正比关系,上述公式转换为:3.3∕L = V/X,所以X=LV/3.3。
2. 电阻屏原理
电阻屏就是基于欧姆定律制作的,它有上下两层薄膜,这两层薄膜就是两个电阻,如下图所示:
平时上下两层薄膜无触触,当点击触摸屏时,上下两层薄膜接触:这时就可以测量触点电压。过程如下:
① 测量X坐标:
在xp、xm两端施加3.3V电压,yp和ym不施加电压(yp就相当于探针),测量yp电压值。该电压值就跟X坐标成正比关系,假设:
② 测量Y坐标:
在yp、ym两端施加3.3V电压,xp和xm不施加电压(xp就相当于探针),测量xp电压值。该电压值就跟Y坐标成正比关系,假设:
在实际使用时,电阻屏的Xmax、Ymax无从得知,所以使用之前要先较准:依次点击触摸屏的四个角和中心点,推算出X、Y坐标的公式:
3. 电阻屏数据
Linux驱动程序中,会上报触点的X、Y数据,注意:这不是LCD的坐标值,需要APP再次处理才能转换为LCD坐标值。
对应的input_event结构体中,“type、code、value”如下:
按下时: EV_KEY BTN_TOUCH 1 /* 按下 */ EV_ABS ABS_PRESSURE 1 /* 压力值,可以上报,也可以不报,可以是其他压力值 */ EV_ABS ABS_X x_value /* X坐标 */ EV_ABS ABS_Y y_value /* Y坐标 */ EV_SYNC 0 0 /* 同步事件 */ 松开时: EV_KEY BTN_TOUCH 0 /* 松开 */ EV_ABS ABS_PRESSURE 0 /* 压力值,可以上报,也可以不报 */ EV_SYNC 0 0 /* 同步事件 */
1.4.2 电容屏
1. 原理
原理如下图所示:
电容屏中有一个控制芯片,它会周期性产生驱动信号,接收电极接收到信号,并可测量电荷大小。当电容屏被按下时,相当于引入了新的电容,从而影响了接收电极接收到的电荷大小。主控芯片根据电荷大小即可计算出触点位置。
怎么通过电荷计算出触点位置?这由控制芯片实现,这类芯片一般是I2C接口。
我们只需要编写程序,通过I2C读取芯片寄存器即可得到这些数据。
2. 电容屏数据
参考文档:Linux内核Documentation\input\multi-touch-protocol.rst。
电容屏可以支持多点触摸(Multi touch),驱动程序上报的数据中怎么分辨触点?
这有两种方法:Type A、Type B,这也对应两种类型的触摸屏:
① Type A
该类型的触摸屏不能分辨是哪一个触点,它只是把所有触点的坐标一股脑地上报,由软件来分辨这些数据分别属于哪一个触点。
Type A已经过时,Linux内核中都没有Type A的源码了。
② Type B
该类型的触摸屏能分辨是哪一个触点,上报数据时会先上报触点ID,再上报它的数据。
具体例子如下,这是最简单的示例,使用场景分析来看看它上报的数据。
当有2个触点时(type, code, value):
EV_ABS ABS_MT_SLOT 0 // 这表示“我要上报一个触点信息了”,用来分隔触点信息 EV_ABS ABS_MT_TRACKING_ID 45 // 这个触点的ID是45 EV_ABS ABS_MT_POSITION_X x[0] // 触点X坐标 EV_ABS ABS_MT_POSITION_Y y[0] // 触点Y坐标 EV_ABS ABS_MT_SLOT 1 // 这表示“我要上报一个触点信息了”,用来分隔触点信息 EV_ABS ABS_MT_TRACKING_ID 46 // 这个触点的ID是46 EV_ABS ABS_MT_POSITION_X x[1] // 触点X坐标 EV_ABS ABS_MT_POSITION_Y y[1] // 触点Y坐标 EV_SYNC SYN_REPORT 0 // 全部数据上报完毕
当ID为45的触点正在移动时:
EV_ABS ABS_MT_SLOT 0 // 这表示“我要上报一个触点信息了”,之前上报过ID,就不用再上报ID了 EV_ABS ABS_MT_POSITION_X x[0] // 触点X坐标 EV_SYNC SYN_REPORT 0 // 全部数据上报完毕
松开ID为45的触点时(在前面slot已经被设置为0,这里这需要再重新设置slot,slot就像一个全局变量一样:如果它没变化的话,就无需再次设置):
// 刚刚设置了ABS_MT_SLOT为0,它对应ID为45,这里设置ID为-1就表示ID为45的触点被松开了 EV_ABS ABS_MT_TRACKING_ID -1 EV_SYNC SYN_REPORT 0 // 全部数据上报完毕
最后,松开ID为46的触点:
EV_ABS ABS_MT_SLOT 1 // 这表示“我要上报一个触点信息了”,在前面设置过slot 1的ID为46 EV_ABS ABS_MT_TRACKING_ID -1 // ID为-1,表示slot 1被松开,即ID为46的触点被松开 EV_SYNC SYN_REPORT // 全部数据上报完毕