您的位置:首页 > 其它

基于input子系统的驱动分析

2016-09-06 14:24 197 查看

基于input子系统的驱动分析

input子系统本质多个模块构成的一个体系,用来实现:对鼠标、键盘、按键这类输入设备驱动的管理。这里所谓的输入设备,是狭义上的,仅仅指人机交互用的输入设备。传感器这种输入设备不属于该范围。

下图是input子系统的结构



值得注意的是,虽然事件处理层主要有event类型的、mouse类型的、key类型的,但是event类型兼容所有的输入设备,并有取代其他类型的趋势。可以认为,所有的设备在/dev/input下,都是以event*命名的

下图是系统工作的具体流程



1.事件处理层分析

事件处理层负责接受驱动层的汇报上来的数据,然后打包成标准格式再给应用层。此外设备文件也是事件处理层负责创建的。

应用层可以打开/dev/input/eventx,即可读取到标准格式的输入事件包struct input_event

struct input_event是一个标准的格式包,由事件处理层创建并传递给应用层。当一个输入事件发生时,会发送多个input_event,形成一帧数据。下面是
<linux/input.h>
中定义的input_event原型。

struct input_event {
struct timeval time;//表示输入事件发生时间点
__u16 type;//输入事件类型
__u16 code;//输入事件编码值
__s32 value;//操作值
};


input_event内元素值对应的含义,在
<linux/input.h>
中定义

2.核心层(input.c)分析

input.c中是input设备框架,它是一个模块,该模块加载函数做的事情非常典型,主要还是注册类和注册设备

/*该模块加载函数做的事情非常典型,主要还是注册类和注册设备*/
static int __init input_init(void)
{
/*无关代码就不贴了*/
...

/*注册了一个叫input的类*/
err = class_register(&input_class);

/*无关代码就不贴了*/
...

/*注册input设备,所有的input设备的主设备号都为13*/
err = register_chrdev(INPUT_MAJOR, "input", &input_fops);

/*无关代码就不贴了*/
...
}


所有的input设备公用一个主设备号(都是13),它们之间以次设备号互相区分。所以在框架中使用register_chrdev注册了一个主设备号为13的设备,而在事件处理层中创建设备文件主设备号都为13,次设备号不同

3.驱动分析

下图是驱动的具体流程

其本质是:驱动只需通过上报读到的硬件状态给事件处理层,其他的都由事件处理层负责。应用层只需读取/dev/input/event*即可



下面是具体的源码,以最常见的按键驱动为例

该驱动基于platform总线,相关知识详见platform总线驱动详尽分析

此外还用到了中断,相关知识详见内核的中断机制

源码:

#include <linux/input.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/platform_device.h>
#include <plat/gpio-cfg.h>
#include <mach/regs-gpio.h>
#include <mach/irqs.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/bitops.h>

#define BUTTON_IRQL     IRQ_EINT2
#define BUTTON_IRQD     IRQ_EINT3

/*“输入设备功能”相关的宏。话说这里有7个按键功能,我们只用了2个*/
#define MAX_BUTTON_CNT      (7)
static int s3c_Keycode[MAX_BUTTON_CNT] = {KEY_POWER, KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_A, KEY_B};

static struct input_dev *button_dev;

/*多个外部中断共用一个中断处理函数,通过传参来确定底层信息*/
static irqreturn_t button_interrupt(int irq, void *gpio)
{
int val = -1;
int code = -1;
/*关中断*/
s3c_gpio_cfgpin((unsigned)gpio, S3C_GPIO_SFN(0x0));//设置为输入模式

val = gpio_get_value((unsigned)gpio);
/*通过判断传参来确定是哪个按键触发*/
switch ((unsigned int)gpio) {
case S5PV210_GPH0(2):   code = KEY_LEFT;    break;
case S5PV210_GPH0(3):   code = KEY_DOWN;    break;
}
/*根据传入的参数选择上报事件包*/
input_report_key(button_dev, code, val);
/*上报同步包*/
input_sync(button_dev);
/*开中断*/
s3c_gpio_cfgpin((unsigned)gpio, S3C_GPIO_SFN(0x0f));//设置为外部中断模式
return IRQ_HANDLED;
}

