正点原子 发表于 2020-7-7 10:25:46

【正点原子Linux连载】第五十八章Linux INPUT子系统实验--摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南

本帖最后由 正点原子 于 2020-10-26 11:58 编辑

1)实验平台:正点原子阿尔法Linux开发板
2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
4)对正点原子Linux感兴趣的同学可以加群讨论:876919289
5)关注正点原子公众号,获取最新资料





第五十八章Linux INPUT子系统实验

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



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

图58.1.1.1 input子系统结构图
      图58.1.1中左边就是最底层的具体设备,比如按键、USB键盘/鼠标等,中间部分属于Linux内核空间,分为驱动层、核心层和时间层,最右边的就是用户空间,所有的输入设备以文件的形式供用户应用程序使用。可以看出input子系统用到了我们前面讲解的驱动分层模型,我们编写驱动程序的时候只需要关注中间的驱动层、核心层和事件层,这三个层的分工如下:
      驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。
      核心层:承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行处理。
      事件层:主要和用户空间进行交互。
58.1.2input驱动编写流程
      input核心层会向Linux内核注册一个字符设备,大家找到drivers/input/input.c这个文件,input.c就是input输入子系统的核心层,此文件里面有如下所示代码:
示例代码58.1.2.1 input核心层创建字符设备过程
1767struct class input_class ={
1768.name       ="input",
1769.devnode    = input_devnode,
1770};
......
2414staticint __init input_init(void)
2415{
2416int err;
2417
2418    err = class_register(&input_class);
2419if(err){
2420      pr_err("unable to register input_dev class\n");
2421return err;
2422}
2423
2424    err = input_proc_init();
2425if(err)
2426goto fail1;
2427
2428    err = register_chrdev_region(MKDEV(INPUT_MAJOR,0),
2429                     INPUT_MAX_CHAR_DEVICES,"input");
2430if(err){
2431      pr_err("unable to register char major %d", INPUT_MAJOR);
2432goto fail2;
2433}
2434
2435return0;
2436
2437fail2:    input_proc_exit();
2438fail1:    class_unregister(&input_class);
2439return err;
2440}
      第2418行,注册一个input类,这样系统启动以后就会在/sys/class目录下有一个input子目录,如图58.1.2.1所示:

图58.1.2.1input类
      第2428~2429行,注册一个字符设备,主设备号为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文件中,定义如下(有省略):
示例代码58.1.2.2 input_dev结构体
121struct input_dev {
122constchar*name;
123constchar*phys;
124constchar*uniq;
125struct input_id id;
126
127unsignedlong propbit;
128
129unsignedlong evbit;      /* 事件类型的位图 */
130unsignedlong keybit;      /* 按键值的位图          */
131unsignedlong relbit;      /* 相对坐标的位图*/
132unsignedlong absbit;      /* 绝对坐标的位图*/
133unsignedlong mscbit;      /* 杂项事件的位图*/
134unsignedlong ledbit;      /*LED相关的位图    */
135unsignedlong sndbit;/* sound有关的位图 */
136unsignedlong ffbit;      /* 压力反馈的位图*/
137unsignedlong swbit;      /*开关状态的位图   */
......
189bool devres_managed;
190};
      第129行,evbit表示输入事件类型,可选的事件类型定义在include/uapi/linux/input.h文件中,事件类型如下:
示例代码58.1.2.3 事件类型
#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    /* LED                         */
#define EV_SND                  0x12    /* sound(声音)         */
#define EV_REP                  0x14    /* 重复事件                */
#define EV_FF                   0x15    /* 压力事件                */
#define EV_PWR                  0x16    /* 电源事件                */
#define EV_FF_STATUS            0x17    /* 压力状态事件      */
      比如本章我们要使用到按键,那么就需要注册EV_KEY事件,如果要使用连按功能的话还需要注册EV_REP事件。
      继续回到示例代码58.1.2.2中,第129行~137行的evbit、keybit、relbit等等都是存放不同事件对应的值。比如我们本章要使用按键事件,因此要用到keybit,keybit就是按键事件使用的位图,Linux内核定义了很多按键值,这些按键值定义在include/uapi/linux/input.h文件中,按键值如下:
