【正点原子Linux连载】第二十二章 Linux INPUT子系统实验摘自【正点原子】ATK-DLRK3568嵌入式Linux驱动开发指南

news2024/11/17 22:16:39

1)实验平台:正点原子ATK-DLRK3568开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=731866264428
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban

第二十二章 Linux INPUT子系统实验

按键、鼠标、键盘、触摸屏等都属于输入(input)设备,Linux内核为此专门做了一个叫做input子系统的框架来处理输入事件。输入设备本质上还是字符设备,只是在此基础上套上了input框架,用户只需要负责上报输入事件,比如按键值、坐标等信息,input核心层负责处理这些事件。本章我们就来学习一下Linux内核中的input子系统。

22.1 input子系统
22.1.1 input子系统简介
input就是输入的意思,因此input子系统就是管理输入的子系统,和pinctrl、gpio子系统一样,都是Linux内核针对某一类设备而创建的框架。比如按键输入、键盘、鼠标、触摸屏等等这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息,因此在应用层的处理就不同,对于驱动编写者而言不需要去关心应用层的事情,我们只需要按照要求上报这些输入事件即可。为此input子系统分为input驱动层、input核心层、input事件处理层,最终给用户空间提供可访问的设备节点,input子系统框架如图22.1.1.1所示:
在这里插入图片描述

图22.1.1.1 input子系统结构图
图22.1.1.1中左边就是最底层的具体设备,比如按键、USB键盘/鼠标等,中间部分属于Linux内核空间,分为驱动层、核心层和事件层,最右边的就是用户空间,所有的输入设备以文件的形式供用户应用程序使用。可以看出input子系统用到了我们前面讲解的驱动分层模型,我们编写驱动程序的时候只需要关注中间的驱动层、核心层和事件层,这三个层的分工如下:
驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。
核心层:承上启下,为驱动层提供输入设备 注册和操作接口。通知事件层对输入事件进行处理。
事件层:主要和用户空间进行交互。
22.1.2 input驱动编写流程
input核心层会向Linux内核注册一个字符设备,大家找到drivers/input/input.c这个文件,input.c就是input输入子系统的核心层,此文件里面有如下所示代码:
示例代码22.1.2.1 input核心层创建字符设备过程

1  struct class input_class = {
2   	.name       	= "input",
3   	.devnode    	= input_devnode,
4  };
5  ......
6  static int __init input_init(void)
7  {
8   	int err;
9  
10  	err = class_register(&input_class);
11  	if (err) {
12     		pr_err("unable to register input_dev class\n");
13      	return err;
14  	}
15 
16  	err = input_proc_init();
17  	if (err)
18      	goto fail1;
19 
20  	err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0),
21                   INPUT_MAX_CHAR_DEVICES, "input");
22  	if (err) {
23      	pr_err("unable to register char major %d", INPUT_MAJOR);
24      	goto fail2;
25  	}
26 
27  	return 0;
28
29  fail2:  input_proc_exit();
30  fail1:  class_unregister(&input_class);
31  return err;
32 }

第10行,注册一个input类,这样系统启动以后就会在/sys/class目录下有一个input子目录,如图22.1.2.1所示:
在这里插入图片描述

图22.1.2.1 input类
第20~21行,注册一个字符设备,主设备号为INPUT_MAJOR,INPUT_MAJOR定义在include/uapi/linux/major.h文件中,定义如下:
#define INPUT_MAJOR 13
因此,input子系统的所有设备主设备号都为13,我们在使用input子系统处理输入设备的时候就不需要去注册字符设备了,我们只需要向系统注册一个input_device即可。
1、注册input_dev
在使用input子系统的时候我们只需要注册一个input设备即可,input_dev结构体表示input设备,此结构体定义在include/linux/input.h文件中,定义如下(有省略):
示例代码 22.1.2.2 input_dev 结构体

1  struct input_dev {
2   const char *name;
3   const char *phys;
4   const char *uniq;
5   struct input_id id;
6  
7   unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
8  
9   unsigned long evbit[BITS_TO_LONGS(EV_CNT)];	/* 事件类型的位图 	*/
10  unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];	/* 按键值的位图 		*/
11  unsigned long relbit[BITS_TO_LONGS(REL_CNT)];	/* 相对坐标的位图	*/
12  unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];	/* 绝对坐标的位图 	*/
13  unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];	/* 杂项事件的位图 	*/
14  unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];	/*LED 相关的位图 	*/
15  unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];	/* sound 有关的位	*/
16  unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];	/* 压力反馈的位图 	*/
17  unsigned long swbit[BITS_TO_LONGS(SW_CNT)];	/*开关状态的位图 	*/ 
......
69  bool devres_managed;
70 };

第9行,evbit表示输入事件类型,可选的事件类型定义在include/uapi/linux/input.h文件中,事件类型如下:
示例代码22.1.2.3 事件类型

1  #define EV_SYN          	0x00    /* 同步事件 		*/
2  #define EV_KEY          	0x01    /* 按键事件 		*/
3  #define EV_REL          	0x02    /* 相对坐标事件 	*/
4  #define EV_ABS          	0x03    /* 绝对坐标事件 	*/
5  #define EV_MSC          	0x04    /* 杂项(其他)事件 	*/
6  #define EV_SW           	0x05    /* 开关事件 		*/
7  #define EV_LED          	0x11    /* LED 			*/
8  #define EV_SND          	0x12    /* sound(声音) 	*/
9  #define EV_REP          	0x14    /* 重复事件 		*/
10 #define EV_FF           	0x15    /* 压力事件 		*/
11 #define EV_PWR          	0x16    /* 电源事件 		*/
12 #define EV_FF_STATUS    	0x17    /* 压力状态事件 	*/
比如本章我们要使用到按键,那么就需要注册EV_KEY事件,如果要使用连按功能的话还需要注册EV_REP事件。
继续回到示例代码22.1.2.2中,第9行~17行的evbit、keybit、relbit等等都是存放不同事件对应的值。比如我们本章要使用按键事件,因此要用到keybit,keybit就是按键事件使用的位图,Linux内核定义了很多按键值,这些按键值定义在include/uapi/linux/input-event-codes.h文件中,按键值如下:

示例代码 22.1.2.4 按键值

1   #define KEY_RESERVED     	0
2   #define KEY_ESC         	1
3   #define KEY_1           	2
4   #define KEY_2           	3
5   #define KEY_3           	4
6   #define KEY_4           	5
7   #define KEY_5           	6
8   #define KEY_6           	7
9   #define KEY_7           	8
10  #define KEY_8           	9
11  #define KEY_9           	10
12  #define KEY_0           	11
.....

我们可以将开发板上的按键值设置为示例代码22.1.2.4中的任意一个,本章我们依旧使用《第十三章 Linux按键输入实验》里面的GPIO3_C5引脚来模拟按键,然后将其按键值设置为KEY_0。
在编写input设备驱动的时候我们需要先申请一个input_dev结构体变量,使用input_allocate_device函数来申请一个input_dev,此函数原型如下所示:
struct input_dev *input_allocate_device(void)
函数参数和返回值含义如下:
参数:无。
返回值:申请到的input_dev。
如果要注销input设备的话需要使用input_free_device函数来释放掉前面申请到的input_dev,input_free_device函数原型如下:
void input_free_device(struct input_dev *dev)
函数参数和返回值含义如下:
dev:需要释放的input_dev。
返回值:无。
申请好一个input_dev以后就需要初始化这个input_dev,需要初始化的内容主要为事件类型(evbit)和事件值(keybit)这两种。input_dev初始化完成以后就需要向Linux内核注册input_dev了,需要用到input_register_device函数,此函数原型如下:
int input_register_device(struct input_dev *dev)
函数参数和返回值含义如下:
dev:要注册的input_dev 。
返回值:0,input_dev注册成功;负值,input_dev注册失败。
同样的,注销input驱动的时候也需要使用input_unregister_device函数来注销掉前面注册的input_dev,input_unregister_device函数原型如下:
void input_unregister_device(struct input_dev *dev)
函数参数和返回值含义如下:
dev:要注销的input_dev 。
返回值:无。
综上所述,input_dev注册过程如下:
①、使用input_allocate_device函数申请一个input_dev。
②、初始化input_dev的事件类型以及事件值。
③、使用input_register_device函数向Linux系统注册前面初始化好的input_dev。
④、卸载input驱动的时候需要先使用input_unregister_device函数注销掉注册的input_dev,然后使用input_free_device函数释放掉前面申请的input_dev。input_dev注册过程示例代码如下所示:
示例代码22.1.2.5 input_dev注册流程

1  struct input_dev *inputdev;      /* input结构体变量 */
2  
3  /* 驱动入口函数 */
4  static int __init xxx_init(void)
5  {
6   	......
7   	inputdev = input_allocate_device(); 	/* 申请input_dev 		*/
8   	inputdev->name = "test_inputdev";  		/* 设置input_dev名字 	*/
9  
10  	/*********第一种设置事件和事件值的方法***********/
11  	__set_bit(EV_KEY, inputdev->evbit); 	/* 设置产生按键事件 		*/
12  	__set_bit(EV_REP, inputdev->evbit); 	/* 重复事件  			*/
13  	__set_bit(KEY_0, inputdev->keybit); 	/*设置产生哪些按键值 	*/
14  	/************************************************/
15  
16  	/*********第二种设置事件和事件值的方法***********/
17  	keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | 
BIT_MASK(EV_REP);
18  	keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |= 
BIT_MASK(KEY_0);
19  	/************************************************/
20 
21  	/*********第三种设置事件和事件值的方法***********/
22  	keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | 
BIT_MASK(EV_REP);
23  	input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
24  	/************************************************/
25  
26  	/* 注册input_dev */
27  	input_register_device(inputdev);
28  	......
29  	return 0;
30 }
31 
32 /* 驱动出口函数 */
33 static void __exit xxx_exit(void)
34 {
35  	input_unregister_device(inputdev);  	/* 注销input_dev 	*/
36  	input_free_device(inputdev);        		/* 删除input_dev 	*/
37 }

第1行,定义一个input_dev结构体指针变量。
第430行,驱动入口函数,在此函数中完成input_dev的申请、设置、注册等工作。第7行调用input_allocate_device函数申请一个input_dev。第1023行都是设置input设备事件和按键值,这里用了三种方法来设置事件和按键值。第27行调用input_register_device函数向Linux内核注册inputdev。
第33~37行,驱动出口函数,第35行调用input_unregister_device函数注销前面注册的input_dev,第36行调用input_free_device函数删除前面申请的input_dev。
2、上报输入事件
当我们向Linux内核注册好input_dev以后还不能高枕无忧的使用input设备,input设备都是具有输入功能的,但是具体是什么样的输入值Linux内核是不知道的,我们需要获取到具体的输入值,或者说是输入事件,然后将输入事件上报给Linux内核。比如按键,我们需要在按键中断处理函数,或者消抖定时器中断函数中将按键值上报给Linux内核,这样Linux内核才能获取到正确的输入值。不同的事件,其上报事件的API函数不同,我们依次来看一下一些常用的事件上报API函数。
首先是input_event函数,此函数用于上报指定的事件以及对应的值,函数原型如下:

void input_event(struct input_dev 	*dev, 
		  unsigned int 			type, 
		  unsigned int 			code, 
		  int 					value)