/*platform驱动的probe(探测)函数与remove函数*/
static int s5pv210_button_probe(struct platform_device *pdev)
{
int error;
int i;

/*向内核申请中断*/
if (request_irq(BUTTON_IRQL, button_interrupt, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "button_left", S5PV210_GPH0(2))) {
printk(KERN_ERR "button-s5pv210.c: Can't allocate irq %d\n", BUTTON_IRQL);
return -EBUSY;
}
/*申请gpio资源*/
error = gpio_request(S5PV210_GPH0(2), "GPH0.2");
if(error)
printk("button-s5pv210: request gpio GPH0(2) fail");
s3c_gpio_cfgpin(S5PV210_GPH0(2), S3C_GPIO_SFN(0x0f));//设置为外部中断模式

/*重复上面两步操作*/
if (request_irq(BUTTON_IRQD, button_interrupt, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "button_down", S5PV210_GPH0(3))) {
printk(KERN_ERR "button-s5pv210.c: Can't allocate irq %d\n", BUTTON_IRQD);
return -EBUSY;
}

error = gpio_request(S5PV210_GPH0(3), "GPH0.3");
if(error)
printk("button-s5pv210: request gpio GPH0(3) fail");
s3c_gpio_cfgpin(S5PV210_GPH0(3), S3C_GPIO_SFN(0x0f));//设置为外部中断模式

/*创建(实例化)输入设备体*/
button_dev = input_allocate_device();
if (!button_dev) {
printk(KERN_ERR "button-s5pv210.c: Not enough memory\n");
error = -ENOMEM;
goto err_free_irq;
}

/*设置“输入设备功能”*/
set_bit(EV_KEY, button_dev->evbit);//EV_KEY是一定要的 不可省略。否则驱动无法正常工作
for(i = 0; i < MAX_BUTTON_CNT; i++)
set_bit(s3c_Keycode[i], button_dev->keybit);

/*注册输入设备*/
error = input_register_device(button_dev);
if (error) {
printk(KERN_ERR "button.c: Failed to register device\n");
goto err_free_dev;
}

return 0;
/*倒影式错误处理流程*/
err_free_dev:
input_free_device(button_dev);
err_free_irq:
free_irq(BUTTON_IRQL, button_interrupt);
return error;
}

static int s5pv210_button_remove(struct platform_device *pdev)
{
input_unregister_device(button_dev);
gpio_free(S5PV210_GPH0(2));
free_irq(BUTTON_IRQL, button_interrupt);
return 0;
}

/*定义我们的platform_driver。注意name要和platform_device中相同*/
static struct platform_driver s5pv210_button_driver = {
.probe      = s5pv210_button_probe,
.remove     = s5pv210_button_remove,
.driver     = {
.name       = "s5pv210_button",
.owner      = THIS_MODULE,
},
};

/*模块与卸载加载函数*/
static int __init button_init(void)
{
return platform_driver_register(&s5pv210_button_driver);
}

static void __exit button_exit(void)
{
platform_driver_unregister(&s5pv210_button_driver);
}