示例代码58.1.2.4 按键值
215 #define KEY_RESERVED                        0
216 #define KEY_ESC                         1
217 #define KEY_1                           2
218 #define KEY_2                           3
219 #define KEY_3                           4
220 #define KEY_4                           5
221 #define KEY_5                           6
222 #define KEY_6                           7
223 #define KEY_7                           8
224 #define KEY_8                           9
225 #define KEY_9                           10
226 #define KEY_0                           11
......
794 #define BTN_TRIGGER_HAPPY39      0x2e6
795 #define BTN_TRIGGER_HAPPY40      0x2e7
      我们可以将开发板上的按键值设置为示例代码58.1.2.4中的任意要一个,比如我们本章实验会将I.MX6U-ALPHA开发板上的KEY按键值设置为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_unregister_device函数向Linux系统注册前面初始化好的input_dev。
      ④、卸载input驱动的时候需要先使用input_unregister_device函数注销掉注册的input_dev,然后使用input_free_device函数释放掉前面申请的input_dev。input_dev注册过程示例代码如下所示:
示例代码58.1.2.5 input_dev注册流程
1struct input_dev *inputdev;/* input结构体变量 */
2
3/* 驱动入口函数 */
4staticint __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= BIT_MASK(EV_KEY)|
BIT_MASK(EV_REP);
18      keyinputdev.inputdev->keybit|=
BIT_MASK(KEY_0);
19      /************************************************/
20
21      /*********第三种设置事件和事件值的方法***********/
22      keyinputdev.inputdev->evbit= 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      return0;
30}
31
32/* 驱动出口函数 */
33staticvoid __exit xxx_exit(void)
34{
35      input_unregister_device(inputdev);      /* 注销input_dev         */
36      input_free_device(inputdev);                /* 删除input_dev         */
37}
      第1行,定义一个input_dev结构体指针变量。
      第4~30行,驱动入口函数,在此函数中完成input_dev的申请、设置、注册等工作。第7行调用input_allocate_device函数申请一个input_dev。第10~23行都是设置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函数,此函数内容如下:
例代码58.1.2.6 input_report_key函数
static inline void input_report_key(struct input_dev *dev,
unsignedint code,int value)
{
    input_event(dev, EV_KEY, code,!!value);
}
      从示例代码58.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。
      返回值:无。
      综上所述,按键的上报事件的参考代码如下所示:
示例代码58.1.2.7 事件上报参考代码
1 /* 用于按键消抖的定时器服务函数 */
2void timer_function(unsignedlong arg)
3{
4      unsignedchar 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这个按键没有按下或松开了。
58.1.3 input_event结构体
      Linux内核使用input_event这个结构体来表示所有的输入事件,input_envent结构体定义在include/uapi/linux/input.h文件中,结构体内容如下:
示例代码58.1.3.1 input_event结构体
24struct input_event {
25      struct timeval time;
26      __u16 type;
27      __u16 code;
28      __s32 value;
29};
      我们依次来看一下input_event结构体中的各个成员变量:
      time:时间,也就是此事件发生的时间,为timeval结构体类型,timeval结构体定义如下:
示例代码58.1.3.2 timeval结构体
1typedeflong                __kernel_long_t;
2typedef __kernel_long_t         __kernel_time_t;
3typedef __kernel_long_t          __kernel_suseconds_t;
4
5struct timeval {
6      __kernel_time_t                     tv_sec;      /* 秒      */
7      __kernel_suseconds_t            tv_usec;      /* 微秒      */
8};
      从示例代码58.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_envent这个结构体非常重要,因为所有的输入设备最终都是按照input_event结构体呈现给用户的,用户应用程序可以通过input_event来获取到具体的输入事件或相关的值,比如按键值等。关于input子系统就讲解到这里,接下来我们就以开发板上的KEY0按键为例,讲解一下如何编写input驱动。
58.2 硬件原理图分析
本章实验硬件原理图参考15.2小节即可。
58.3实验程序编写
本实验对应的例程路径为:开发板光盘->2、Linux驱动例程->20_input。
58.3.1 修改设备树文件
      直接使用49.3.1小节创建的key节点即可。
58.3.2 按键input驱动程序编写
新建名为“20_input”的文件夹,然后在20_input文件夹里面创建vscode工程,工作区命名为“keyinput”。工程创建好以后新建keyinput.c文件,在keyinput.c里面输入如下内容:
示例代码58.3.2.1 keyinput.c文件代码段
1   #include <linux/types.h>
2   #include <linux/kernel.h>
3   #include <linux/delay.h>
4   #include <linux/ide.h>
5   #include <linux/init.h>
6   #include <linux/module.h>
7   #include <linux/errno.h>
8   #include <linux/gpio.h>
9   #include <linux/cdev.h>
10#include <linux/device.h>
11#include <linux/of.h>
12#include <linux/of_address.h>
13#include <linux/of_gpio.h>
14#include <linux/input.h>
15#include <linux/semaphore.h>
16#include <linux/timer.h>
17#include <linux/of_irq.h>
18#include <linux/irq.h>
19#include <asm/mach/map.h>
20#include <asm/uaccess.h>
21#include <asm/io.h>
22/***************************************************************
23Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
24文件名                : keyinput.c
25作者      : 左忠凯
26版本      : V1.0
27描述      : Linux按键input子系统实验
28其他      : 无
29论坛      : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
30日志      : 初版V1.0 2019/8/21 左忠凯创建
31***************************************************************/
32#define KEYINPUT_CNT         1      /* 设备号个数      */
33#define KEYINPUT_NAME         "keyinput"      /* 名字      */
34#define KEY0VALUE               0X01/* KEY0按键值      */
35#define INVAKEY                  0XFF/* 无效的按键值      */
36#define KEY_NUM               1      /* 按键数量      */
37
38/* 中断IO描述结构体 */
39struct irq_keydesc {
40int gpio;      /* gpio                         */
41int irqnum;      /* 中断号                */
42unsignedchar value;      /* 按键对应的键值      */
43char name;      /* 名字                        */
44      irqreturn_t (*handler)(int,void*);/* 中断服务函数 */
45};
46
47/* keyinput设备结构体 */
48struct keyinput_dev{
49      dev_t devid;      /* 设备号                */
50struct cdev cdev;      /* cdev                     */
51struct class *class;      /* 类                        */
52struct device *device;      /* 设备                        */
53struct device_node*nd;/* 设备节点                */
54struct timer_list timer;      /* 定义一个定时器      */
55struct irq_keydesc irqkeydesc;      /* 按键描述数组 */
56unsignedchar curkeynum;                /* 当前的按键号      */
57struct input_dev *inputdev;                              /* input结构体      */
58};
59
60struct keyinput_dev keyinputdev;/* key input设备 */
61
62/* @description                  : 中断服务函数,开启定时器,延时10ms,
63   *                           定时器用于按键消抖。
64   * @param - irq             : 中断号
65   * @param - dev_id          : 设备结构。
66   * @return                  : 中断执行结果
67   */
68static irqreturn_t key0_handler(int irq,void*dev_id)
69{
70struct keyinput_dev *dev =(struct keyinput_dev *)dev_id;
71
72      dev->curkeynum =0;
73      dev->timer.data =(volatilelong)dev_id;
74      mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
75return IRQ_RETVAL(IRQ_HANDLED);
76}
77
78/* @description         : 定时器服务函数,用于按键消抖,定时器到了以后
79   *                        再次读取按键值,如果按键还是处于按下状态就表示按键有效。
80   * @param - arg         : 设备结构变量
81   * @return            : 无
82   */
83void timer_function(unsignedlong arg)
84{
85unsignedchar value;
86unsignedchar num;
87struct irq_keydesc *keydesc;
88struct keyinput_dev *dev =(struct keyinput_dev *)arg;
89
90      num = dev->curkeynum;
91      keydesc =&dev->irqkeydesc;
92      value = gpio_get_value(keydesc->gpio);      /* 读取IO值      */
93if(value ==0){                /* 按下按键                */
94/* 上报按键值 */
95//input_event(dev->inputdev, EV_KEY, keydesc->value, 1);
96          input_report_key(dev->inputdev, keydesc->value,1);/*1,按下*/
97          input_sync(dev->inputdev);
98}else{      /* 按键松开 */
99//input_event(dev->inputdev, EV_KEY, keydesc->value, 0);
100         input_report_key(dev->inputdev, keydesc->value,0);
101         input_sync(dev->inputdev);
102}
103}
104
105/*
106* @description         : 按键IO初始化
107* @param               : 无
108* @return            : 无
109*/
110staticint keyio_init(void)
111{
112unsignedchar i =0;
113char name;
114int ret =0;
115
116   keyinputdev.nd = of_find_node_by_path("/key");
117if(keyinputdev.nd==NULL){
118         printk("key node not find!\r\n");
119return-EINVAL;
120}
121
122/* 提取GPIO */
123for(i =0; i < KEY_NUM; i++){
124         keyinputdev.irqkeydesc.gpio =of_get_named_gpio(keyinputdev.nd,"key-gpio", i);
125if(keyinputdev.irqkeydesc.gpio <0){
126             printk("can't get key%d\r\n", i);
127}
128}
129
130/* 初始化key所使用的IO,并且设置成中断模式 */
131for(i =0; i < KEY_NUM; i++){
132         memset(keyinputdev.irqkeydesc.name,0,sizeof(name));
133         sprintf(keyinputdev.irqkeydesc.name,"KEY%d", i);
134         gpio_request(keyinputdev.irqkeydesc.gpio, name);
135         gpio_direction_input(keyinputdev.irqkeydesc.gpio);
136         keyinputdev.irqkeydesc.irqnum =
irq_of_parse_and_map(keyinputdev.nd, i);
137}
138/* 申请中断 */
139   keyinputdev.irqkeydesc.handler = key0_handler;
140   keyinputdev.irqkeydesc.value = KEY_0;
141
142for(i =0; i < KEY_NUM; i++){
143         ret = request_irq(keyinputdev.irqkeydesc.irqnum,
keyinputdev.irqkeydesc.handler,
144                            IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
keyinputdev.irqkeydesc.name,&keyinputdev);
145if(ret <0){
146             printk("irq %d request failed!\r\n",
keyinputdev.irqkeydesc.irqnum);
147return-EFAULT;
148}
149}
150
151/* 创建定时器 */
152   init_timer(&keyinputdev.timer);
153   keyinputdev.timer.function = timer_function;
154
155/* 申请input_dev */
156   keyinputdev.inputdev = input_allocate_device();
157   keyinputdev.inputdev->name = KEYINPUT_NAME;
158 #if0
159/* 初始化input_dev,设置产生哪些事件 */
160   __set_bit(EV_KEY, keyinputdev.inputdev->evbit);/*按键事件      */
161   __set_bit(EV_REP, keyinputdev.inputdev->evbit);/* 重复事件*/
162
163/* 初始化input_dev,设置产生哪些按键 */
164   __set_bit(KEY_0, keyinputdev.inputdev->keybit);
165 #endif
166
167 #if0
168   keyinputdev.inputdev->evbit= BIT_MASK(EV_KEY)|
BIT_MASK(EV_REP);
169   keyinputdev.inputdev->keybit|=
BIT_MASK(KEY_0);
170 #endif
171
172   keyinputdev.inputdev->evbit= BIT_MASK(EV_KEY)|
BIT_MASK(EV_REP);
173   input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
174
175/* 注册输入设备 */
176   ret = input_register_device(keyinputdev.inputdev);
177if(ret){
178         printk("register input device failed!\r\n");
179return ret;
180}
181return0;
182}
183
184/*
185* @description         : 驱动入口函数
186* @param               : 无
187* @return            : 无
188*/
189staticint __init keyinput_init(void)
190{
191   keyio_init();
192return0;
193}
194
195/*
196* @description         : 驱动出口函数
197* @param               : 无
198* @return            : 无
199*/
200staticvoid __exit keyinput_exit(void)
201{
202unsigned i =0;
203/* 删除定时器 */
204   del_timer_sync(&keyinputdev.timer);
205
206/* 释放中断 */
207for(i =0; i < KEY_NUM; i++){
208         free_irq(keyinputdev.irqkeydesc.irqnum,&keyinputdev);
209}
210/* 释放input_dev */
211   input_unregister_device(keyinputdev.inputdev);
212   input_free_device(keyinputdev.inputdev);
213}
214
215 module_init(keyinput_init);
216 module_exit(keyinput_exit);
217 MODULE_LICENSE("GPL");
218 MODULE_AUTHOR("zuozhongkai");
      keyinput.c文件内容其实就是实验“13_irq”中的imx6uirq.c文件中修改而来的,只是将其中与字符设备有关的内容进行了删除,加入了input_dev相关的内容,我们简单来分析一下示例代码58.3.2.1中的程序。
      第57行,在设备结构体中定义一个input_dev指针变量。
      第93~102行,在按键消抖定时器处理函数中上报输入事件,也就是使用input_report_key函数上报按键事件以及按键值,最后使用input_sync函数上报一个同步事件,这一步一定得做!
      第156~180行,使用input_allocate_device函数申请input_dev,然后设置相应的事件以及事件码(也就是KEY模拟成那个按键,这里我们设置为KEY_0)。最后使用input_register_device函数向Linux内核注册input_dev。
      第211~212行,当注销input设备驱动的时候使用input_unregister_device函数注销掉前面注册的input_dev,最后使用input_free_device函数释放掉前面申请的input_dev。
58.3.3 编写测试APP
新建keyinputApp.c文件,然后在里面输入如下所示内容:
示例代码58.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 "sys/ioctl.h"
6#include "fcntl.h"
7#include "stdlib.h"
8#include "string.h"
9#include <poll.h>
10 #include <sys/select.h>
11 #include <sys/time.h>
12 #include <signal.h>
13 #include <fcntl.h>
14 #include <linux/input.h>
15/***************************************************************
16 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
17文件名      : keyinputApp.c
18作者      : 左忠凯
19版本      : V1.0
20描述      : input子系统测试APP。
21其他      : 无
22使用方法                :./keyinputApp /dev/input/event1
23论坛      : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
24日志      : 初版V1.0 2019/8/26 左忠凯创建
25 ***************************************************************/
26
27/* 定义一个input_event变量,存放输入事件信息 */
28staticstruct input_event inputevent;
29
30/*
31* @description          : main主程序
32* @param - argc          : argv数组元素个数
33* @param - argv         : 具体参数
34* @return                : 0 成功;其他失败
35*/
36int main(int argc,char*argv[])
37{
38      int fd;
39      int err =0;
40      char*filename;
41
42      filename = argv;
43
44      if(argc !=2){
45      printf("Error Usage!\r\n");
46      return-1;
47      }
48
49      fd = open(filename, O_RDWR);
50      if(fd <0){
51      printf("Can't open file %s\r\n", filename);
52      return-1;
53      }
54
55                while(1){
56      err = read(fd,&inputevent,sizeof(inputevent));
57      if(err >0){/* 读取数据成功 */
58                switch(inputevent.type){
59                case EV_KEY:
60                if(inputevent.code < BTN_MISC){/* 键盘键值 */
61                printf("key %d %s\r\n", inputevent.code,
inputevent.value ?"press":"release");
62                }else{
63                printf("button %d %s\r\n", inputevent.code,
inputevent.value ?"press":"release");
64                }
65                break;
66
67                /* 其他类型的事件,自行处理 */
68                case EV_REL:
69                break;
70                case EV_ABS:
71                break;
72                case EV_MSC:
73                break;
74                case EV_SW:
75                break;
76                }
77      }else{
78                printf("读取数据失败\r\n");
79      }
80      }
81      return0;
82}
      第58.1.3小节已经说过了,Linux内核会使用input_event结构体来表示输入事件,所以我们要获取按键输入信息,那么必须借助于input_event结构体。第28行定义了一个inputevent变量,此变量为input_event结构体类型。
      第56行,当我们向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)、获取按键状态,按下还是松开的?