函数参数和返回值含义如下:
	dev:需要上报的input_dev。
	type: 上报的事件类型,比如EV_KEY。
	code:事件码,也就是我们注册的按键值,比如KEY_0、KEY_1等等。
	value:事件值,比如1表示按键按下,0表示按键松开。
	返回值:无。
	input_event函数可以上报所有的事件类型和事件值,Linux内核也提供了其他的针对具体事件的上报函数,这些函数其实都用到了input_event函数。比如上报按键所使用的input_report_key函数,此函数内容如下:
例代码22.1.2.6 input_report_key函数
static inline void input_report_key(struct input_dev *dev, 
unsigned int code, int value)
{
    input_event(dev, EV_KEY, code, !!value);
}
	从示例代码22.1.2.6可以看出,input_report_key函数的本质就是input_event函数,如果要上报按键事件的话还是建议大家使用input_report_key函数。
	同样的还有一些其他的事件上报函数,这些函数如下所示:
void input_report_rel(struct input_dev *dev, unsigned int code, int value)
void input_report_abs(struct input_dev *dev, unsigned int code, int value)
void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
void input_report_switch(struct input_dev *dev, unsigned int code, int value)
void input_mt_sync(struct input_dev *dev)

当我们上报事件以后还需要使用input_sync函数来告诉Linux内核input子系统上报结束,input_sync函数本质是上报一个同步事件,此函数原型如下所示:
void input_sync(struct input_dev *dev)
函数参数和返回值含义如下:
dev:需要上报同步事件的input_dev。
返回值:无。
综上所述,按键的上报事件的参考代码如下所示:
示例代码22.1.2.7 事件上报参考代码

1  /* 用于按键消抖的定时器服务函数 */
2  void timer_function(unsigned long arg)
3  {
4   	unsigned char value;
5   
6   	value = gpio_get_value(keydesc->gpio);  	/* 读取IO值 	*/
7   	if(value == 0){                         		/* 按下按键 		*/
8       		/* 上报按键值 */
9       		input_report_key(inputdev, KEY_0, 1);	/* 最后一个参数1,按下 */
10      	input_sync(inputdev);               		/*  同步事件 	*/
11  	} else {                                			/* 按键松开 		*/
12      	input_report_key(inputdev, KEY_0, 0);	/* 最后一个参数0,松开 */
13      	input_sync(inputdev);               		/*  同步事件 	*/
14  	}   
15 }
第6行,获取按键值,判断按键是否按下。

第9~10行,如果按键值为0那么表示按键被按下了,如果按键按下的话就要使用input_report_key函数向Linux系统上报按键值,比如向Linux系统通知KEY_0这个按键按下了。
第12~13行,如果按键值为1的话就表示按键没有按下,是松开的。向Linux系统通知KEY_0这个按键没有按下或松开了。
22.1.3 input_event结构体
Linux内核使用input_event这个结构体来表示所有的输入事件,input_event结构体定义在include/uapi/linux/input.h文件中,结构体内容如下:
示例代码 22.1.3.1 input_event 结构体

1 struct input_event {
2   struct timeval time;
3   __u16 type;
4   __u16 code;
5   __s32 value;
6 };
我们依次来看一下input_event结构体中的各个成员变量:
time:时间,也就是此事件发生的时间,为timeval结构体类型,timeval结构体定义如下:

示例代码22.1.3.2 timeval结构体

1 typedef long         	 	__kernel_long_t;
2 typedef __kernel_long_t   	__kernel_time_t;
3 typedef __kernel_long_t  	__kernel_suseconds_t;
4 
5 struct timeval {
6   	__kernel_time_t     		tv_sec;   	/* 秒	*/
7   	__kernel_suseconds_t    	tv_usec;  	/* 微秒 	*/
8 };

从示例代码22.1.3.2可以看出,tv_sec和tv_usec这两个成员变量都为long类型,也就是32位,这个一定要记住,后面我们分析event事件上报数据的时候要用到。
type:事件类型,比如EV_KEY,表示此次事件为按键事件,此成员变量为16位。
code:事件码,比如在EV_KEY事件中code就表示具体的按键码,如:KEY_0、KEY_1等等这些按键。此成员变量为16位。
value:值,比如EV_KEY事件中value就是按键值,表示按键有没有被按下,如果为1的话说明按键按下,如果为0的话说明按键没有被按下或者按键松开了。
input_event这个结构体非常重要,因为所有的输入设备最终都是按照input_event结构体呈现给用户的,用户应用程序可以通过input_event来获取到具体的输入事件或相关的值,比如按键值等。关于input子系统就讲解到这里,接下来我们就以开发板上的GPIO3_C5这个引脚为例,讲解一下如何编写input驱动。
22.2 硬件原理图分析
本章实验硬件原理图参考13.2小节即可。本章我们依旧使用《第十三章 Linux按键输入实验》里面的GPIO3_C5引脚来模拟按键。
22.3 实验程序编写
本实验对应的例程路径为:开发板光盘01、程序源码3、Linux驱动例程19_input。
22.3.1 修改设备树文件
直接使用15.3.1小节中创建的key节点即可。
22.3.2 按键input驱动程序编写
新建名为“19_input”的文件夹,然后在19_input文件夹里面创建vscode工程,工作区命名为“keyinput”。工程创建好以后新建keyinput.c文件,在keyinput.c里面输入如下内容:
示例代码 22.3.2.1 keyinput.c 文件代码段

