文章目录
一、input子系统
1.input子系统介绍
Input 子系统是管理输入的子系统, 和 pinctrl 和 gpio 子系统一样, 都是 Linux 内核针对某一类设备而创建的框架。
Input子系统是Linux对输入设备提供的统一驱动框架。如按键、键盘、触摸屏和鼠标等输入设备的驱动方式是类似的,当出现按键、触摸等操作时,硬件产生中断,然后CPU直接读取引脚电平,或通过SPI、I2C等通讯方式从设备的寄存器读取具体的按键值或触摸坐标,然后把这些信息提交给内核。
使用Input子系统驱动的输入设备可以通过统一的数据结构提交给内核,该数据结构包括输入的时间、类型、代号以及具体的键值或坐标,而内则通过/dev/input目录下的文件接口传递给用户空间。
2.input子系统框架
3.input事件结构
应用程序空间在从input设备read()读取数据时,它的每个数据元素是struct input_event
结构体类型,该结构体在Linux内核源码中其定义在 include/uapi/linux/input.h
文件中,而应用程序空间则定义在 /usr/include/linux/input.h
文件中。
struct input_event结构体:
struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};
- time:该变量用于记录事件产生的时间戳。表示“自系统启动以来过了多少时间”,由秒和微秒(long 类型 32bit)组成。
- type:输入设备的事件类型。系统常用的默认类型有EV_KEY、 EV_REL和EV_ABS,分别用于表示按键状态改变事件、相对坐标改变事件及绝对坐标改变事件。
- code:事件代号,表示该类事件下的哪一个事件。例如 在EV_KEY事件类型中,code的值常用于表示键盘上具体的按键,比如数字键1、2、3,字母键A、B、C里等。
- value :事件的值。对于EV_KEY事件类型,当按键按下时,该值为1;按键松开时,该值为0。
3.input事件设备名
首先我们可以/dev/input路径下看见有许多event节点,每一个event文件都对应相应的设备,不过event事件的编号与对应的设备并不是固定的,因为编号是系统检测到设备的先后顺序安排的。
查看对应的硬件设备的方法有多种,如下:
(1)查看by-path中的时间编号的对应的硬件设备
ls -l /dev/input/by-path/
(2)通过/sys/class/input目录查看
上一个方法目录下的文件实际上都是链接,/dev下的设备都是通过/sys导出的,所以也可以通过/sys/class/input目录查看。
cat /sys/class/input/event1/device/name
(3)通过/proc/bus/input/devices文件查看
cat /proc/bus/input/devices
其中:
- I:id of the device 设备ID
- N:name of the device 设备名称
- P:physical path to the device in the system hierarchy 系统层次结构中设备的物理路径
- S:sysfs path 位于sys文件系统的路径
- U:unique identification code for the device 设备的唯一标识码
- H:list of input handles associated with the device 与设备关联的输入句柄列表
- B:bitmaps 位图
(4)使用evtest工具查看事件编号对应的具体的硬件设备
在开发input子系统驱动时,常常会使用 evtest 工具进行测试,它列出了系统当前可用的事件文件,并且列出了这些事件对应的设备名。
二、按键测试
1.测试程序源代码
keypad.c
/*********************************************************************************
* Copyright: (C) 2023 Deng Yonghao<dengyonghao2001@163.com>
* All rights reserved.
*
* Filename: keypad.c
* Description: This file keypad test process
*
* Version: 1.0.0(2023年03月16日)
* Author: Deng Yonghao <dengyonghao2001@163.com>
* ChangeLog: 1, Release initial version on "2023年03月16日 10时00分36秒"
*
********************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <fcntl.h>
#include <libgen.h>
#include <getopt.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <linux/input.h>
#include <linux/kd.h>
#include <linux/keyboard.h>
#if 0
struct input_event
{
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
}
#endif
#define EV_RELEASED 0
#define EV_PRESSED 1
#define BUTTON_CNT 10
/*单个C文件并没有对应.h文件,则直接在C文件中声明函数*/
void usage(char *name);
void display_button_event(struct input_event *ev, int cnt);
int main(int argc, char **argv)
{
char *kbd_dev = "/dev/input/event1"; //可知默认监听按键的设备为event2
char kbd_name[256] = "Unknown"; //用于保存获取到的设备名称
int kbd_fd = -1; //open()打开文件的文件描述符
int rv = 0; //函数的返回值,默认为0
int opt; //getopt_long解析命令行参数返回值
int size = sizeof(struct input_event);
fd_set rds;//用于监听的事件的集合
struct input_event ev[BUTTON_CNT];
/* getopt_long参数函数第四个参数的定义,二维数组,每个成员由四个元素组成 */
/* { 参数名称,是否带参数,flags指针(NULL时将val的数值从getopt_long的返回值返回出去),
函数找到该选项时的返回值(字符)}*/
struct option long_options[] = {
{"device", required_argument, NULL, 'd'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
/*获取命令行参数的解析返回值*/
while((opt = getopt_long(argc, argv, "d:h", long_options, NULL)) != -1)
{
switch(opt)
{
case 'd':
kbd_dev = optarg;
break;
case 'h':
usage(argv[0]);
return 0;
default:
break;
}
}
if(NULL == kbd_dev)
{
/*命令行argv[0]是输入的命令,如 ./keypad*/
usage(argv[0]);
return -1;
}
/* 获取uid 建议以root权限运行确保可以正常运行 */
if ((getuid ()) != 0)
{
printf ("You are not root! This may not work...\n");
}
/* 打开按键对应的设备节点,如果错误则返回负数 */
if ((kbd_fd = open(kbd_dev, O_RDONLY)) < 0)
{
printf("Open %s failure: %s", kbd_dev, strerror(errno));
return -1;
}
/* 使用ioctl获取 /dev/input/event*对应的设备名字 */
ioctl (kbd_fd, EVIOCGNAME (sizeof (kbd_name)), kbd_name);
printf ("Monitor input device %s (%s) event on poll mode:\n", kbd_dev, kbd_name);
/*用select监听按键事件*/
while(1)
{
FD_ZERO(&rds);//清空select()的读事件集合
FD_SET(kbd_fd, &rds);//将按键设备的文件描述符加入到读事件集合中
rv = select(kbd_fd + 1, &rds, NULL, NULL, NULL);
if(rv < 0)
{
printf("Select() system call failure: %s\n", strerror(errno));
goto CleanUp;
}
else if (FD_ISSET(kbd_fd, &rds))//按键发生了事件时
{
/*read读取input设备的数据包,数据包为input_event结构体类型*/
if((rv = read(kbd_fd, ev, size*BUTTON_CNT))<size)
{
printf("Reading data from kbd_fd failure: %s\n", strerror(errno));
break;
}
else
{
display_button_event(ev, rv/size);
}
}
}
CleanUp:
close(kbd_fd);
return 0;
}
/*用于打印程序的提示信息*/
void usage(char *name)
{
char *progname = NULL;
char *ptr = NULL;
ptr = strdup(name);//字符串拷贝函数,该函数内部将调用malloc()来动态分配内存,然后将$name字符串内容拷贝到malloc分配的内存中,这样使用完之后需要free释放内存.
progname = basename(ptr);//去除该可执行文件的路径名,获取其自身名称(即keypad)
printf("Usage: %s [-p] -d <device>\n", progname);
printf(" -d[device ] button device name\n");
printf(" -p[poll ] Use poll mode, or default use infinit loop.\n");
printf(" -h[help ] Display this help information\n");
free(ptr);//释放内存
return;
}
/*display_button_event函数来解析按键设备上报的数据,并答应按键按下的相关信息*/
void display_button_event(struct input_event *ev, int cnt)
{
int i;
static struct timeval pressed_time;//该变量用来存放按键按下的时间,是静态变量
struct timeval duration_time;//该变量用来存放按键按下持续时间
for(i=0; i<cnt; i++)
{
if(EV_KEY==ev[i].type && EV_PRESSED==ev[i].value)
{
pressed_time = ev[i].time;
printf("Keypad[%d] pressed time: %ld.%ld\n",
ev[i].code, pressed_time.tv_sec, pressed_time.tv_usec);
}
if(EV_KEY==ev[i].type && EV_RELEASED==ev[i].value)
{
timersub(&ev[i].time, &pressed_time, &duration_time);//时间差函数,求第一个参数与第二个参数的差,结果放入第三个参数
printf("keypad[%d] released time: %ld.%ld\n",
ev[i].code, ev[i].time.tv_sec, ev[i].time.tv_usec);
printf("keypad[%d] duration time: %ld.%ld\n",
ev[i].code, duration_time.tv_sec, duration_time.tv_usec);
}
}
}
2.Makefile
CC=arm-linux-gnueabihf-gcc
APP_NAME=keypad
all:clean
@${CC} ${APP_NAME}.c -o ${APP_NAME}
clean:
@rm -f ${APP_NAME}
3.运行测试
成功得到了按下与放开的时间,并计算出了按下按键的持续时间,可见我每一次按下按键都增加了按键按下的持续时间,运行测试成功。