58.4 运行测试
58.4.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第四十章实验基本一样,只是将obj-m变量的值改为“keyinput.o”,Makefile内容如下所示:
示例代码58.4.1.1 Makefile文件
1KERNELDIR:= /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
......
4obj-m := keyinput.o
......
11 clean:
12$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
      第4行,设置obj-m变量的值为“keyinput.o”。
      输入如下命令编译出驱动模块文件:
make-j32
      编译成功以后就会生成一个名为“keyinput.ko”的驱动模块文件。
2、编译测试APP
      输入如下命令编译测试keyinputApp.c这个测试程序:
arm-linux-gnueabihf-gcc keyinputApp.c -o keyinputApp
      编译成功以后就会生成keyinputApp这个应用程序。
58.4.2 运行测试
      将上一小节编译出来keyinput.ko和keyinputApp这两个文件拷贝到rootfs/lib/modules/4.1.15目录中,重启开发板,进入到目录lib/modules/4.1.15中。在加载keyinput.ko驱动模块之前,先看一下/dev/input目录下都有哪些文件,结果如图58.4.2.1所示:

图58.4.2.1 当前/dev/input目录文件
      从图58.4.2.1可以看出,当前/dev/input目录只有event0和mice这两个文件。接下来输入如下命令加载keyinput.ko这个驱动模块。