1   #include <linux/module.h>
2   #include <linux/errno.h>
3   #include <linux/of.h>
4   #include <linux/platform_device.h>
5   #include <linux/of_gpio.h>
6   #include <linux/input.h>
7   #include <linux/timer.h>
8   #include <linux/of_irq.h>
9   #include <linux/interrupt.h>
10  
11  #define KEYINPUT_NAME       "keyinput"  /* 名字       */
12  
13  /* key设备结构体 */
14  struct key_dev{
15      struct input_dev *idev; 	/* 按键对应的input_dev指针 	*/
16      struct timer_list timer;	/* 消抖定时器 */
17      int gpio_key;            	/* 按键对应的GPIO编号 			*/
18      int irq_key;             	/* 按键对应的中断号 				*/
19  };
20  
21  static struct key_dev key;	/* 按键设备 	*/
22  
23  /*
24   * @description  	: 按键中断服务函数
25   * @param – irq  	: 触发该中断事件对应的中断号
26   * @param – arg 	: arg参数可以在申请中断的时候进行配置
27   * @return        	: 中断执行结果
28   */
29  static irqreturn_t key_interrupt(int irq, void *dev_id)
30  {
31      if(key.irq_key != irq)
32          return IRQ_NONE;
33      
34      /* 按键防抖处理,开启定时器延时15ms */
35      disable_irq_nosync(irq); /* 禁止按键中断 */
36      mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));
37      
38      return IRQ_HANDLED;
39  }
40  
41  /*
42   * @description   	: 按键初始化函数
43   * @param – nd       	: device_node设备指针
44   * @return            	: 成功返回0,失败返回负数
45   */
46  static int key_gpio_init(struct device_node *nd)
47  {
48      int ret;
49      unsigned long irq_flags;
50      
51      /* 从设备树中获取GPIO */
52      key.gpio_key = of_get_named_gpio(nd, "key-gpio", 0);
53      if(!gpio_is_valid(key.gpio_key)) {
54          printk("key:Failed to get key-gpio\n");
55          return -EINVAL;
56      }
57      
58      /* 申请使用GPIO */
59      ret = gpio_request(key.gpio_key, "KEY0");
60      if (ret) {
61          printk(KERN_ERR "key: Failed to request key-gpio\n");
62          return ret;
63      }   
64      
65      /* 将GPIO设置为输入模式 */
66      gpio_direction_input(key.gpio_key);
67      
68      /* 获取GPIO对应的中断号 */
69      key.irq_key = irq_of_parse_and_map(nd, 0);
70      if(!key.irq_key){
71          return -EINVAL;
72      }
73  
74      /* 获取设备树中指定的中断触发类型 */
75      irq_flags = irq_get_trigger_type(key.irq_key);
76      if (IRQF_TRIGGER_NONE == irq_flags)
77          irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
78          
79      /* 申请中断 */
80      ret = request_irq(key.irq_key, key_interrupt, irq_flags, 
"Key0_IRQ", NULL);
81      if (ret) {
82          gpio_free(key.gpio_key);
83          return ret;
84      }
85  
86      return 0;
87  }
88  
89  /*
90   * @description 	: 定时器服务函数,用于按键消抖,定时时间到了以后
91   *                    再读取按键值,根据按键的状态上报相应的事件
92   * @param – arg  	: arg参数就是定时器的结构体
93   * @return        	: 无
94   */
95  static void key_timer_function(struct timer_list *arg)
96  {
97      int val;
98      
99      /* 读取按键值并上报按键事件 */
100     val = gpio_get_value(key.gpio_key);
101     input_report_key(key.idev, KEY_0, !val);
102     input_sync(key.idev);
103     
104     enable_irq(key.irq_key);
105 }
106 
107 /*
108  * @description  	: platform驱动的probe函数,当驱动与设备匹配成功
109  *                     以后此函数会被执行
110  * @param – pdev 	: platform设备指针
111  * @return        	: 0,成功;其他负值,失败
112  */
113 static int atk_key_probe(struct platform_device *pdev)
114 {
115     int ret;
116     
117     /* 初始化GPIO */
118     ret = key_gpio_init(pdev->dev.of_node);
119     if(ret < 0)
120         return ret;
121         
122     /* 初始化定时器 */
123     timer_setup(&key.timer, key_timer_function, 0);
124     
125     /* 申请input_dev */
126     key.idev = input_allocate_device();
127     key.idev->name = KEYINPUT_NAME;
128     
129 #if 0
130     /* 初始化input_dev,设置产生哪些事件 */
131     __set_bit(EV_KEY, key.idev->evbit); /* 设置产生按键事件 */
132     __set_bit(EV_REP, key.idev->evbit); /* 重复事件,比如按下去不放开,就
会一直输出信息 */
133 
134     /* 初始化input_dev,设置产生哪些按键 */
135     __set_bit(KEY_0, key.idev->keybit); 
136 #endif
137 
138 #if 0
139     key.idev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
140     key.idev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
141 #endif
142 
143     key.idev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
144     input_set_capability(key.idev, EV_KEY, KEY_0);
145 
146     /* 注册输入设备 */
147     ret = input_register_device(key.idev);
148     if (ret) {
149         printk("register input device failed!\r\n");
150         goto free_gpio;
151     }
152     
153     return 0;
154 free_gpio:
155     free_irq(key.irq_key,NULL);
156     gpio_free(key.gpio_key);
157     del_timer_sync(&key.timer);
158     return -EIO;
159     
160 }
161 
162 /*
163  * @description 	: platform驱动的remove函数,当platform驱动模块
164  *                     卸载时此函数会被执行
165  * @param – dev 	: platform设备指针
166  * @return        	: 0,成功;其他负值,失败
167  */
168 static int atk_key_remove(struct platform_device *pdev)
169 {
170     free_irq(key.irq_key,NULL);         	/* 释放中断号 	*/
171     gpio_free(key.gpio_key);            	/* 释放GPIO 	*/
172     del_timer_sync(&key.timer);         	/* 删除timer 	*/
173     input_unregister_device(key.idev);	/* 释放input_dev */
174     
175     return 0;
176 }
177 
178 static const struct of_device_id key_of_match[] = {
179     {.compatible = "alientek,key"},
180     {/* Sentinel */}
181 };
182 
183 static struct platform_driver atk_key_driver = {
184     .driver = {
185         .name = "rk3568-key",
186         .of_match_table = key_of_match,
187     },
188     .probe  = atk_key_probe,
189     .remove = atk_key_remove,
190 };
191 
192 module_platform_driver(atk_key_driver);
193 
194 MODULE_LICENSE("GPL");
195 MODULE_AUTHOR("ALIENTEK");
196 MODULE_INFO(intree, "Y");

在本程序中用到了定时器和中断相关的API函数,这些内容在前面章节都已经给大家介绍过了,而本章的重点知识点是linux下的input子系统,那下面我们将对keyinput.c代码进行讲解。
第14~19行,自定义的按键设备结构体struct key_dev,用于描述一个按键设备,其中的成员变量包括一个input_dev指针变量,定时器timer、GPIO以及中断号。
第29~39行,按键中断处理函数key_interrupt,当按键按下或松开的时候都会触发,也就是上升沿和下降沿都会触发此中断。key_interrupt函数中的操作也很简单,调用disable_irq_nosync函数先禁止中断,然后使用mod_timer打开定时器,定时时长为15ms。
第46~87行,按键的GPIO初始化。
第95~105行,定时器服务函数key_timer_function,使用到定时器的目的主要是为了使用软件的方式进行按键消抖处理,在key_timer_function函数中,我们使用gpio_get_value获取按键GPIO的电平状态,使用input_report_key函数上报按键事件。按键按下以后val为1,释放以后val为0。事件上报完成之后使用input_sync函数同步事件,表示此事件已上报完成,input子系统核心层就会进行相关的处理。第104行enable_irq函数使能中断,因为在按键中断发生的时候我们会关闭中断,等事件处理完成之后再打开。
第113~160行,platform驱动的probe函数atk_key_probe,其中第 136~161 行,使用 input_allocate_device 函数申请 input_dev,然后设置相应的事件以及事件码(也就是 KEY 模拟成那个按键,这里我们设置为 KEY_0)。最后使用 input_register_device函数向 Linux 内核注册 input_dev。
第168~176行,platform驱动的remove函数mykey_remove,在该函数中先释放GPIO在使用del_timer_sync删除定时器并且调用input_unregister_device卸载按键设备。
22.3.3 编写测试APP
新建keyinputApp.c文件,然后在里面输入如下所示内容:
示例代码 22.3.3.1 keyinputApp.c 文件代码段

1  #include <stdio.h>
2  #include <unistd.h>
3  #include <sys/types.h>
4  #include <sys/stat.h>
5  #include <fcntl.h>
6  #include <stdlib.h>
7  #include <string.h>
8  #include <linux/input.h>
9  
10 /*
11  * @description   	: main主程序
12  * @param – argc 	: argv数组元素个数
13  * @param – argv  	: 具体参数
14  * @return       	: 0 成功;其他 失败
15  */
16 int main(int argc, char *argv[])
17 {
18     int fd, ret;
19     struct input_event ev;
20 
21     if(2 != argc) {
22         printf("Usage:\n"
23              "\t./keyinputApp /dev/input/eventX    @ Open Key\n"
24         );
25         return -1;
26     }
27 
28     /* 打开设备 */
29     fd = open(argv[1], O_RDWR);
30     if(0 > fd) {
31         printf("Error: file %s open failed!\r\n", argv[1]);
32         return -1;
33     }
34 
35     /* 读取按键数据 */
36     for ( ; ; ) {
37 
38         ret = read(fd, &ev, sizeof(struct input_event));
39         if (ret) {
40             switch (ev.type) {
41             case EV_KEY:             			/* 按键事件 				*/ 
42                 if (KEY_0 == ev.code) {   		/* 判断是不是KEY_0按键 */ 
43                     if (ev.value)            	/* 按键按下 				*/ 
44                         printf("Key0 Press\n");
45                     else                 			/* 按键松开 				*/ 
46                         printf("Key0 Release\n");
47                 }
48                 break;
49 
50             /* 其他类型的事件,自行处理 */
51             case EV_REL:
52                 break;
53             case EV_ABS:
54                 break;
55             case EV_MSC:
56                 break;
57             case EV_SW:
58                 break;
59             };
60         }
61         else {
62             printf("Error: file %s read failed!\r\n", argv[1]);
63             goto out;
64         }
65     }
66 
67 out:
68     /* 关闭设备 */
69     close(fd);
70     return 0;
71 }

第22.1.3小节已经说过了,Linux内核会使用input_event结构体来表示输入事件,所以我们要获取按键输入信息,那么必须借助于input_event结构体。第19行定义了一个input_event类型变量ev。
第36~65行,当我们向Linux内核成功注册input_dev设备以后,会在/dev/input目录下生成一个名为“eventX(X=0….n)”的文件,这个/dev/input/eventX就是对应的input设备文件。我们读取这个文件就可以获取到输入事件信息,比如按键值什么的。使用read函数读取输入设备文件,也就是/dev/input/eventX,读取到的数据按照input_event结构体组织起来。获取到输入事件以后(input_event结构体类型)使用switch case语句来判断事件类型,本章实验我们设置的事件类型为EV_KEY,因此只需要处理EV_KEY事件即可。比如获取按键编号(KEY_0的编号为11)、获取按键状态,按下还是松开的?
22.4 运行测试
22.4.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第五章实验基本一样,只是将obj-m变量的值改为“miscled.o”,Makefile内容如下所示:
示例代码22.4.1.1 Makefile文件

1  KERNELDIR := /home/alientek/rk3568_linux_sdk/kernel
...... 
4  obj-m := keyinput.o
......
11 clean:
12  $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

