内核驱动(二)Linux按键驱动代码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/JerryGou/article/details/82530589

一、按键驱动

1、对按键驱动添加设备信息

linux-stable-3.10.46对按键的驱动定义在gpio_keys.c (drivers\input\keyboard)文件中,在led驱动分析中,我们知道,只有平台设备和平台驱动两者的name名字一致才可以注册成功一个驱动。这里,内核代码中没有对按键平台信息的定义,因此我们需要给他补充完整。

首先将按键驱动编译到内核:

Device Drivers --->
        Input device support --->
                [*]  Keyboards --->
                        <*>  GPIO Buttons

为了简单起见,我们就在mach-Louis210.c(arch\arm\mach-Louis210)下定义设备信息,其中struct gpio_keys_button定义在gpio_keys.h (include\linux)文件中,其定义为:

struct gpio_keys_button {
      /* Configurationparameters */
      unsigned int code;    按键对应的编码,其定义在Input.h
      int gpio;             设置按键对应引脚
      int active_low;       设置按键按下是否是低电平(低电平有效 1)
      const char *desc;     给按键起个名字,相当于led4 led5 led6等
      unsigned int type;        设备按键类型, 默认类型是EV_KEY
      int wakeup;               设置是否设置为唤醒源
      int debounce_interval;    消抖间隔时间,单位毫秒,默认为5毫秒
      bool can_disable;     设置是否共享中断
      int value;            绝对坐标值
      unsigned int irq;     irq中断号的设定
};

结构体介绍完毕,那么就仿照Mach-Louis210.c在mach-Louis210.c定义设备信息如下:

/* gpio keys (add by Louis) */

设置按键的GPIO信息
static struct gpio_keys_button buttons[] = {   
	[0] = {
		.code = KEY_UP,          按键对应的键码
		.gpio = S5PV210_GPH0(0), 按键对应的IO口
		.active_low = 1,         通过查看驱动代码,可得知表示是否按键按下是低电平,如是则设1
		.desc = "KEY_UP",        申请io口,申请中断时使用的名字
		.type = EV_KEY,          输入设备的事件类型,按键用EV_KEY
		.debounce_interval = 50, 防抖动用,间隔多久时间
	},
	[1] = {
		.code = KEY_DOWN,
		.gpio = S5PV210_GPH0(1),
		.active_low = 1,
		.desc = "KEY_DOWN",
		.type = EV_KEY,
		.debounce_interval = 50,
	},
	[2] = {
		.code = KEY_LEFT,
		.gpio = S5PV210_GPH0(2),
		.active_low = 1,
		.desc = "KEY_LEFT",
		.type = EV_KEY,
		.debounce_interval = 50,
	},
	[3] = {
		.code = KEY_RIGHT,
		.gpio = S5PV210_GPH0(3),
		.active_low = 1,
		.desc = "KEY_RIGHT",
		.type = EV_KEY,
		.debounce_interval = 50,
	},
	[4] = {
		.code = KEY_ENTER,
		.gpio = S5PV210_GPH0(4),
		.active_low = 1,
		.desc = "KEY_ENTER",
		.type = EV_KEY,
		.debounce_interval = 50,
	},
	[5] = {
		.code = KEY_BACK,
		.gpio = S5PV210_GPH0(5),
		.active_low = 1,
		.desc = "KEY_BACK",
		.type = EV_KEY,
		.debounce_interval = 50,
	},
	[6] = {
		.code = KEY_MENU,
		.gpio = S5PV210_GPH2(6),
		.active_low = 1,
		.desc = "KEY_MENU",
		.type = EV_KEY,
		.debounce_interval = 50,
	},
	[7] = {
		.code = KEY_POWER,
		.gpio = S5PV210_GPH2(7),
		.active_low = 1,
		.desc = "KEY_POWER",
		.type = EV_KEY,
		.debounce_interval = 50,
	},
};

设置按键的设备平台数据
static struct gpio_keys_platform_data Louis210_keys_pdata = {
	.buttons = buttons,     对应的按键的GPIO信息
	.nbuttons = ARRAY_SIZE(buttons),    获取要定义的按键个数
	.rep = 1,   //同一次按键是否重复上报
};
 