depmod                              //第一次加载驱动的时候需要运行此命令
modprobe keyinput.ko      //加载驱动模块
      当驱动模块加载成功以后再来看一下/dev/input目录下有哪些文件,结果如图58.4.2.2所示:

图58.4.2.2 加载驱动以后的/dev/input目录
      从图58.4.2.2可以看出,多了一个event1文件,因此/dev/input/event1就是我们注册的驱动所对应的设备文件。keyinputApp就是通过读取/dev/input/event1这个文件来获取输入事件信息的,输入如下测试命令:
./keyinputApp /dev/input/event1
      然后按下开发板上的KEY按键,结果如图58.4.2.3所示:

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

图58.4.2.4 原始数据值
      图58.4.2.4就是input_event类型的原始事件数据值,采用十六进制表示,这些原始数据的含义如下:
示例代码58.4.2.1 input_event类型的原始事件值
      /*****************input_event类型********************/
/* 编号 */      /* tv_sec */      /* tv_usec */         /* type */      /* code */   /* value */
0000000      0c410000      d7cd 000c             0001      000b                   0001 0000
0000010         0c41 0000         d7cd000c      00000000      00000000
0000020      0c420000      54bb0000      0001 000b      00000000
0000030      0c420000      54bb0000      00000000      00000000
      type为事件类型,查看示例代码58.1.2.3可知,EV_KEY事件值为1,EV_SYN事件值为0。因此第1行表示EV_KEY事件,第2行表示EV_SYN事件。code为事件编码,也就是按键号,查看示例代码58.1.2.4可以,KEY_0这个按键编号为11,对应的十六进制为0xb,因此第1行表示KEY_0这个按键事件,最后的value就是按键值,为1表示按下,为0的话表示松开。综上所述,示例代码58.4.2.1中的原始事件值含义如下:
      第1行,按键(KEY_0)按下事件。
      第2行,EV_SYN同步事件,因为每次上报按键事件以后都要同步的上报一个EV_SYN事件。
      第3行,按键(KEY_0)松开事件。
      第4行,EV_SYN同步事件,和第2行一样。
58.5 Linux自带按键驱动程序的使用
58.5.1 自带按键驱动程序源码简析
      Linux内核也自带了KEY驱动,如果要使用内核自带的KEY驱动的话需要配置Linux内核,不过Linux内核一般默认已经使能了KEY驱动,但是我们还是要检查一下。按照如下路径找到相应的配置选项:
-> Device Drivers                                                                                          
      -> Input device support                                                                                    
      -> Generic input layer (needed for keyboard, mouse, ...) (INPUT [=y])   
      -> Keyboards (INPUT_KEYBOARD [=y])
                              ->GPIO Buttons   
      选中“GPIO Buttons”选项,将其编译进Linux内核中,如图58.5.1.1所示:

图58.5.1.1 内核自带KEY驱动使能选项
      选中以后就会在.config文件中出现“CONFIG_KEYBOARD_GPIO=y”这一行,Linux内核就会根据这一行来将KEY驱动文件编译进Linux内核。Linux内核自带的KEY驱动文件为drivers/input/keyboard/gpio_keys.c,gpio_keys.c采用了platform驱动框架,在KEY驱动上使用了input子系统实现。在gpio_keys.c文件中找到如下所示内容:
示例代码58.5.1.1 gpio_keys文件代码段
673staticconststruct of_device_id gpio_keys_of_match[]={
674{.compatible ="gpio-keys",},
675{},
676};
......
842staticstruct platform_driver gpio_keys_device_driver ={
843.probe      = gpio_keys_probe,
844.remove   = gpio_keys_remove,
845.driver   ={
846.name   ="gpio-keys",
847.pm =&gpio_keys_pm_ops,
848.of_match_table = of_match_ptr(gpio_keys_of_match),
849}
850};
851
852staticint __init gpio_keys_init(void)
853{
854return platform_driver_register(&gpio_keys_device_driver);
855}
856
857staticvoid __exit gpio_keys_exit(void)
858{
859   platform_driver_unregister(&gpio_keys_device_driver);
860}
      从示例代码58.5.1.1可以看出,这就是一个标准的platform驱动框架,如果要使用设备树来描述KEY设备信息的话,设备节点的compatible属性值要设置为“gpio-keys”。当设备和驱动匹配以后gpio_keys_probe函数就会执行,gpio_keys_probe函数内容如下(为了篇幅有缩减):