第4行,设置obj-m变量的值为“keyinput.o”。
输入如下命令编译出驱动模块文件:
make ARCH=arm64 //ARCH=arm64必须指定,否则编译会失败
编译成功以后就会生成一个名为“keyinput.ko”的驱动模块文件。
2、编译测试APP
输入如下命令编译测试keyinputApp.c这个测试程序:
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc keyinputApp.c -o keyinputApp
编译成功以后就会生成keyinputApp这个应用程序。
22.4.2 运行测试
在Ubuntu中将上一小节编译出来的keyinput.ko和keyinputApp通过adb命令发送到开发板的/lib/modules/4.19.232目录下,命令如下:
adb push keyinput.ko keyinputApp /lib/modules/4.19.232
重启开发板,进入到目录lib/modules/5.4.31中。在加载keyinput.ko驱动模块之前,先看一下/dev/input目录下都有哪些文件,结果如图22.4.2.1所示:
在这里插入图片描述

图22.4.2.1 /dev/input目录
从图22.4.2.1可以看出,默认出厂系统中/dev/input目录下有event0~2三个文件。
接下来输入如下命令加载keyinput.ko这个驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe keyinput //加载驱动模块
当驱动模块加载成功以后再来看一下/dev/input目录下有哪些文件,结果如图22.4.2.2所示:
在这里插入图片描述

图22.4.2.2 加载驱动以后的/dev/input目录
从图22.4.2.2可以看出,加载驱动以后在/dev/input目录下生成了一个event10文件,这其实就是我们注册的驱动所对应的设备文件。keyinputApp就是通过读取/dev/input/event10这个文件来获取输入事件信息的,输入如下测试命令:
./keyinputApp /dev/input/event10
使用杜邦线将图13.2.1中GPIO3_C5这个IO接到开发板的3.3V电压上并拔出,模拟按键被按下与释放过程,结果如图22.4.2.3所示:
在这里插入图片描述

图22.4.2.3 测试结果
从图22.4.2.3可以看出,当我们按下或者释放开发板上的按键以后都会在终端上输出相应的内容,提示我们哪个按键按下或释放了,在Linux内核中KEY_0为11。
另外,我们也可以不用keyinputApp来测试驱动,可以直接使用hexdump命令来查看/dev/input/event10文件内容,输入如下命令:
hexdump /dev/input/event10
同样模拟按键按下,输出信息如下图所示:
在这里插入图片描述

图22.4.2.4 原始数据值
图22.4.2.4就是input_event类型的原始事件数据值,采用十六进制表示,这些原始数据的含义如下:
示例代码22.4.2.1 input_event类型的原始事件值

/**************************input_event类型*****************************/
/* 编号 */ /* tv_sec */ /* tv_usec */ /* type */  /* code */  /* value */
0000000 	5378 6445 		228b 000b 		0001 		000b 		0001 0000
0000010 	5378 6445 		228b 000b 		0000 		0000 		0000 0000
0000020 	5378 6445 		d913 000e 		0001 		000b 		0000 0000
0000030 	5378 6445 		d913 000e 		0000 		0000 		0000 0000

type为事件类型,查看示例代码22.1.2.3可知,EV_KEY事件值为1,EV_SYN事件值为0。因此第1行表示EV_KEY事件,第2行表示EV_SYN事件。code为事件编码,也就是按键号,查看示例代码22.1.2.4可以,KEY_0这个按键编号为11,对应的十六进制为0xb,因此第1行表示KEY_0这个按键事件,最后的value就是按键值,为1表示按下,为0的话表示松开。综上所述,示例代码22.4.2.1中的原始事件值含义如下:
第1行,按键(KEY_0)按下事件。
第2行,EV_SYN同步事件,因为每次上报按键事件以后都要同步的上报一个EV_SYN事件。
第3行,按键(KEY_0)松开事件。
第4行,EV_SYN同步事件,和第2行一样。
22.5 Linux自带按键驱动程序的使用
22.5.1 使能内核自带按键驱动程序源码简析
一般我们直接用GPIO引脚来驱动按键,但是这样会浪费大量的IO。瑞芯微官方使用ADC来驱动按键,不同的按键按下以后ADC值就不同,这样就可以通过采集具体电压来区分哪个按键按下了。这样就可以通过一个ADC引脚来外接很多按键,省下了宝贵的GPIO资源。
Linux主线内核自带的KEY驱动默认是用GPIO来连接按键的,如果你的产品使用GPIO来直接驱动按键,那么就可以使用此驱动。按照如下路径找到相应的配置选项:

 Device Drivers                                                                                            
	 Input device support                                                                                    
       	 Generic input layer (needed for keyboard, mouse, ...) (INPUT [=y])    
            	 Keyboards (INPUT_KEYBOARD [=y])  
				GPIO Buttons   
选中“GPIO Buttons”选项,将其编译进Linux内核中,如图22.5.1.1所示:

在这里插入图片描述

图22.5.1.1 内核自带KEY驱动使能选项
前面说了,瑞芯微官方使用ADC来驱动按键,所以图22.5.1.1中的通用按键驱动本节就用不了。应该使用“ADC Ladder Buttons”驱动,配置路径如下:
Device Drivers
Input device support
Generic input layer (needed for keyboard, mouse, …) (INPUT [=y])
Keyboards (INPUT_KEYBOARD [=y])
ADC Ladder Buttons
选中以后如图22.5.1.2所示:
在这里插入图片描述

图22.5.1.2 使能ADC按键驱动
对应的驱动文件为drivers/input/keyboard/adc-keys.c。
22.5.2 自带ADC按键驱动程序的使用
要使用Linux内核自带的ADC按键驱动程序很简单,只需要根据Documentation/devicetree/bindings/input/adc-keys.txt这个文件在设备树中添加指定的设备节点即可,节点要求如下:
①、节点名字为“adc-keys”。
②、adc-keys节点的compatible属性值一定要设置为“adc-keys”。
③、io-channel属性描述用于驱动按键的ADC通道。
④、io-channel-names属性要设置为“buttons”。
⑤、keyup-threshold-microvolt属性指定按键抬起时的电压,单位是微伏,正点原子ATK-CLRK3568F核心板上按键对应的ADC默认接到1.8V电压,所以按键抬起时就是1.8V。keyup-threshold-microvolt设置为1800000。
另外也有两个可选的属性:
①、poll-interval:轮询间隔,单位为ms,也就是ADC多久轮询查看一次按键的ADC值。
②、autorepeat属性描述是否支持连按,bool类型,可以设置为“true”或“false”。
所有的KEY都是adc-keys的子节点,每个子节点可以用如下属性描述自己:
label:按键名字。
linux,code:KEY要模拟的按键,也就是示例代码22.1.2.4中的这些按键。
press-threshold-microvolt:按键按下以后对应的ADC值,单位为微伏。
这里我们将开发板上的四个用户按键都用起来,KEY4、KEY5、KEY6和KEY7分别模拟为键盘上的:ESC、RIGHT、LEFT和MENU按键。
打开rk3568-atk-evb1-ddr4-v10.dtsi文件,我们出厂系统已经设置好了adc-keys节点,内容如下:
示例代码 22.5.2.1 gpio-keys 节点内容

1   &adc_keys {
2           vol-up-key {
3                   label = "volume up";
4                   linux,code = <KEY_VOLUMEUP>;
5                   press-threshold-microvolt = <17822>;
6           };
7 
8           vol-down-key {
9                   label = "volume down";
10                  linux,code = <KEY_VOLUMEDOWN>;
11                  press-threshold-microvolt = <415385>;
12          };
13
14          menu-key {
15                  label = "menu";
16                  linux,code = <KEY_MENU>;
17                  press-threshold-microvolt = <805525>;
18          };
19
20          esc-key {
21                  label = "esc";
22                  linux,code = <KEY_ESC>;
23                  press-threshold-microvolt = <1201993>;
24          };
25
26          /delete-node/ back-key;
27  };

第2~6行,V+按键子节点,也就是KEY4按键,此按键按下以后电压为17822uV。
第8~12行,V-按键子节点,也就是KEY5按键,此按键按下以后电压为415385uV。
第14~18行,MENU按键子节点,也就是KEY6按键,此按键按下以后电压为805525uV。
第20~24行,ESC按键子节点,也就是KEY7按键,此按键按下以后电压为1201993uV。
系统启动以后查看/dev/input目录,看看都有哪些文件,结果如图22.5.2.1所示:
在这里插入图片描述

图22.5.2.1 /dev/input目录文件
从图22.5.2.1可以看出存在event0~event9这10个文件,其中肯定就有按键对应的文件。每个都测试一遍,就知道哪个文件是按键的了。这里是event7这个文件,输入如下命令:
hexdump /dev/input/event7
然后按下ATK-DLRK3568开发板上的V+(KEY4)、ESC(KEY7)、V-(KEY5)和MENU(KEY6)这四个按键,终端输出图22.5.2.2所示内容:
在这里插入图片描述

图22.5.2.2 按键信息
如果按下按键以后会在终端上输出图22.5.2.2所示的信息那么就表示Linux内核的按键驱动工作正常。至于图22.5.2.2中内容的含义大家就自行分析,这个已经在22.4.2小节详细的分析过了,这里就不再讲解了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1548325.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DNS协议 是什么?说说DNS 完整的查询过程?

一、是什么 DNS&#xff08;Domain Names System&#xff09;&#xff0c;域名系统&#xff0c;是互联网一项服务&#xff0c;是进行域名和与之相对应的 IP 地址进行转换的服务器 简单来讲&#xff0c;DNS相当于一个翻译官&#xff0c;负责将域名翻译成ip地址 IP 地址&#…

【数据分享】1929-2023年全球站点的逐日平均海平面压力(Shp\Excel\免费获取)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、能见度等指标&#xff0c;说到气象数据&#xff0c;最详细的气象数据是具体到气象监测站点的数据&#xff01; 有关气象指标的监测站点数据&#xff0c;之前我们分享过1929-2023年全球气象站…

笔记本作为其他主机显示屏(HDMI采集器)

前言&#xff1a; 我打算打笔记本作为显示屏来用&#xff0c;连上工控机&#xff0c;这不是贼方便吗 操作&#xff1a; 一、必需品 HDMI采集器一个 可以去绿联买一个&#xff0c;便宜的就行&#xff0c;我的大概就长这样 win10下载 PotPlayer 软件 下载链接&#xff1a;h…

考了PMP证后工资大概是多少 ?

PMP自1999年引入国内以来&#xff0c;大家对这个证书的了解并不深&#xff0c;每年考试的人数也不多。但随着越来越多的企业认可PMP认证&#xff0c;目前考证的人数不断增加&#xff0c;几乎所有与项目管理相关的人都知道这个证书的重要性。这个证书在招聘要求中出现频率较高&a…

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记12:DAC数模转换

系列文章目录 嵌入式|蓝桥杯STM32G431&#xff08;HAL库开发&#xff09;——CT117E学习笔记01&#xff1a;赛事介绍与硬件平台 嵌入式|蓝桥杯STM32G431&#xff08;HAL库开发&#xff09;——CT117E学习笔记02&#xff1a;开发环境安装 嵌入式|蓝桥杯STM32G431&#xff08;…

【Codesys】-扫描添加失败,手动添加第三方模块,真·DC模式的高速计数模块