module_init(button_init);
module_exit(button_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("taurenking");
MODULE_DESCRIPTION("s5pv210-button");
MODULE_ALIAS("s5pv210-button");


寻找系统当前按键驱动

因为多个按键驱动之间会冲突,所以首先我们都要找到当前使能的按键驱动,再决定是删除还是修改

寻找当前使能的驱动不是一件容易的事,因为文件名有可能起得不规范,安放的位置也有可能不规范。就比如当前x210开发板上,已经使能了一个按键驱动,但是找遍了驱动文件夹下可能的地方,还是找不到名字相像的已编译.o文件

于是在开发板控制台下,测试读取各event,发现是event0对应按键,于是进入/sys/class/input/event0/device,cat name,发现名字叫s3c-button,这下我们就找到了线索,用slickedit在源码目录搜索s3c-button,找到了,发现是arch/arm/mach-s5pv210/button-smdkv210.c

可见把驱动文件放在该目录十分不合理,极大的增加了我们寻找难度

驱动的实现方式

其实这类按键驱动有两种实现方式,中断和轮询,我们这里使用了最为典型的中断方式

中断方式:驱动只需为每个按键申请中断,然后统一绑定一个中断处理函数,在中断处理函数中上报数据给事件处理层即可

轮询方式:驱动只需申请一个定时器,然后绑定一个定时器中断处理函数,在定时器中断处理函数中轮询每个按键,然后上报数据给事件处理层即可

头文件解析

内核的头文件默认搜索路径在根目录下include文件夹内,所以#include这些头文件时路径的确认非常方便

当头文件不在根目录下include文件夹内呢?基本上这些头文件#include时都包含了符号链接。确定带有符号链接路径的方法是,如果路径不在include文件夹内,但是里面包含了mach、asm等字样,那么路径多半可以确认为
<asm/xxxx>
<mach/xxxx>


当我们实在无法确定带有符号链接路径时,我们只需要借鉴其他文件即可。比如我们用到了copy_from_user。我们知道它在uaccess.h中。但是不知道路径怎么写,这时只需要搜索一下同样调用“copy_from_user”的那些文件,直接抄他们的头文件路径即可

中断处理函数

驱动只需为每个按键申请中断,然后统一绑定一个中断处理函数,在中断处理函数中上报数据给事件处理层即可

那中断处理函数怎么判断是哪个按键被按下呢?可以去读gpio的电平,但是这不是最方便的做法,我们的中断处理函数是可以传参的。注册时request_irq的第五个参数将作为中断服务函数的参数,我们可以放设备底层信息(类型不限),这是实现多个中断共享同一个服务函数的手段,在这里我们放了一个gpio号。关于中断详见内核的中断机制

由于我们设置中断为电平变化沿触发,所以还是要在中断处理函数中读电平,注意读的时候要先把gpio设为输入模式,读完再设回中断模式

当我们读完了gpio,也知道了哪个按键被按下时,就应该把信息汇报给事件处理层了,利用input_report_key函数,该函数第一个参数是设备体,确定了input_event(输入事件信息包)的 type元素,第二第三个参数分别确定了code和value元素。上报完具体信息也可以发送一个同步包,input_sync(button_dev),其实不发也是可以的

除了按键用的input_report_key,内核还提供了其他报告输入事件用的接口

/* 报告指定 type、code 的输入事件 */
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);
/* 报告键值 */
void input_report_key(struct input_dev *dev, unsigned int code, int value);
/* 报告相对坐标 */
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_sync(struct input_dev *dev);


probe和release函数,及其内部的各种注册操作

实例化设备体,再设置“输入设备功能”。所谓的“输入设备功能”,就是设备驱动上传给事件处理层的input_event格式,在注册输入设备前必须设置它。等事件处理层和输入设备绑定时,事件处理层就能知道这个格式了。对于这一步操作,很多驱动中没有使用标准的接口input_set_capability,而是使用了很直接的方法来设置“输入设备功能”

怎么设置呢?这里面很有门道,主要是利用了set_bit这个函数。举个例子:set_bit(a, b)意思是往地址b开始处的第a个“位”写1,注意是位,不是字节。这样就很容易理解了,keybit元素实质是一个数组,但是在input子系统中把它当成了长度很长的寄存器来使用,通过把特定位置1,就能启动该功能,每一位的定义由input.h中的宏提供

设置evbit元素就代表设置了input_event的type元素为按键,在文件开头定义了一个数组,里面都是input.h中的宏,该数组代表了我们要设置的所有位,即设置input_event的code元素种类。遍历该数组,并设置之前创建的设备体中的keybit元素

最后用input_register_device注册我们的输入设备,来和事件驱动层进行匹配绑定。至于怎么匹配绑定?因为所有的输入设备都自动、默认和event类型handler绑定,所以我们不需要操心这方面
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: