58 input 子系统

一、input子系统

1、input子系统简介

在这里插入图片描述
内核空间里面分为 3 部分。
驱动层 :输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。
核心层 :承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行处理。其实就是 drivers/input/input.c
事件层 :主要和用户空间进行交互。

本次实验 驱动的主要内容 是完成驱动层像核心层上报事件的过程;测试app的主要内容 是 从设备节点文件中读取 struct input_event 结构体,并解析出对应的事件。

input子系统也是字符设备。只是主设备号均为13。在字符设备的基础上再封装成 input_dev 设备。
input核心层会帮我们注册input字符设备驱动。drivers/input/input.c 这个文件 就是 input 输入子系统的核心层。

初始化 key_io;设置中断,计时器;input_dev 结构体。
设备号、cdev、class、device就不需要再去管了。
在自定义的设备结构体里面加入一个 struct input_dev 设备。此结构体定义在 include/linux/input.h 文件中。

内核已经写好了input驱动,我们只需要去完善具体的输入设备,完善输入设备的时候需要按照 input子系统驱动框架 的要求来。最终给用户空间提供可访问的设备节点(/dev/input/xxx)(测试app是否用的就是这个文件?)

2、编写驱动的主要步骤

1、使用 struct input_dev *input_allocate_device(void) 向内核申请一个 struct input_dev 变量。

2、初始化 struct input_dev。本章按键初始化的成员变量有:name,evbit,keybit。对于后两个位图变量的初始化有 专门的 api

3、int input_register_device(struct input_dev *dev) 向内核注册这个 struct input_dev 变量。

4、在中断处理函数或者定时器处理函数中向核心层上报事件:
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)

4、模块出口函数中 先释放 gpio,在调用
void input_unregister_device(struct input_dev *dev)
来注销掉前面注册的 struct input_dev
再调用 void input_free_device(struct input_dev *dev) 来释放掉前面申请到的 struct input_dev

注意:
设备号、cdev、class、device就不需要再去管了。
我们只需要处理好 io ,struct input_dev 相关即可。

3、相关的内核结构体

struct input_dev 表示一个 input 设备,定义在 include/linux/input.h

struct input_dev {
	const char *name;
	...
	/* 以下是各种位图*/
	unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
	/* 事件类型的位图, 上报不同的类型的事件,要初始化相关的位图 */
	unsigned long evbit[BITS_TO_LONGS(EV_CNT)];
	 /* 按键值的位图 */
	unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];
	 /* relative axis 相对坐标的位图 */
	unsigned long relbit[BITS_TO_LONGS(REL_CNT)];
	/* 绝对坐标的位图 */
	unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];
	/* 杂项事件的位图 */
	unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];
	unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];
	unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];
	unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
	unsigned long swbit[BITS_TO_LONGS(SW_CNT)];
	/* 注意 BITS_TO_LONGS 是位图相关操作的一个宏,相关定义涉及 
	 * bitops.h, mroute6.h
	 */
	...
}

evbit 表示输入事件类型,可选的事件类型定义在 include/uapi/linux/input.h 。要上报某种事件,要先初始化结构体事件类型的位图 evbit。若有必要还要初始化对应的事件码位图。下面将以按键为例介绍。

#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 /* 压力状态事件 */

struct input_event 在内核中表示所有的输入事件。定义在 include/uapi/linux/input.h
所有的输入设备最终都是按照 struct input_event 呈现给用户的;
用户应用程序可以通过 struct input_event 来获取到具体的输入事件或相关的值。

struct input_event {
	/* 此事件发生的时间,详见下 */
	struct timeval time;
	/* 事件类型,比如 EV_KEY,表示此次事件为按键事件。16bit */
	__u16 type;
	/* 事件码,比如在 EV_KEY 事件中 code 就表示具体的按键码,
	 * 如: KEY_0、 KEY_1等等这些按键。16bit 
	 */
	__u16 code;
	/* 值,比如 EV_KEY 事件中 value 就是按键值,表示按键有没有
	 * 被按下,如果为 1 的话说明按键按下,如果为 0 的话说明按键
	 * 没有被按下或者按键松开了。这是由开发者自己规定的。32bit
	 */
	__s32 value;
};

struct timeval,定义在 time.h
表示从1970-1-1 0:0:0开始计时,所经历的秒数和微秒数。注意是二者合起来才是经历的时间。

typedef long __kernel_long_t;
typedef __kernel_long_t __kernel_time_t;
typedef __kernel_long_t __kernel_suseconds_t;

struct timeval {
	__kernel_time_t tv_sec; 		/* 秒 */
	__kernel_suseconds_t tv_usec; 	/* 微秒 */
};

4、内核提供的相关api

申请一个 input_dev 结构体。
struct input_dev *input_allocate_device(void)
参数:无。
返回值: 
	成功:申请到的 input_dev。
	失败:NULL
释放 input_dev。注意先 unregister 再 free。
void input_free_device(struct input_dev *dev)
dev:需要释放的 input_dev。
返回值: 无。
向 Linux 内核注册 input_dev,初始化之完成后调用此函数。
int input_register_device(struct input_dev *dev)
dev:要注册的 input_dev 。
返回值: 
	0, input_dev 注册成功;
	负值, input_dev 注册失败。
注销 input_dev。
void input_unregister_device(struct input_dev *dev)
dev:要注销的 input_dev 。
返回值: 无。
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_report_key,input_report_rel,input_sync 都是对 input_event 的在封装。
用于上传特定类型的事件。
这些封装后的api 定义在 input.h。
通知 Linux 内核 input 子系统上报结束,此函数其实是对 input_event 函数的再封装
void input_sync(struct input_dev *dev)
dev:需要上报同步事件的 input_dev。
返回值: 无。

二、开发流程

input 核心层会向 Linux 内核注册一个字符设备,大家找到 drivers/input/input.c 这个文件,input.c 就是 input 输入子系统 的核心层,此文件里面有如下所示代码:

struct class input_class = {
	/*  这个 name 和 /sys/class 目录下的 input 相对应 */
	.name = "input",
	.devnode = input_devnode,
};
......
static int __init input_init(void)
{
	int err;

	err = class_register(&input_class);
	if (err) {
		pr_err("unable to register input_dev class\n");
		return err;
	}

	err = input_proc_init();
	if (err)
		goto fail1;

	err = register_chrdev_region( MKDEV(INPUT_MAJOR, 0),
				INPUT_MAX_CHAR_DEVICES, "input");
	if (err) {
		pr_err("unable to register char major %d", INPUT_MAJOR);
		goto fail2;
	}

	return 0;

fail2: 
	input_proc_exit();
fail1: 
	class_unregister(&input_class);
	return err;
}

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

在这里插入图片描述
第 20~25行,注册一个字符设备,主设备号为 INPUT_MAJOR, INPUT_MAJOR 定义在 include/uapi/linux/major.h 文件中,定义如下:

#define INPUT_MAJOR 13

因此, input 子系统的所有设备主设备号都为 13,我们在使用 input 子系统处理输入设备的时候就不需要去注册字符设备了,我们只需要向系统注册一个 input_device 即可。 ls -al xxx 可以看到 major 为 13。

综上所述, 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 注册过程示例代码如下所示:

struct input_dev *inputdev; /* input 结构体变量 */

/* 驱动入口函数 
 * 在此函数中完成 input_dev 的申请、设置、注册等工作
 */
static int __init xxx_init(void)
{
	......
	/* 申请一个 input_dev。*/
	inputdev = input_allocate_device(); /* 申请 input_dev */
	inputdev->name = "test_inputdev"; /* 设置 input_dev 名字 */

	/* 下面用了三种方法来设置事件和按键值 */
	/*********第一种设置事件和事件值的方法***********/
	__set_bit(EV_KEY, inputdev->evbit); /* 设置产生按键事件 */
	__set_bit(EV_REP, inputdev->evbit); /* 重复事件 */
	__set_bit(KEY_0, inputdev->keybit); /*设置产生哪些按键值 */
	/************************************************/

	/*********第二种设置事件和事件值的方法***********/
	keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
	keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |=
	BIT_MASK(KEY_0);
	/************************************************/

	/*********第三种设置事件和事件值的方法***********/
	keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
	input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
	/************************************************/

	/* 向内核注册 input_dev */
	input_register_device(inputdev);
	......
	return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
	input_unregister_device(inputdev); /* 注销 input_dev */
	input_free_device(inputdev); /* 删除 input_dev */
}