static struct platform_device Louis210_keys = {
	.name = "gpio-keys",
	.dev = {
		.platform_data =  &Louis210_keys_pdata,
	},
	.id = -1,
};

添加平台数据至平台设备数组 
static struct platform_device *Louis210_devices[] __initdata = {
	&Louis210_keys,
};

      平台设备信息就设置完毕,

2、对按键驱动的测试

通过查看按键设备信息可以看到添加的按键驱动信息,

在/proc/bus/input目录下执行 cat  devices可以看到:

[root@Louis210: gpio-keys]# cd /proc/bus/input/
[root@Louis210: input]# cat devices
I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="gpio-keys"
P: Phys=gpio-keys/input0
S: Sysfs=/devices/platform/gpio-keys/input/input2
U: Uniq=
H: Handlers=kbd event2 
B: PROP=0
B: EV=100003
B: KEY=40000800 101680 0 0 10000000

这里列出了按键驱动的详细信息,第一个I是

struct input_id {
      __u16 bustype;
      __u16 vendor;
      __u16 product;
      __u16 version;

}列出的信息

下面有名字,设备文件存在的路径,等等信息。

可以通过按键动作进行测试。

当有按键按下时,我们可以通过读取/dev/input/event2文件来查看上报事件,此时我们以16进制查看类型读取显示该文件,使用hexdump命令:

[root@Louis210: input]# hexdump  /dev/input/event2
0000000 4cb8 386d c2ff 000c 0001 0069 0001 0000
0000010 4cb8 386d c2ff 000c 0000 0000 0000 0000
0000020 4cb8 386d 2078 000f 0001 0069 0000 0000
0000030 4cb8 386d 2078 000f 0000 0000 0000 0000

0000040 4cbc 386d 99d6 000d 0001 0067 0001 0000
0000050 4cbc 386d 99d6 000d 0000 0000 0000 0000
0000060 4cbd 386d 032e 0001 0001 0067 0000 0000
0000070 4cbd 386d 032e 0001 0000 0000 0000 0000

0000080 4cc0 386d b0e1 0002 0001 006c 0001 0000
0000090 4cc0 386d b0e1 0002 0000 0000 0000 0000
00000a0 4cc0 386d 5c7b 0005 0001 006c 0000 0000
00000b0 4cc0 386d 5c7b 0005 0000 0000 0000 0000

本代码设置三个按键,当我按下三个按键的时候,会上报三组数据,就以第一组数据为例,当我按下按键的时候(保持按下这个动作,不松开按键),会打印出两组数据:

[root@Louis210: input]# hexdump  /dev/input/event2
0000000 4cb8 386d c2ff 000c 0001 0069 0001 0000
0000010 4cb8 386d c2ff 000c 0000 0000 0000 0000

其中0001(倒数第4列)代表事件类型,因为类型为EV_KEY所以为1
其中0069(倒数第3列)代表按键的编码 设置按键功能为 KEY_LEFT  KEY_RIGHT  KEY_END则编码为0x69 0x6A 0x6B
其中0001(倒数第2列)代表按键值,当按键按下的时候按键值为1

测试按键程序key.c

#include<stdint.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
#include<linux/input.h>

int main(void)
{
    int fd;
    int flag=0;
    struct input_event ev_key;
    fd=open("/dev/input/event2", O_RDWR);   读写方式打开
    
    if(fd < 0)
    {
        perror("open device buttons");
        close(fd);
        
        return -1;
    }

    while(1)
    {
        read(fd,&ev_key,sizeof(struct input_event));
        
        if(flag!=ev_key.type)    这里是判断是否有上报按键事件的发生。
            printf("type:%d,code:%d,value:%d,sec:%d,nsec:%d\n",
            ev_key.type,ev_key.code,ev_key.value,ev_key.time.tv_sec,ev_key.time.tv_usec);
            
        flag=ev_key.type;
    }

    close(fd);
    return 0;
}

read的返回值是一个

struct input_event {
      struct timeval time;    系统开机时间
      __u16 type;
      __u16 code;
      __s32 value;
};

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

可以看出来,这里是按键按下时候的系统时间(第一个参数单位是秒,第二个参数单位是微秒)
后面的参数为事件驱动类型(对应的是ev_key)
后面两个是按键的编码和按键的值(这里的按键值是松开是0或者按键按键其值为1)。

3、对按键驱动分析

分析按键驱动之前,我觉得有必要先学习一下有关输入系统的介绍:

Input子系统分为三层,从下至上分别是设备驱动层核心层以及事件处理层

输入设备主要的工作过程都是动作产生(按键,触屏……)-->产生中断-->读取数值(键值,坐标……)-->将数值传递给应用程序。一个大致的工作流程就是,input device向上层报告-->input core接收报告,并根据在注册input device时建立好的连接选择哪一类handler来处理事件-->通过handler将数据存放在相应的dev(evdev,mousedev…)实例的缓冲区中,等待应用程序来读取。这三层中的输入核心层和事件处理层都是内核已经完成了的,因此需要我们完成的只有设备驱动层。

下面网上有张图来解释输入子系统的框架:

通过上面的图可以看出来,input有像按键等的输入设备event;触摸屏等的输入设备ts;遥控杆等输入设备js;鼠标等输入设备mouse和键盘等不会在/dev/input下产生节点,而是作为ttyn终端(不包括串口终端)的输入。

通过上面的介绍,结合具体的函数或结构体来解释就是设备驱动层为具体用户设备驱动,输入设备由struct input_dev 结构表示,并由input_register_deviceinput_unregister_device来注册和卸载;input_hander事件处理层主要和用户空间交互,接收用户空间下发的file_operation操作命令,生成/dev/input/xx设备节点供用户空间进行file_operations操作; input_core层负责管理系统中的input_dev 设备 和input_hander事件处理,并起到承上启下作用,负责输入设备和input_hander之间信息传输,框架图如下:

大概介绍到这里,下面来分析代码:

按键的驱动定义在gpio_keys.c(drivers\input\keyboard)文件中,老规矩首先看到这个宏:

late_initcall(gpio_keys_init);
module_exit(gpio_keys_exit);

这里有个宏late_initcall,这个宏的跟之前遇见的subsys_initcall的功能是一样的,只不过其优先级不同,具体的他们全部定义在Init.h (include\linux)中:

#define early_initcall(fn)             __define_initcall(fn,early)
#define pure_initcall(fn)              __define_initcall(fn,0)
#define core_initcall(fn)              __define_initcall(fn,1)
#define core_initcall_sync(fn)         __define_initcall(fn,1s)
#define postcore_initcall(fn)          __define_initcall(fn,2)
#define postcore_initcall_sync(fn)     __define_initcall(fn,2s)
#define arch_initcall(fn)              __define_initcall(fn,3)
#define arch_initcall_sync(fn)         __define_initcall(fn,3s)
#define subsys_initcall(fn)            __define_initcall(fn, 4)
#define subsys_initcall_sync(fn)       __define_initcall(fn,4s)
#define fs_initcall(fn)                __define_initcall(fn,5)
#define fs_initcall_sync(fn)           __define_initcall(fn,5s)
#define rootfs_initcall(fn)            __define_initcall(fn,rootfs)
#define device_initcall(fn)            __define_initcall(fn, 6)
#define device_initcall_sync(fn)       __define_initcall(fn,6s)
#define late_initcall(fn)              __define_initcall(fn, 7)
#define late_initcall_sync(fn)         __define_initcall(fn,7s)
#define module_init(x)                 __initcall(x);
#define __initcall(fn)                 device_initcall(fn)

通过上面的宏可以看出他们的后面的参数不同,系数越小优先级越大,可以看出:
优先级由大到小为:subsys_initcall>module_init > late_initcall

接下来就来看gpio_keys_init gpio_keys_exit

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);
}

他们分别调用了平台驱动注册和注销函数,先看平台驱动注册函数的参数结构体:

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

从这个结构体看,跟led的格式就一样的,显示比较name是否都是"gpio-keys"平台设备跟平台驱动的名字比较搭配的话,在注册的时候就会去执行gpio_keys_probe函数,在注销的时候就会执行gpio_keys_remove的函数,往下看就是对应驱动的名字了;然后gpio_keys_pm_ops,这个由宏

static SIMPLE_DEV_PM_OPS(gpio_keys_pm_ops, gpio_keys_suspend,gpio_keys_resume);

生成的,该宏定义在Pm.h (include\linux),根据该宏的定义,最终会调用gpio_keys_remove和gpio_keys_suspend函数控制电源的进入工作状态或者低功耗状态。

下一个定义了设备和驱动匹配函数,匹配方法可以用两个字方法:如果driver中定义了of_device_id,则通过driver中的of_device_id和device中的device_node内容进行匹配判断,匹配工作由of_match_node来完成,该函数会遍历of_device_id列表,查找是否有成员与device_node相匹配,具体由matches的name,type和compatioble来进行对比,如果找到则返回相应的表项,否则返回null.如果没有定义of_device_id,device_node或不能找到对应的匹配项,则通过第二种方式platform_device_id来进行对比匹配,通过platform_match_id来完成。

接下来详细说明上面提到的几个函数。

先看探测函数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);    
获取平台设备信息,即得到Louis210_keys_pdata的数据信息。
	struct gpio_keys_drvdata *ddata;    驱动信息的数据结构体,
	struct input_dev *input;    定义了一个输入设备,
	int i, error;
	int wakeup = 0;

	if (!pdata) {
		pdata = gpio_keys_get_devtree_pdata(dev);
		if (IS_ERR(pdata))
			return PTR_ERR(pdata);
	}

	ddata = kzalloc(sizeof(struct gpio_keys_drvdata) +      分配且清空数据空间
			pdata->nbuttons * sizeof(struct gpio_button_data),
			GFP_KERNEL);
	input = input_allocate_device();        分配一个input设备
	if (!ddata || !input) {
		dev_err(dev, "failed to allocate state\n");
		error = -ENOMEM;
		goto fail1;
	}

	ddata->pdata = pdata;
	ddata->input = input;
	mutex_init(&ddata->disable_lock);  
 为ddata初始化互斥锁(互斥体),mutex的使用场合跟信号量基本相同,一般用户那些进程之间竞争,
 且占用时间较长的场合,当占用时间较短是,一般使用互旋锁。

	platform_set_drvdata(pdev, ddata);  将驱动数据保存到驱动平台数据中,后期将会使用保存的函数
	input_set_drvdata(input, ddata);    将驱动数据保存到输入设备中中,后期将会使用保存的函数

	input->name = pdata->name ? : pdev->name;
	input->phys = "gpio-keys/input0";
	input->dev.parent = &pdev->dev;
	input->open = gpio_keys_open;       在挂起或者唤醒的时候会调用open和close函数
	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);

    循环获取每个按键的信息,并通过gpio_keys_setup_key函数为每个按键初始化引脚,滤波消抖,
    申请外部中断,申请定时器中断平且设定中断定时器服务函数
	for (i = 0; i < pdata->nbuttons; i++) { 
		const struct gpio_keys_button *button = &pdata->buttons[i];
		struct gpio_button_data *bdata = &ddata->data[i];

		error = gpio_keys_setup_key(pdev, input, bdata, button);
		if (error)
			goto fail2;

		if (button->wakeup)
			wakeup = 1;
	}

	error = sysfs_create_group(&pdev->dev.kobj, &gpio_keys_attr_group);     
在kobj目录下创建一个属性集合,并显示集合中的属性文件。如果文件已存在,会报错。以函数为例,
这里将会在gpio-keys/目录下创建一个属性文件gpio_keys_attr_group,gpio_keys_attr_group最终会调用:
	if (error) {
		dev_err(dev, "Unable to export keys/switches, error: %d\n",
			error);
		goto fail2;
	}

	error = input_register_device(input);   向内核注册一个input设备
	if (error) {
		dev_err(dev, "Unable to register input device, error: %d\n",
			error);
		goto fail3;
	}

	device_init_wakeup(&pdev->dev, wakeup);     电源管理有关

	return 0;

 fail3:
	sysfs_remove_group(&pdev->dev.kobj, &gpio_keys_attr_group);     删除之前创建的属性文件
 fail2:
	while (--i >= 0)
		gpio_remove_key(&ddata->data[i]);       释放掉申请的引脚,取消申请的队列等等

	platform_set_drvdata(pdev, NULL);
 fail1:
	input_free_device(input);
	kfree(ddata);
	/* If we have no platform data, we allocated pdata dynamically. */
	if (!dev_get_platdata(&pdev->dev))
		kfree(pdata);

	return error;
}

structgpio_keys_drvdata {
      const struct gpio_keys_platform_data *pdata;      定义的平台信息,包括引脚等的信息
      struct input_dev *input;                          分配了一个输入设备
      struct mutex disable_lock;                        分配一个互斥锁
      struct gpio_button_data data[0];                  分配一个按键数据结构体
};

电源管理有关
staticinline int device_init_wakeup(struct device *dev, bool val)
{

    device_set_wakeup_capable(dev, val);    设置设备能不能被唤醒
    device_set_wakeup_enable(dev, val);     设置设备使不使用唤醒;
    return 0;

}

probe函数做完了整个驱动要做的事情,现在总结一下probe都做了些什么事:
1、首先获取平台设备信息:dev_get_platdata(dev)或者gpio_keys_get_devtree_pdata(dev)
2、为定义的设备信息申请一块内存:kzalloc
3、申请分配一个输入设备: input_allocate_device
4、为该输入设备设置属性:input->······
5、初始化按键相关的引脚、中断、及其有关的定时器信息:gpio_keys_setup_key
6、为该设备创建一个属性集合:sysfs_create_group
7、正式申请为输入设备:input_register_device

刚才有看到在probe函数中,调用了好多自定义的一些函数,接下来逐个分析一下这些函数的实现。
先看一下设置按键的函数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, error;

	bdata->input = input;
	bdata->button = button;
	spin_lock_init(&bdata->lock);
    
/**************************************************************************
    初始化自旋锁,自旋锁适用于临界区不是很大的情况(临界区的执行时间比较短)
总结自旋锁使用流程:
    1、首先定义一个自旋锁:
        spinlock_t lock
    2、初始化自旋锁:
       spin_lock_init(lock)
    3、获取自旋锁
      (1)spin_trylock(lock)//假如获得锁返回真,否则返回假,返回假货不会原地打转的等着获得锁
      (2)spin_lock(lock)//假如获得锁返回真,否则返回假,返回假货将会原地打转的等着获得锁
    4、释放掉自旋锁
       spin_unlock(lock);
**************************************************************************/
   
	if (gpio_is_valid(button->gpio)) {  //测试端口是否合法

		error = gpio_request_one(button->gpio, GPIOF_IN, desc); //申请IO口,并设置为输入模式
		if (error < 0) {
			dev_err(dev, "Failed to request GPIO %d, error %d\n",
				button->gpio, error);
			return error;
		}

		if (button->debounce_interval) {
			error = gpio_set_debounce(button->gpio,
					button->debounce_interval * 1000);  按键消抖处理
			/* use timer if gpiolib doesn't provide debounce */
			if (error < 0)
				bdata->timer_debounce =
						button->debounce_interval;
		}

		irq = gpio_to_irq(button->gpio);    申请GPIO中断,设置为外部中断,同时获取中断号
		if (irq < 0) {
			error = irq;
			dev_err(dev,
				"Unable to get irq number for GPIO %d, error %d\n",
				button->gpio, error);
			goto fail;
		}
		bdata->irq = irq;   赋值中断号

		INIT_WORK(&bdata->work, gpio_keys_gpio_work_func); 
        
/**************************************************************************
        创建工作,关联工作函数 将函数gpio_keys_gpio_work_func假如工作队列,让中断的第二阶段
去执行该函数。实际上&bdata->work是一个描述工作队列的结构体,变量定义为struct work_struct work
        
总结工作队列过程:
    1、首先定义一个工作结构体:
        struct work_struct work
    2、将定义的工作加入内核工作队列并且绑定放入队列的函数:
        INIT_WORK(work, work_func)  或者  INIT_DELAYED_WORK(work, work_func)
    3、当需要执行工作函数的时:
        (1)对应的INIT_WORK  执行schedule_work(work)会马上调用work_func函数
        (2) 对应的NIT_DELAYED_WORK 执行schedule_delayed_work(time,work)会在time时间后执行work_func
    4、取消工作队列中的工作:
        (1)对应的INIT_WORK   cancel_work_sync(work)
        (2)对应的NIT_DELAYED_WORK
            cancel_delayed_work_sync//取消延时工作并且等待其完成工作
            cancel_delayed_work(work)//取消延时工作
**************************************************************************/
        
		setup_timer(&bdata->timer,
			    gpio_keys_gpio_timer, (unsigned long)bdata);
                
/**************************************************************************
总结定时器的使用方法:
    1、定义一个定时器timer_listmytimer
    2、初始化定时器并赋值成员setup_timer(mytimer, timer_func, data);
    3、增加定时器add_timer(mytimer)
    4、该修改定时器的定时时间expire  mod_timer(mytimer,expire)
    5、取消定时器,有两个可选择
        1. del_timer(mytimer) 直接删除定时器
        2. del_timer_sync(mytimer)等待本次定时器处理完毕再取消(不适用中断上下文)
		isr = gpio_keys_gpio_isr;   //设置外部中断服务函数,并配置中断处理标志为上升沿触发和下降沿触发
		irqflags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;
        设定中断处理的属性,也就是中断触发方式处理方式等等,假如设置IRQF_SHARED表明多个设备共享
一个中断,此时会用到dev_id;当设置IRQF_DISABLED时候表明,中断为快速中断
**************************************************************************/

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

		if (button->type && button->type != EV_KEY) {
			dev_err(dev, "Only EV_KEY allowed for IRQ buttons.\n");
			return -EINVAL;
		}

		bdata->timer_debounce = button->debounce_interval;
		setup_timer(&bdata->timer,
			    gpio_keys_irq_timer, (unsigned long)bdata);

		isr = gpio_keys_irq_isr;
		irqflags = 0;
	}

	input_set_capability(input, button->type ?: EV_KEY, button->code);
    设定该按键具有什么能力,以本例来说,button1 button2 button3具有按键Left Right End的能力

	/*
	 * If platform has specified that the button can be disabled,
	 * we don't want it to share the interrupt line.
	 */
	if (!button->can_disable)
		irqflags |= IRQF_SHARED;
        
/**************************************************************************
设置是否共享中断,这里涉及到中断共享机制:
多个中断共享一个中断线必须用IRQF_SHARED做标记,以IRQF_SHARED作为标记的中断假如要向内核申请成功
一个中断需要两个条件之一:
该中断还没有被申请
该中断虽然被申请了,但是已经申请的中断也有IRQF_SHARED做标记
当发生共享中断时,所用挂载到此中断的中断服务函数都会得到响应(遍历所有该中断线上的中断),所以说,
当该中断设备为共享中断的时候,中断服务函数首先需要判断是否是自己的dev_id假如不是自己的dev_id那么
返回IRQ_NOTE(表明不是本中断),假如检测到时本中断的话就会执行中断里面的函数,最后返回IRQ_HANDLED。
**************************************************************************/

	error = request_any_context_irq(bdata->irq, isr, irqflags, desc, bdata);
    
/**************************************************************************
申请一个中断线,其中bdata->irq为要申请的中断线(中断号);isr为中断服务函数;irqflags中断的触发
方式或者处理方式;desc要申请中断的描述符;bdata为dev_id,区分共享中断线线而设定的。

申请外部中断的流程:
    1、申请某个引脚为外部中断 intirq = gpio_to_irq(button->gpio);
    2、设备外部中断函数irq_handler_tisr = gpio_keys_gpio_isr;
    3、设置中断发出类型 unsignedlong flags =?
    4、描述该中断的一个assic字符串的名字  constchar *name=?
    5、设备dev-id 是一个空函数指针  void*dev_id =?    
**************************************************************************/

	if (error < 0) {
		dev_err(dev, "Unable to claim irq %d; error %d\n",
			bdata->irq, error);
		goto fail;
	}

	return 0;

fail:
	if (gpio_is_valid(button->gpio))
		gpio_free(button->gpio);

	return error;
}

总结设置按键的函数gpio_keys_setup_key作用:
       该函数主要是申请外部中断,设定中断服务函数,由于在消抖的时候会用到定时器,这个函数还初始化了定时器。并且绑定了定时器中断服务函数。

接下来看一下按键服务函数gpio_keys_gpio_isr

static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
{
/*************************************************************************
首先看中断服务函数的返回值:
IRQ_NONE ,还没有发生中断
IRQ_HANDLED     中断已经处理完毕
IRQ_WAKE_THREAD中断需要唤醒处理线程
**************************************************************************/
	struct gpio_button_data *bdata = dev_id;

	BUG_ON(irq != bdata->irq);  代码调试用的

	if (bdata->button->wakeup)  根据wakeup保存非睡眠状态
		pm_stay_awake(bdata->input->dev.parent);

/*************************************************************************        
设置定时器定时时间,定时器时间到后就会执行定时器服务函数
(在按键设置函数gpio_keys_setup_key中已经设置定时器服务函数为gpio_keys_gpio_timer)
**************************************************************************/

	if (bdata->timer_debounce)
		mod_timer(&bdata->timer, jiffies + msecs_to_jiffies(bdata->timer_debounce));
        else
		schedule_work(&bdata->work);    执行中断底部的队列部分

	return IRQ_HANDLED;
}

static void gpio_keys_gpio_timer(unsigned long_data)
{
    structgpio_button_data *bdata = (struct gpio_button_data *)_data;
    schedule_work(&bdata->work);      执行中断底部的队列部分;
}

对于按键中断服务函数gpio_keys_gpio_isr可以看出来,中断函数确实是分为中断顶半部(top half)和中断底半部(bottom half),对于按键来说,中断顶半部就是消抖(消抖还用到了定时间,所以说需要的执行时间很短),中断的底半部就是用来执行工作队列里面的工作了,接下来看一下队列的工作都做了什么:

static void gpio_keys_gpio_work_func(struct work_struct *work)
{
	struct gpio_button_data *bdata = container_of(work, struct gpio_button_data, work);

	gpio_keys_gpio_report_event(bdata);

	if (bdata->button->wakeup)  //电源管理相关
		pm_relax(bdata->input->dev.parent);
}

可以看到队列工作就是为了调用时间上报函数gpio_keys_gpio_report_event

static void gpio_keys_gpio_report_event(struct gpio_button_data *bdata)
{
	const struct gpio_keys_button *button = bdata->button;
	struct input_dev *input = bdata->input;
	unsigned int type = button->type ?: EV_KEY;
	int state = (gpio_get_value_cansleep(button->gpio) ? 1 : 0) ^ button->active_low;
    
/*************************************************************************
gpio_get_value_cansleep是获取按键的值,其gpio_get_value区别是,有些芯片的引脚与
cpu是依靠一些总线连接的比如iic总线,那么这些引脚就有可能产生休眠,因此要用
gpio_get_value_cansleep来获取按键的值,获取后异或button->active_low(之前设定的
是1),那么当有按键按下时,gpio_get_value_cansleep得到的是低电平也就是0,所以0^1
=1,这里也是再说名,当按键按下时,获取的state应该是1。
**************************************************************************/

	if (type == EV_ABS) {   这个类型判断是不是EV_ABS(绝对坐标)事件,本次是EV_KEY事件
		if (state)
			input_event(input, type, button->code, button->value);
	} else {
		input_event(input, type, button->code, !!state);
	}
    
	input_sync(input);  上报事件上报完成
}

到这里上报事件就完工了,回想一下,整个输入子系统的流程可以总结为两部:
1、首先定义申请注册一个输入设备。
2、申请按键为外部中断,当按键按下时,在中断服务函数中处理,并上报按键事件。

猜你喜欢

转载自blog.csdn.net/JerryGou/article/details/82530589