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小节详细的分析过了,这里就不再讲解了。