向 内核注册好 input_dev 以后,我们需要获取到具体的 输入事件或者输入值 ,然后将输入事件上报给内核。
比如按键,我们需要在按键中断处理函数或者消抖定时器中断函数中将按键值上报给内核,这样内核才能获取到正确的输入值。

当我们上报事件以后还需要使用 input_sync 函数来告诉 Linux 内核 input 子系统上报结束;
input_sync 函数本质是上报一个同步事件,此函数原型如下所示:

按键的上报事件的参考代码如下所示:

/* 用于按键消抖的定时器服务函数 */
void timer_function(unsigned long arg)
{
	unsigned char value;

	value = gpio_get_value(keydesc->gpio); /* 读取 IO 值 */
	if(value == 0){ /* 按下按键 */
		/* 上报按键值 */
 		input_report_key(inputdev, KEY_0, 1); /* 最后一个参数 1, 按下 */
		input_sync(inputdev); /* 同步事件 */
	} else { /* 按键松开 */
		input_report_key(inputdev, KEY_0, 0); /* 最后一个参数 0, 松开 */
		input_sync(inputdev); /* 同步事件 */
	}
}

三、驱动例程

初始化 key_io;设置中断,计时器;input_dev 结构体。
设备号、cdev、class、device就不需要再去管了。

#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/init.h>
#include <linux/fs.h>
#include<linux/slab.h>
#include<linux/io.h>
#include<linux/uaccess.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<linux/of.h>
#include<linux/of_address.h>
#include<linux/of_irq.h>
#include<linux/gpio.h>
#include<linux/of_gpio.h>
#include<linux/atomic.h>
#include<linux/timer.h>
#include<linux/string.h>
#include<linux/jiffies.h>
#include<linux/irq.h>
#include<asm/mach/map.h>
#include<asm/uaccess.h>
#include<asm/io.h>
#include<linux/interrupt.h>
#include<linux/input.h>


#define     KEYINPUT_CNT        1
#define     KEYINPUT_NAME       "keyinput"
#define     KEY0_ACTIVE         1
#define     KEY0_INACTIVE       2


// 按键结构体
struct irq_keydesc
{
    int gpio;   	//gpio号
    int irqnum; 	//中断号
    u8 value;   	//键值
    char name[8];	//名字
    irqreturn_t (*handler)(int, void *);//中断处理函数
};


// 自定义input设备结构体
struct keyinput_dev
{
/*  使用 input子系统 不再需要下面这些成员变量,input子系统核心层
	帮我们做好了。
    dev_t devid;
    u32 major;
    u32 minor;
    struct cdev cdev;
    struct class *class;
    struct device *device;
*/
    struct device_node *nd;
    /* 表示一个定时器 */
    struct timer_list timer;
    int timer_period;
    struct irq_keydesc irqkey;
	/* input_dev 内嵌在 自定设备类型结构体中 */
    struct input_dev *inputdev;
};

struct keyinput_dev keyinputdev;


/* 按键中断处理函数
 * 第二个参数与 rquest_irq 的最后一个参数一致
 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{  
    struct keyinput_dev *dev = dev_id;
    mod_timer(&dev->timer, jiffies+msecs_to_jiffies(10));
    
    return IRQ_HANDLED;
}

// 按键io 的初始化
static int keyio_init(struct keyinput_dev *dev)
{
    int ret = 0;
 
   	/* 1.初始化 io相关 */
    dev->nd = of_find_node_by_path("/gpiokey");
    if(dev->nd == NULL)
    {
        ret = -1;
        printk("%s(%d):fail\n", __FILE__, __LINE__);
        goto fail_nd;
    }

  
	dev->irqkey.gpio = of_get_named_gpio(dev->nd, "key-gpios", 0);
	if(dev->irqkey.gpio < 0)
	{
		ret = -1;
		printk("%s(%d):fail\n", __FILE__, __LINE__);
   		goto fail_gpio;
	}


	memset(dev->irqkey.name, 0, sizeof(dev->irqkey.name));
	sprintf(dev->irqkey.name, "key_%d", 0);
	/* 第二个参数 label 可以自定义 */
	ret = gpio_request(dev->irqkey.gpio, dev->irqkey.name);
	if(ret)
	{
		ret = -1;
		printk("%s(%d):fail\n", __FILE__, __LINE__);
		goto fail_request;
	}
	
	ret = gpio_direction_input(dev->irqkey.gpio);
	if(ret) {
		ret = -1;
		printk("%s(%d):fail\n", __FILE__, __LINE__);
		goto fail_setdir;
	}

	/* 2、中断相关 */
#if 1
	dev->irqkey.irqnum = gpio_to_irq(dev->irqkey.gpio);
#else
	dev->irqkey.irqnum = irq_of_parse_and_map(dev->nd, 0);
#endif
	dev->irqkey.handler = key0_handler;
	dev->irqkey.value = KEY0_ACTIVE;
   	/* 
	 * 注意 request_irq 的最后一个参数要和 free_irq 的最后一个参数一致。
	 */
	ret = request_irq(dev->irqkey.irqnum, 
                            dev->irqkey.handler,
                            IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,
                            dev->irqkey.name,
                            dev );
	if(ret)
	{
		ret = -1;
		printk("irq %d request failed\n", dev->irqkey.irqnum);
		goto fail_irq;
	}

	return 0;

fail_irq:
	free_irq(dev->irqkey.irqnum, dev);
fail_setdir:
    gpio_free(dev->irqkey.gpio);
fail_request:
fail_gpio:
fail_nd:
    return ret;
}

// 定时器超时函数
static void timer_func(unsigned long arg)
{
    int value = 0;
    struct keyinput_dev *dev = (void *)arg;

    value = gpio_get_value(dev->irqkey.gpio);
    /*
     * 注意要是上报了未初始化的事件类型或者事件码,内核是不会接收的。
     * hexdump 和 测试app 都无效
     */
    if(value == 0) 
    {
        /* 上报 按键值,按下值为 1 */
        printk("%s(%d):KEY0 PUSHED \n", __FILE__, __LINE__);
        input_event(dev->inputdev, EV_KEY, KEY_0, 1);
        input_sync(dev->inputdev);
    }
    else if(value == 1)
    {
        /* 上报 按键值 */
        printk("%s(%d):KEY0 RELEASED \n", __FILE__, __LINE__);
        input_event(dev->inputdev, EV_KEY, KEY_0, 0);
        input_sync(dev->inputdev);
    }
}

static int __init keyinput_init(void)
{
    int ret = 0;
    printk("%s(%d):\n", __FILE__, __LINE__);
    /* 设备号,cdev,class,device 都不再需要我们去初始化
     * input 核心层初始化好了
     */

	/* 1、处理 io,io中断相关 */
    ret = keyio_init(&keyinputdev);
    if(ret < 0)
    {
    	printk("%s(%d):fail\n", __FILE__, __LINE__);
        goto fail_keyinit;
    }
    
	/* 2、处理定时器相关 */
    init_timer(&keyinputdev.timer);
    keyinputdev.timer.function = timer_func;
    /* 设置超时处理函数的参数 */
    keyinputdev.timer.data = (u32 )&keyinputdev;

	/* 3、处理 input_dev 相关 */
    /*申请 input_dev*/
    keyinputdev.inputdev = input_allocate_device();
    if(keyinputdev.inputdev == NULL)
    {
        ret = -1;
        printk("%s(%d):fail\n", __FILE__, __LINE__);
        goto fail_alloc;
    }

    /* 初始化 input_dev */
    keyinputdev.inputdev->name = KEYINPUT_NAME;
    /* 设置按键事件,注意若是上报了 未在此处注册的事件或者事件值
     * 那么内核无法接收这些未注册的事件或者事件值
     */
    __set_bit(EV_KEY, keyinputdev.inputdev->evbit);
    /* 设置重复事件,支持按键连按 */
    __set_bit(EV_REP, keyinputdev.inputdev->evbit);
    /* 设置按键值,KEY_0 的值为 11*/
    __set_bit(KEY_0, keyinputdev.inputdev->keybit);

    /* 注册 input_dev */
    ret = input_register_device(keyinputdev.inputdev);
    if(ret)
    {
    	printk("%s(%d):fail\n", __FILE__, __LINE__);
        goto fail_register;
    }

    return 0;

fail_register:
    input_free_device(keyinputdev.inputdev);
fail_alloc:
	free_irq(keyinputdev.irqkey.irqnum, &keyinputdev);
	gpio_free(keyinputdev.irqkey.gpio);
fail_keyinit:
    printk("%s(%d):fail\n", __FILE__, __LINE__);
    return ret;

}

static void __exit keyinput_exit(void)
{
    printk("%s(%d):%s\n", __FILE__, __LINE__, __func__);
    /* 释放中断
	 * 注意 free_irq 的最后一个参数要和 前面的 request_irq
	 * 的最后一个参数一致
	 */
	free_irq(keyinputdev.irqkey.irqnum, &keyinputdev);
    // 释放 io
	gpio_free(keyinputdev.irqkey.gpio);
	/* 删除定时器 */
    del_timer_sync(&keyinputdev.timer);
   
    /* 注意先 unregister,再 free */
    input_unregister_device(keyinputdev.inputdev);
    input_free_device(keyinputdev.inputdev);
}

module_init(keyinput_init);
module_exit(keyinput_exit);
MODULE_LICENSE("GPL");

在卸载驱动时,出现以下错误提示:

Trying to free already-free IRQ 49

表示中断没有释放成功,中断申请函数 request_irq( ) 与中断释放函数 free_irq( ) 的最后一个参数(void *dev 设备结构体)要保持一致,必须是同一个指针。

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)void free_irq(unsigned int irq, void *dev)

以上两个函数,在使用的时候,最后一个参数必须保持一致,问题解决。

四、应用程序源码

按键驱动对应的文件就是 /dev/input/event1
应用程序读取 /dev/input/event1 来的到按键信息(就是 input_event 结构体),也就是按键是否按下。
一般键码都使用 BTN_X,KEY_X 都是用来模拟键盘上的按键的

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"
#include<linux/input.h>

static struct input_event inputevent;

int main(int argc, char *argv[])
{
	int fd;
	int ret = 0;
	char *filename;
	
	if (argc != 2) {
		printf("APP:Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	fd = open(filename, O_RDWR);
	if (fd < 0) {
		printf("APP:Can't open file %s\r\n", filename);
		return -1;
	}
    
	while(1)
	{
		/* 读写正确的情况下返回16,是此结构体的字节数 */
		ret = read(fd, &inputevent, sizeof(inputevent));
		if(ret > 0)
		{
			/*input_event 结构体读取成功*/
			switch(inputevent.type)
			{
				case EV_KEY:
					printf("%s(%d):EV_KEY event triggered.\n",
							__FILE__, __LINE__);
					break;
				case EV_SYN:
					printf("%s(%d):EV_SYN event triggered.\n",
							__FILE__, __LINE__);
					break;
				default:
					break;
			}
		}
		else
		{
			printf("APP:read data fail.\n");
		}
	}
	return ret;
}

五、编译测试

  • 查看input_event结构体原始数据
/lib/modules/4.1.15 # 
/lib/modules/4.1.15 # ls /dev/input/
event0  mice
/lib/modules/4.1.15 # 
/lib/modules/4.1.15 # 
/lib/modules/4.1.15 # 
/lib/modules/4.1.15 # modprobe keyinput.ko 
/home/jl/linux/imx6ull/linux_driver/21_input/keyinput.c(253):
input: keyinput as /devices/virtual/input/input3
/lib/modules/4.1.15 # 
/lib/modules/4.1.15 # 
/lib/modules/4.1.15 # ls /dev/input/
event0  event1  mice
加载驱动模块后出现 event1
/lib/modules/4.1.15 # 
/lib/modules/4.1.15 # 
/lib/modules/4.1.15 # 

# hexdump 出来的的十六进制序列其实就是 input_event 结构体的二进制表示
编号8 	   秒8 		微秒8 事件类型4 事件码4 事件值8
/lib/modules/4.1.15 # hexdump /dev/input/event1
 /home/jl/linux/imx6ull/linux_driver/21_input/keyinput.c(188):KEY0 PUSHED
# 事件类型为1,表示 EV_KEY事件,KEY_0 按键:11,1表示按下 
0000080, 06d9 0000, 28ad 000e, 0001, 000b, 0001 0000
# 事件类型为0,表示 EV_SYN事件
0000090, 06d9 0000, 28ad 000e, 0000, 0000, 0000 0000
/home/jl/linux/imx6ull/linux_driver/21_input/keyinput.c(195):KEY0 RELEASED 
# 事件类型为1,表示 EV_KEY事件,KEY_0 按键:11,0表示释放 
00000a0, 06da 0000, 09f7 0001, 0001, 000b, 0000 0000
# 事件类型为0,表示 EV_SYN事件
00000b0, 06da 0000, 09f7 0001, 0000, 0000, 0000 0000
/home/jl/linux/imx6ull/linux_driver/21_input/keyinput.c(188):KEY0 PUSHED 
00000c0 06da 0000 05cf 000c 0001 000b 0001 0000
00000d0 06da 0000 05cf 000c 0000 0000 0000 0000
/home/jl/linux/imx6ull/linux_driver/21_input/keyinput.c(195):KEY0 RELEASED 
00000e0 06da 0000 b424 000d 0001 000b 0000 0000
00000f0 06da 0000 b424 000d 0000 0000 0000 0000

六、linux kernel自带的按键驱动程序

  • 若要使用内核自带的按键驱动,需要按照绑定文档来添加一个设备节点,如下:
gpio-keys {
		compatible = "gpio_keys" ;
		pinctrl-names = "default";
		#address-cells = <1>;
		#size-cells = <0>;
		pinctrl-0 = <&pinctrl_gpiokey>;
		status = "okay";
		interrupt-parent = <&gpio1>;
		interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
		key0 {
			label = "GPIO ENTER KEY";
			/* 在此处定义按键对应的键值 */
			linux,code = <KEY_ENTER>;
			gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
		};
	};

按键定义为 enter 键可以唤醒lcd屏幕,在下讲中测试。

若以后不需要使用内核自带的led驱动了,也不需要删除这个设备树节点,只需要将属性 status 的 值 改为 "disabled" 即可。

Linux 内核也自带了 KEY 驱动,如果要使用内核自带的 KEY 驱动的话需要配置内核,不过内核一般默认已经使能了 KEY 驱动,但是我们还是要检查一下。按照如下路径找到相应的配置选项:

-> Device Drivers
	-> Generic input layer (needed for keyboard, mouse, ...) (INPUT [=y])
		-> Keyboards (INPUT_KEYBOARD [=y])
			->GPIO Buttons

选中“GPIO Buttons”选项,将其编译进 Linux 内核中,如下图 所示
在这里插入图片描述
选中以后就会在.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 文件中找到如下所示内容:

static const struct of_device_id gpio_keys_of_match[] = 
{
	{ .compatible = "gpio-keys", },
	{ },
};
......
static struct platform_driver gpio_keys_device_driver = 
{
	.probe = gpio_keys_probe,
	.remove = gpio_keys_remove,
	.driver = 
	{
		.name = "gpio-keys",
		.pm = &gpio_keys_pm_ops,
		.of_match_table = of_match_ptr(gpio_keys_of_match),
	}
};

static int __init gpio_keys_init(void)
{
	return platform_driver_register(&gpio_keys_device_driver);
}

static void __exit gpio_keys_exit(void)
{
	platform_driver_unregister(&gpio_keys_device_driver);
}

从上面的示例代码 可以看出,这就是一个标准的 platform 驱动框架,如果要使用设备树来描述 KEY 设备信息的话,设备节点的 compatible 属性值要设置为“gpio-keys”。当设备和驱动匹配以后 gpio_keys_probe 函数就会执行, gpio_keys_probe 函数内容如下(为了篇幅有缩减):

static int gpio_keys_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);
	struct gpio_keys_drvdata *ddata;
	struct input_dev *input;
	size_t size;
	int i, error;
	int wakeup = 0;

	if (!pdata) {
		/* 从设备树中获取到 KEY 相关的设备节点信息。 */
		pdata = gpio_keys_get_devtree_pdata(dev);
		if (IS_ERR(pdata))
			return PTR_ERR(pdata);
	}
	......
	/* 申请一个input_dev。devm_ 开头的申请使用完不需要手动
	 * 手动去释放。
	 */
	input = devm_input_allocate_device(dev);
	if (!input) {
		dev_err(dev, "failed to allocate input device\n");
		return -ENOMEM;
	}

	ddata->pdata = pdata;
	ddata->input = input;
	mutex_init(&ddata->disable_lock);

	platform_set_drvdata(pdev, ddata);
	input_set_drvdata(input, ddata);
	/* 初始化 input_dev */
	input->name = pdata->name ? : pdev->name;
	input->phys = "gpio-keys/input0";
	input->dev.parent = &pdev->dev;
	input->open = gpio_keys_open;
	input->close = gpio_keys_close;

	input->id.bustype = BUS_HOST;
	input->id.vendor = 0x0001;
	input->id.product = 0x0001;
	input->id.version = 0x0100;

	/* Enable auto repeat feature of Linux input subsystem */
	if (pdata->rep)
		__set_bit(EV_REP, input->evbit); /* 设置了 EV_REP 事件 */

	for (i = 0; i < pdata->nbuttons; i++) {
		const struct gpio_keys_button *button = &pdata->buttons[i];
		struct gpio_button_data *bdata = &ddata->data[i];

		/*设置 input_dev 的 EV_KEY 事件已经事件码 */
		error = gpio_keys_setup_key(pdev, input, bdata, button);
		if (error)
			return error;

		if (button->wakeup)
			wakeup = 1;
	}
	......
	/* 注册 input_dev */
	error = input_register_device(input);
	if (error) {
		dev_err(dev, "Unable to register input device, error: %d\n",error);
		goto err_remove_group;
	}
	......
}