示例代码58.5.1.2 gpio_keys_probe函数代码段
689staticint gpio_keys_probe(struct platform_device *pdev)
690{
691struct device *dev =&pdev->dev;
692conststruct gpio_keys_platform_data *pdata =
dev_get_platdata(dev);
693struct gpio_keys_drvdata *ddata;
694struct input_dev *input;
695size_t size;
696int i, error;
697int wakeup =0;
698
699if(!pdata){
700         pdata = gpio_keys_get_devtree_pdata(dev);
701if(IS_ERR(pdata))
702return PTR_ERR(pdata);
703}
......
713   input = devm_input_allocate_device(dev);
714if(!input){
715         dev_err(dev,"failed to allocate input device\n");
716return-ENOMEM;
717}
718
719   ddata->pdata = pdata;
720   ddata->input = input;
721   mutex_init(&ddata->disable_lock);
722
723   platform_set_drvdata(pdev, ddata);
724   input_set_drvdata(input, ddata);
725
726   input->name = pdata->name ?: pdev->name;
727   input->phys ="gpio-keys/input0";
728   input->dev.parent =&pdev->dev;
729   input->open = gpio_keys_open;
730   input->close = gpio_keys_close;
731
732   input->id.bustype = BUS_HOST;
733   input->id.vendor =0x0001;
734   input->id.product =0x0001;
735   input->id.version =0x0100;
736
737/* Enable auto repeat feature of Linux input subsystem */
738if(pdata->rep)
739         __set_bit(EV_REP, input->evbit);
740
741for(i =0; i < pdata->nbuttons; i++){
742conststruct gpio_keys_button *button =&pdata->buttons;
743struct gpio_button_data *bdata =&ddata->data;
744
745         error = gpio_keys_setup_key(pdev, input, bdata, button);
746if(error)
747return error;
748
749if(button->wakeup)
750             wakeup =1;
751}
......
760   error = input_register_device(input);
761if(error){
762         dev_err(dev,"Unable to register input device, error: %d\n",
763             error);
764goto err_remove_group;
765}
......
774}
      第700行,调用gpio_keys_get_devtree_pdata函数从设备树中获取到KEY相关的设备节点信息。
第713行,使用devm_input_allocate_device函数申请input_dev。
      第726~735,初始化input_dev。
      第739行,设置input_dev事件,这里设置了EV_REP事件。
      第745行,调用gpio_keys_setup_key函数继续设置KEY,此函数会设置input_dev的EV_KEY事件已经事件码(也就是KEY模拟为哪个按键)。
      第760行,调用input_register_device函数向Linux系统注册input_dev。
      我们接下来再来看一下gpio_keys_setup_key函数,此函数内容如下:
示例代码58.5.1.3 gpio_keys_setup_key函数代码段
437staticint gpio_keys_setup_key(struct platform_device *pdev,
438struct input_dev *input,
439struct gpio_button_data *bdata,
440conststruct gpio_keys_button *button)
441{
442constchar*desc = button->desc ? button->desc :"gpio_keys";
443struct device *dev =&pdev->dev;
444   irq_handler_t isr;
445unsignedlong irqflags;
446int irq;
447int error;
448
449   bdata->input = input;
450   bdata->button = button;
451   spin_lock_init(&bdata->lock);
452
453if(gpio_is_valid(button->gpio)){
454
455         error = devm_gpio_request_one(&pdev->dev, button->gpio,
456                           GPIOF_IN, desc);
457if(error <0){
458             dev_err(dev,"Failed to request GPIO %d, error %d\n",
459               button->gpio, error);
460return error;
......
488         isr = gpio_keys_gpio_isr;
489         irqflags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;
490
491}else{
492if(!button->irq){
493             dev_err(dev,"No IRQ specified\n");
494return-EINVAL;
495}
496         bdata->irq = button->irq;
......
506
507         isr = gpio_keys_irq_isr;
508         irqflags =0;
509}
510
511   input_set_capability(input, button->type ?: EV_KEY,
button->code);
......
540return0;
541}
      第511行,调用input_set_capability函数设置EV_KEY事件以及KEY的按键类型,也就是KEY作为哪个按键?我们会在设备树里面设置指定的KEY作为哪个按键。
      一切都准备就绪以后剩下的就是等待按键按下,然后向Linux内核上报事件,事件上报是在gpio_keys_irq_isr函数中完成的,此函数内容如下:
示例代码58.5.1.4 gpio_keys_irq_isr函数代码段
392static irqreturn_t gpio_keys_irq_isr(int irq,void*dev_id)
393{
394struct gpio_button_data *bdata = dev_id;
395conststruct gpio_keys_button *button = bdata->button;
396struct input_dev *input = bdata->input;
397unsignedlong flags;
398
399   BUG_ON(irq != bdata->irq);
400
401   spin_lock_irqsave(&bdata->lock, flags);
402
403if(!bdata->key_pressed){
404if(bdata->button->wakeup)
405             pm_wakeup_event(bdata->input->dev.parent,0);
406
407         input_event(input, EV_KEY, button->code,1);
408         input_sync(input);
409
410if(!bdata->release_delay){
411             input_event(input, EV_KEY, button->code,0);
412             input_sync(input);
413goto out;
414}
415
416         bdata->key_pressed = true;
417}
418
419if(bdata->release_delay)
420         mod_timer(&bdata->release_timer,
421             jiffies + msecs_to_jiffies(bdata->release_delay));
422 out:
423   spin_unlock_irqrestore(&bdata->lock, flags);
424return IRQ_HANDLED;
425}
      gpio_keys_irq_isr是按键中断处理函数,第407行向Linux系统上报EV_KEY事件,表示按键按下。第408行使用input_sync函数向系统上报EV_REP同步事件。
      综上所述,Linux内核自带的gpio_keys.c驱动文件思路和我们前面编写的keyinput.c驱动文件基本一致。都是申请和初始化input_dev,设置事件,向Linux内核注册input_dev。最终在按键中断服务函数或者消抖定时器中断服务函数中上报事件和按键值。
58.5.2 自带按键驱动程序的使用
      要使用Linux内核自带的按键驱动程序很简单,只需要根据Documentation/devicetree/bindings/input/gpio-keys.txt这个文件在设备树中添加指定的设备节点即可,节点要求如下:
      ①、节点名字为“gpio-keys”。
      ②、gpio-keys节点的compatible属性值一定要设置为“gpio-keys”。
      ③、所有的KEY都是gpio-keys的子节点,每个子节点可以用如下属性描述自己:
      gpios:KEY所连接的GPIO信息。
      interrupts:KEY所使用GPIO中断信息,不是必须的,可以不写。
      label:KEY名字
      linux,code:KEY要模拟的按键,也就是示例代码58.1.2.4中的这些按键。
      ④、如果按键要支持连按的话要加入autorepeat。
      打开imx6ull-alientek-emmc.dts,根据上面的要求创建对应的设备节点,设备节点内容如下所示:
示例代码58.5.2.1 gpio-keys节点内容
1gpio-keys {
2      compatible ="gpio-keys";
3      #address-cells =<1>;
4      #size-cells =<0>;
5      autorepeat;
6      key0 {
7          label ="GPIO Key Enter";
8          linux,code =<KEY_ENTER>;
9          gpios =<&gpio1 18 GPIO_ACTIVE_LOW>;
10};
11};
      第5行,autorepeat表示按键支持连按。
      第6~10行,ALPHA开发板KEY按键信息,名字设置为“GPIO Key Enter”,这里我们将开发板上的KEY按键设置为“EKY_ENTER”这个按键,也就是回车键,效果和键盘上的回车键一样。后面学习LCD驱动的时候需要用到此按键,因为Linux内核设计的10分钟以后LCD关闭,也就是黑屏,就跟我们用电脑或者手机一样,一定时间以后关闭屏幕。这里将开发板上的KEY按键注册为回车键,当LCD黑屏以后直接按一下KEY按键即可唤醒屏幕,就跟当电脑熄屏以后按下回车键即可重新打开屏幕一样。
      最后设置KEY所使用的IO为GPIO1_IO18,一定要检查一下设备树看看此GPIO有没有被用到其他外设上,如果有的话要删除掉相关代码!
重新编译设备树,然后用新编译出来的imx6ull-alientek-emmc.dtb启动Linux系统,系统启动以后查看/dev/input目录,看看都有哪些文件,结果如图58.5.2.1所示:

图58.5.2.1 /dev/input目录文件
      从图58.5.2.1可以看出存在event1这个文件,这个文件就是KEY对应的设备文件,使用hexdump命令来查看/dev/input/event1文件,输入如下命令:
hexdump /dev/input/event1
      然后按下ALPHA开发板上的按键,终端输出图58.5.2.2所示内容:

图58.5.2.2 按键信息
      如果按下KEY按键以后会在终端上输出图58.5.2.2所示的信息那么就表示Linux内核的按键驱动工作正常。至于图58.5.2.2中内容的含义大家就自行分析,这个已经在58.4.2小节详细的分析过了,这里就不再讲解了。
      大家如果发现按下KEY按键以后没有反应,那么请检查一下三方面:
      ①、是否使能Linux内核KEY驱动。
      ②、设备树中gpio-keys节点是否创建成功。
      ③、在设备树中是否有其他外设也使用了KEY按键对应的GPIO,但是我们并没有删除掉这些外设信息。检查Linux启动log信息,看看是否有类似下面这条信息:
gpio-keys gpio_keys:Failedtorequest GPIO 18, error -16
      上述信息表示GPIO 18申请失败,失败的原因就是有其他的外设正在使用此GPIO。

armok. 发表于 2020-7-7 10:29:34

打赏!

庆祝论坛“打赏”功能实施, 现在开始发技术主题,可以获得打赏
https://www.amobbs.com/thread-5735948-1-1.html
页: [1]
查看完整版本: 【正点原子Linux连载】第五十八章Linux INPUT子系统实验--摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南