欧姆龙耦合器&#xff08;NX-ECC201&#xff09;和高速计数模块&#xff08;NX-EC0132&#xff09;单独使用。扫描设备添加到Codesys中&#xff0c;会报错&#xff0c;无法自动添加。需要手动添加。内容方法如下。如遇到其他第三方耦合器解决方式也同下面一样。记录一下&#x…

三位数组合-第12届蓝桥杯选拔赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第42讲。 三位数组合&#…

MySql实战--事务到底是隔离的还是不隔离的

第3篇文章和你讲事务隔离级别的时候提到过&#xff0c;如果是可重复读隔离级别&#xff0c;事务T启动的时候会创建一个视图read-view&#xff0c;之后事务T执行期间&#xff0c;即使有其他事务修改了数据&#xff0c;事务T看到的仍然跟在启动时看到的一样。也就是说&#xff0c…

【zip技巧】4种方法,删除ZIP压缩包密码

之前给大家介绍了zip压缩包加密方法&#xff0c;那么zip压缩包取消密码&#xff0c;大家了解多少呢&#xff1f;有密码的情况下&#xff0c;有哪些方法可以取消密码&#xff1f;无密码又该如何取消密码&#xff1f;今天总结四个方法分享给大家。 一、 最原始的方法&#xff0…

“Hands-free AG audio“和“Stereo“的区别

用蓝牙连接耳机后&#xff0c;发现有两个选项 一个音量大&#xff0c;一个音质好&#xff0c;好奇去查了查。 “Hands-free AG audio”&#xff08;自由通话音频&#xff09;是指一种技术或功能&#xff0c;可以使您在进行通话时无需使用手部操作或接触设备。这通常适用于汽车…

Spring:面试八股

文章目录 参考Spring模块CoreContainerAOP 参考 JavaGuide Spring模块 CoreContainer Spring框架的核心模块&#xff0c;主要提供IoC依赖注入功能的支持。内含四个子模块&#xff1a; Core&#xff1a;基本的核心工具类。Beans&#xff1a;提供对bean的创建、配置、管理功能…

智能文档处理技术综述

一、 智能文档处理介绍 智能文档处理&#xff08;Intelligent Document Processing, IDP&#xff09;是利用人工智能&#xff08;AI&#xff09;、机器学习&#xff08;ML&#xff09;、计算机视觉&#xff08;CV&#xff09;、自然语言处理&#xff08;NLP&#xff09;等技术…

【WEEK4】 【DAY5】AJAX第二部分【中文版】

2024.3.22 Friday 接上文【WEEK4】 【DAY4】AJAX第一部分【中文版】 目录 8.4.Ajax异步加载数据8.4.1.新建User.java8.4.2.在pom.xml中添加lombok、jackson支持8.4.3.更改tomcat设置8.4.4.修改AjaxController.java8.4.5.新建test2.jsp8.4.5.1.注意&#xff1a;和WEB-INF平级&…

R语言学习——Rstudio软件

R语言免费但有点难上手&#xff0c;是数据挖掘的入门级别语言&#xff0c;拥有顶级的可视化功能。 优点&#xff1a; 1统计分析&#xff08;可以实现各种分析方法&#xff09;和计算&#xff08;有很多函数&#xff09; 2强大的绘图功能 3扩展包多&#xff0c;适合领域多 …

实现UI自动化测试,这5个常见问题你必须知道!

UI自动化测试一直都是如此的令人纠结&#xff0c;自动化测试初学者总是拿它入门&#xff0c;但有些经验丰富者对其又是毁誉参半&#xff0c;抑或抛出分层自动化测试那个经典的“金字塔”&#xff0c;来说明UI自动化测试还是少做为好。 我在从事7年产品研发之后&#xff0c;临危…

【学习】软件测试行业有哪些从业方向

从事任何一个行业&#xff0c;不论想入行的新人还是已经在职的从业人员&#xff0c;一定要系统化的掌握自身的学习路线和发展方向&#xff0c;随时对自身的优劣点掌握清楚。尤其是对于软件测试这个岗位。测试职业所涉及的技能范围比较广&#xff0c;测试流程、测试计划、缺陷管…

Linux如何将桌面版转为mini版-解决中文字体变为英文字体

中文字体转为英文字体 我们进入Rocky-Linux后&#xff0c;ls或者打开文件夹发现有中文 我们执行命令 sudo localedef -i en_US -f UTF-8 en_US.UTF-8将其转为英文&#xff0c;并且重启机器 此时中文转化为英文 桌面版linux转为MINN版 1. 我们可以卸载桌面版 sudo dnf gr…

每日一题——LeetCode1748.唯一元素的和

方法一 两次遍历 var sumOfUnique function(nums) {let map new Map()for(let num of nums){map.set(num,map.has(num)?map.get(num)1:1)}let res0for(let num of nums){if(map.get(num)1) resnum}return res }; 消耗时间和内存情况&#xff1a; 方法二 一次遍历 var su…

VMware下建立CentOS 7

1.点击新建虚拟机 2.下一步 3.选择号安装程序光盘映像文件位置&#xff0c;下一步 4.选择版本和操作系统然后下一步 5.编辑虚拟机名称并选择安装位置&#xff0c;然后下一步 6.设置最大磁盘大小&#xff0c;下一步 7.点击完成 8.点击编辑虚拟机设置 9.将此虚拟机内存设置为2G&a…

如何搭建selenium自动化测试框架?selenium自动化测试环境搭建(webdriver+Python框架)

一、安装Python 选择Python版本后&#xff0c;进入Python官方网站下载适配机型版本&#xff1a;http://www.python.org/ 二、安装setuptools和pip setuptools下载地址&#xff1a;https://pypi.python.org/pypi/setuptools pip下载地址&#xff1a;https://pypi.python.org/py…