我们接下来再来看一下 gpio_keys_setup_key 函数,此函数内容如下:

static int gpio_keys_setup_key(struct platform_device *pdev,
								struct input_dev *input,
								struct gpio_button_data *bdata,
								const struct gpio_keys_button *button)
{
	const char *desc = button->desc ? button->desc : "gpio_keys";
	struct device *dev = &pdev->dev;
	irq_handler_t isr;
	unsigned long irqflags;
	int irq;
	int error;

	bdata->input = input;
	bdata->button = button;
	spin_lock_init(&bdata->lock);

	if (gpio_is_valid(button->gpio)) {
		error = devm_gpio_request_one(&pdev->dev, button->gpio, GPIOF_IN, desc);
		if (error < 0) {
			dev_err(dev, "Failed to request GPIO %d, error %d\n",
						button->gpio, error);
			return error;
			......
			isr = gpio_keys_gpio_isr;
			irqflags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;

		} else {
			if (!button->irq) {
				dev_err(dev, "No IRQ specified\n");
				return -EINVAL;
			}
			bdata->irq = button->irq;
			......

			isr = gpio_keys_irq_isr;
			irqflags = 0;
		}

	/* 设置 EV_KEY 事件以及 KEY 的按键类型 */
	input_set_capability(input, button->type ?: EV_KEY, button->code);
	......
	return 0;
}

一切都准备就绪以后剩下的就是等待按键按下,然后向 Linux 内核上报事件,事件上报是在 gpio_keys_irq_isr 函数中完成的,此函数内容如下:

static irqreturn_t gpio_keys_irq_isr(int irq, void *dev_id)
{
	struct gpio_button_data *bdata = dev_id;
	const struct gpio_keys_button *button = bdata->button;
	struct input_dev *input = bdata->input;
	unsigned long flags;

	BUG_ON(irq != bdata->irq);

	spin_lock_irqsave(&bdata->lock, flags);

	if (!bdata->key_pressed) {
		if (bdata->button->wakeup)
			pm_wakeup_event(bdata->input->dev.parent, 0);

		/* 上报 EV_KEY 事件,表示按键按下 */
		input_event(input, EV_KEY, button->code, 1);
		/* 上报 EV_REP 同步事件 */
		input_sync(input);

		if (!bdata->release_delay) {
			input_event(input, EV_KEY, button->code, 0);
			input_sync(input);
			goto out;
		}

		bdata->key_pressed = true;
	}

	if (bdata->release_delay)
		mod_timer(&bdata->release_timer,
					jiffies + msecs_to_jiffies(bdata->release_delay));
out:
	spin_unlock_irqrestore(&bdata->lock, flags);
	return IRQ_HANDLED;
}

综上所述, Linux 内核自带的 gpio_keys.c 驱动文件思路和我们前面编写的 keyinput.c 驱动文件基本一致:都是申请和初始化 input_dev,设置事件,向 Linux 内核注册 input_dev。最终在按键中断服务函数或者消抖定时器中断服务函数中上报事件和按键值。

要 使 用 Linux 内 核 自 带 的 按 键 驱 动 程 序 很 简 单 , 只 需 要 根 据 Documentation/devicetree/bindings/input/gpio-keys.txt 这个文件在设备树中添加指定的设备节点即可,节点要求如下:

1、节点名字为“gpio-keys”。
2、 gpio-keys 节点的 compatible 属性值一定要设置为“gpio-keys”。
3、所有的 KEY 都是 gpio-keys 的子节点,每个子节点可以用如下属性描述自己:
			gpios: KEY 所连接的 GPIO 信息。
			interrupts: KEY 所使用 GPIO 中断信息,不是必须的,可以不写。
			label: KEY 名字
			linux,code: KEY 要模拟的按键,也就是示例代码 58.1.2.4 中的这些按键。
4、如果按键要支持连按的话要加入 autorepeat。

打开 imx6ull-alientek-emmc.dts,根据上面的要求创建对应的设备节点,设备节点内容如下所示:

gpio-keys 
{
	/*这个值一定要和 bindings 文档一致*/
	compatible = "gpio-keys";
	#address-cells = <1>;
	#size-cells = <0>;
	/*表示支持按键连按*/
	autorepeat;
	key0 
	{
		label = "GPIO Key Enter";
		/*设置为ENTER键码28,按下后能读到为1c*/
		linux,code = <KEY_ENTER>;
		/* 必须要设置为 GPIO_ACTIVE_LOW ,因为按键按下这个gpio才是低电平
			否则 hexdump /dev/eventX 会一直有 按键enter 的输入 */ 
		gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
	};
};

第 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 可以看出存在 event1 这个文件,这个文件就是 KEY 对应的设备文件,使用 hexdump 命令来查看/dev/input/event1 文件,输入如下命令:

hexdump /dev/input/event1

然后按下 ALPHA 开发板上的按键,终端输出图 58.5.2.2 所示内容:
下图中的 001c 对应的就是 enter 键码
在这里插入图片描述

如果按下 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: Failed to request GPIO 18, error -16

上述信息表示 GPIO 18 申请失败,失败的原因就是有其他的外设正在使用此 GPIO。

好的,这里为你提供一个Python实现的可登录的简易点餐系统的实现代码: ```python # 定义管理员账号密码 admin_username = 'root' admin_password = 'admin123' # 定义菜单 menu = { 1: {'name': '烤鸭', 'price': 98}, 2: {'name': '水煮牛肉', 'price': 68}, 3: {'name': '宫保鸡丁', 'price': 58}, 4: {'name': '鱼香肉丝', 'price': 48}, 5: {'name': '红烧肉', 'price': 38}, } # 定义订单列表 order = [] # 显示菜单 def show_menu(): print('菜单如下:') for k, v in menu.items(): print(k, v['name'], v['price'], '元') # 点餐 def order_dish(): while True: dish_no = input('请输入菜品编号(输入0结束点餐):') if dish_no == '0': break if int(dish_no) not in menu: print('该菜品不存在,请重新输入!') continue dish_count = input('请输入菜品数量:') dish_name = menu[int(dish_no)]['name'] dish_price = menu[int(dish_no)]['price'] order.append({'name': dish_name, 'price': dish_price, 'count': int(dish_count)}) print('您已成功点餐:', dish_name, dish_price, '元 x', dish_count) # 查询订单 def query_order(): print('您的订单如下:') for dish in order: print(dish['name'], dish['price'], '元 x', dish['count']) # 管理员子系统 def admin_system(): print('欢迎进入管理员子系统!') # TODO: 实现添加、修改、删除、查询菜品、查询和取消点餐订单和返回上级菜单功能 pass # 登录页面 def login(): for i in range(3): role = input('请选择角色(1.管理员,2.客户):') if role == '1': username = input('请输入账号:') password = input('请输入密码:') if username == admin_username and password == admin_password: print('管理员登录成功!') admin_system() break else: print('账号或密码错误!') elif role == '2': print('客户登录成功!') show_menu() order_dish() query_order() break else: print('角色选择错误,请重新选择!') else: print('登录失败!') # 主程序 while True: print('欢迎使用点餐系统!') login() cont = input('是否继续使用点餐系统(Y/N):') if cont.upper() != 'Y': break print('感谢使用点餐系统!') ``` 这个点餐系统实现了管理员登录、显示菜单、点餐、订单查询等功能。你可以根据需要进行修改和扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值