对于s3c2440开发板上的几个按键功能,linux系统提供了很好的支持,只要把按键对应的IO端口配置好,按键就可以工作了。
针对我的开发板,为了配置按键的IO端口,需要修改arch/arm/mach-s3c24xx目录下的mach-zhaocj2440.c文件中的zhaocj2440_buttons结构为:
static struct gpio_keys_button zhaocj2440_buttons[] = {
{
.gpio = S3C2410_GPF(0), /* K4 */
.code = KEY_F1,
.desc = "Button 4",
.active_low = 1,
},
{
.gpio = S3C2410_GPF(1), /* K1 */
.code = KEY_F2,
.desc = "Button 1",
.active_low = 1,
},
{
.gpio = S3C2410_GPF(2), /* K3 */
.code = KEY_F3,
.desc = "Button 3",
.active_low = 1,
},
{
.gpio = S3C2410_GPF(4), /* K2*/
.code = KEY_POWER,
.desc = "Button 2",
.active_low = 1,
},
};
修改好后使用默认的menuconfig直接编译即可。把编译好的系统下载到开发板上,系统运行后,在dev目录有一个event0文件,这个就是按键设备。也可以通过下列命令查看一下设备信息:
[root@zhaocj /]#ls -l /proc/bus/input
-r--r--r-- 1 0 0 0 Jan 1 00:05 devices
-r--r--r-- 1 0 0 0 Jan 1 00:05 handlers
[root@zhaocj /]#cat /proc/bus/input/devices
I: Bus=0019 Vendor=0001 Product=0001Version=0100
N: Name="gpio-keys"
P: Phys=gpio-keys/input0
S:Sysfs=/devices/platform/gpio-keys/input/input0
U: Uniq=
H: Handlers=kbd event0
B: PROP=0
B: EV=3
B: KEY=100000 0 38000000 0
下面我们就来具体测试一下按键功能:
[root@zhaocj/]#hexdump /dev/event0
0000000 44e4 386d 3190000f 0001 003d 0001 0000
0000010 44e4 386d 31c0000f 0000 0000 0000 0000
0000020 44e5 386d 3b9a0003 0001 003d 0000 0000
0000030 44e5 386d 3bbd0003 0000 0000 0000 0000
0000040 44ea 386d a2750008 0001 003c 0001 0000
0000050 44ea 386d a2a30008 0000 0000 0000 0000
0000060 44ea 386d e426000b 0001 003c 0000 0000
0000070 44ea 386d e449000b 0000 0000 0000 0000
0000080 4525 386d 91190004 0001 0074 0001 0000
0000090 4525 386d 91490004 0000 0000 0000 0000
00000a0 4525 386d a78b0007 0001 0074 0000 0000
00000b0 4525 386d a7ad0007 0000 0000 0000 0000
00000c0 452a 386d f4af0005 0001 003b 0001 0000
00000d0 452a 386d f4df0005 0000 0000 0000 0000
00000e0 452a 386d ea3d0008 0001 003b 0000 0000
00000f0 452a 386d ea5e0008 0000 0000 0000 0000
上面是依次按下4个按键所得到的结果,每按一个键,会出现4行数据,这是因为每按一次键包括键的按下和键的抬起两个动作,而每个动作结束后还会有一个同步事件发生,因此会出现4行数据。其中每行的后几个数据的含义是基于input_event数据结构的:
struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};
即数据表示的是按键的类型、按键的代码和按键的值。
我们也可以编写一段应用程序来测试按键功能:
#include <stdio.h>
#include <stdlib.h>
#include <linux/input.h>
int main(void)
{
intfd;
struct input_event ev_key;
fd= open("/dev/event0", 666);
if(fd < 0)
{
perror("open device buttons");
exit(1);
}
for(;;)
{
read(fd,&ev_key,sizeof(struct input_event));
printf("type:%d,code:%d,value:%d\n",ev_key.type,ev_key.code,ev_key.value);
}
close(fd);
return 0;
}
运行该程序,并依此按下4个键,得到的结果为:
type:1,code:61,value:1
type:0,code:0,value:0
type:1,code:61,value:0
type:0,code:0,value:0
type:1,code:60,value:1
type:0,code:0,value:0
type:1,code:60,value:0
type:0,code:0,value:0
type:1,code:116,value:1
type:0,code:0,value:0
type:1,code:116,value:0
type:0,code:0,value:0
type:1,code:59,value:1
type:0,code:0,value:0
type:1,code:59,value:0
type:0,code:0,value:0
下面就介绍一下linux是如何实现按键功能的。这里主要涉及到两方面的内容:linux的中断处理和输入子系统。
Linux中断分为两个部分:前半部和后半部。前半部是实际响应中断的函数,需要用request_irq注册;后半部是由前半部调度,并在一个更安全的时间来执行的函数,该函数就是具体负责执行中断的内容。前半部不允许被其他中断干扰,因此它的内容短小,而执行后半部时可以响应其他中断。这种机制的好处是可以迅速的响应中断。
Linux输入子系统包括三个层次:事件处理层(Event Handler)、核心层(Input Core)和驱动层(Input Driver)。
1.事件处理层负责与用户程序打交道,将核心层传来的事件报告给用户程序。
2.核心层是链接其他两个层之间的纽带与桥梁,向下提供驱动层的接口,向上提供事件处理层的接口。
3.驱动层负责操作具体的硬件设备,这层的代码是针对具体的驱动程序的,键盘、鼠标、触摸屏等字符设备驱动功能的实现工作主要就在这层。
输入子系统有三个核心结构体:input_dev,input_handle和input_handler。input_dev表示一个输入设备,包含输入设备的一些相关信息;input_handler表示对输入事件的具体处理,它为输入设备的功能实现了一个接口;input_handle是用来连接输入设备和输入事件。输入子系统主要的任务就是把这三个结构体连接在一起。
下面就按照输入子系统的三个层次,由底层向用户层的方向介绍按键是如何工作的。
在驱动层,Linux提供了一个通用的基于GPIO的按键驱动程序文件——gpio_keys.c(它是在drivers/input/keyboard目录下),我们的按键驱动也是调用该文件的。
首先进行按键的初始化:
static int __init gpio_keys_init(void)
{
//注册驱动
returnplatform_driver_register(&gpio_keys_device_driver);
}
gpio_keys_device_driver的内容为:
static struct platform_drivergpio_keys_device_driver = {
.probe = gpio_keys_probe,
.remove = __devexit_p(gpio_keys_remove),
.driver = {
.name = "gpio-keys", //与设备名一致,即与Mach-zhaocj2440.c中的平台设备zhaocj2440_button_device中的.name一致
.owner = THIS_MODULE,
.pm = &gpio_keys_pm_ops,
.of_match_table= gpio_keys_of_match,
}
};
主要介绍gpio_keys_probe函数:
static int __devinit gpio_keys_probe(structplatform_device *pdev)
{
conststruct gpio_keys_platform_data *pdata = pdev->dev.platform_data;
structgpio_keys_drvdata *ddata;
structdevice *dev = &pdev->dev;
structgpio_keys_platform_data alt_pdata;
structinput_dev *input;
inti, error;
intwakeup = 0;
if(!pdata) {
//用另一种方法得到平台总线数据
error= gpio_keys_get_devtree_pdata(dev, &alt_pdata);
if(error)
return error;
pdata = &alt_pdata;
}
//为驱动数据ddata开辟一段内存空间,并清零
ddata= kzalloc(sizeof(struct gpio_keys_drvdata) +
pdata->nbuttons* sizeof(struct gpio_button_data),
GFP_KERNEL);
//为输入设备input开辟一段内存空间
input= input_allocate_device();
if(!ddata || !input) {
dev_err(dev,"failed to allocate state\n");
error= -ENOMEM;
gotofail1;
}
//为ddata幅值
ddata->input= input;
ddata->n_buttons= pdata->nbuttons;
ddata->enable= pdata->enable;
ddata->disable= pdata->disable;
mutex_init(&ddata->disable_lock);
//把驱动数据ddata以私有数据形式存放在平台总线设备pdev中,以备日后使用
platform_set_drvdata(pdev, ddata);
//把驱动数据ddata以私有数据形式存放在输入设备input中,以备日后使用
input_set_drvdata(input,ddata);
//为输入设备input幅值
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 */
//为输入设备input赋予自动重复特性,因为按键可以反复地按
if(pdata->rep)
__set_bit(EV_REP,input->evbit);
//为每个按键进行初始化,及设置属性
for (i = 0; i <pdata->nbuttons; i++) {
conststruct gpio_keys_button *button = &pdata->buttons[i];
struct gpio_button_data *bdata =&ddata->data[i];
// gpio_keys_setup_key为关键函数,在后面进行分析
error = gpio_keys_setup_key(pdev,input, bdata, button);
if (error)
gotofail2;
if(button->wakeup)
wakeup= 1;
}
//在指定目录下生成sys属性文件,即在执行完下面语句后,会在/sys/devices/platform/gpio-keys/目录下生成四个文件:disabled_keys、disabled_switches、switches、keys,前两个是可读可写的,后两个为只读。
error= sysfs_create_group(&pdev->dev.kobj, &gpio_keys_attr_group);
if(error) {
dev_err(dev,"Unable to export keys/switches, error: %d\n",
error);
gotofail2;
}
//注册输入设备input,把输入设备加到输入设备链表中,并寻找合适的handler与input_handler配对
error= input_register_device(input);
if(error) {
dev_err(dev,"Unable to register input device, error: %d\n",
error);
gotofail3;
}
/*get current state of buttons that are connected to GPIOs */
//把当前的按键状态上报给系统,这个是在系统初始化,即上电的时候报告给系统的
for (i = 0; i <pdata->nbuttons; i++) {
struct gpio_button_data *bdata = &ddata->data[i];
if(gpio_is_valid(bdata->button->gpio))
gpio_keys_gpio_report_event(bdata);
}
//用于事件的同步,即告知系统设备已传递完一组完整的数据
input_sync(input);
device_init_wakeup(&pdev->dev,wakeup);
return0;
…… ……
}
下面介绍gpio_keys_probe函数中的主要函数gpio_keys_setup_key,它的作用是初始化按键:
static int __devinitgpio_keys_setup_key(struct platform_device *pdev,
struct input_dev *input,
struct gpio_button_data *bdata,
const struct gpio_keys_button *button)
{
constchar *desc = button->desc ? button->desc : "gpio_keys";
structdevice *dev = &pdev->dev;
irq_handler_tisr;
unsignedlong irqflags;
intirq, error;
bdata->input= input;
bdata->button= button;
spin_lock_init(&bdata->lock);
//判断按键所对应的gpio是否有效
if(gpio_is_valid(button->gpio)) {
//向系统申请gpio
error= gpio_request(button->gpio, desc);
if(error < 0) {
dev_err(dev,"Failed to request GPIO %d, error %d\n",
button->gpio,error);
returnerror;
}
//设置gpio的方向为输入
error= gpio_direction_input(button->gpio);
if(error < 0) {
dev_err(dev,
"Failedto configure direction for GPIO %d, error %d\n",
button->gpio,error);
gotofail;
}
//设置按键的去抖时间间隔
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;
}
//提取gpio所对应的外部中断
irq= gpio_to_irq(button->gpio);
if(irq < 0) {
error= irq;
dev_err(dev,
"Unableto get irq number for GPIO %d, error %d\n",
button->gpio,error);
gotofail;
}
bdata->irq= irq;
//初始化工作队列,因为gpio按钮所使用的中断的后半部是基于工作队列方式的
//也就是在bdata->work的工作队列中增加一个任务gpio_keys_gpio_work_func
INIT_WORK(&bdata->work,gpio_keys_gpio_work_func);
//设置定时器,因为在按键去抖上用到了定时器
setup_timer(&bdata->timer,
gpio_keys_gpio_timer, (unsignedlong)bdata);
//gpio_keys_gpio_isr为按键中断函数
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;
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;
}
//标识输入设备对哪些事件感兴趣,因为是按键,所以感兴趣的事件一定是EV_KEY
input_set_capability(input,button->type ?: EV_KEY, button->code);
/*
* If platform has specified that the buttoncan be disabled,
* we don't want it to share the interruptline.
*/
if(!button->can_disable)
irqflags|= IRQF_SHARED;
//分配一条中断线,即当按键中断(bdata->irq)发生时,进入中断函数(isr,即gpio_keys_irq_isr函数内)。
error= request_any_context_irq(bdata->irq, isr, irqflags, desc, bdata);
if(error < 0) {
dev_err(dev,"Unable to claim irq %d; error %d\n",
bdata->irq,error);
gotofail;
}
return0;
fail:
if(gpio_is_valid(button->gpio))
gpio_free(button->gpio);
returnerror;
}
下面介绍按键中断函数:
static irqreturn_t gpio_keys_gpio_isr(intirq, void *dev_id)
{
structgpio_button_data *bdata = dev_id;
BUG_ON(irq!= bdata->irq);
if (bdata->timer_debounce)
mod_timer(&bdata->timer,
jiffies+ msecs_to_jiffies(bdata->timer_debounce));
else
schedule_work(&bdata->work);
returnIRQ_HANDLED;
}
该中断函数即为中断机制的前半部,它主要思想是在按键去抖间隔时间内不执行中断后半部分内容,只有在经过去抖间隔时间达到以后,才执行工作队列中的内容&bdata->work,即执行gpio_keys_gpio_work_func函数。
中断后半部函数gpio_keys_gpio_work_func调用的是gpio_keys_gpio_report_event函数,而gpio_keys_gpio_report_event函数调用的是input_event函数,该函数的功能是把输入事件上报给系统。input_event函数经过一次次传递和封装,最终到达用户空间而被用户所用。
下面介绍输入子系统的核心层文件input.c(它在drivers/input目录下),其中上文提到的input_event函数就在该文件内。
首先从初始化函数input_init开始介绍:
static int __init input_init(void)
{
interr;
//注册input类,这里就是生成了sys/class/input目录,在该目录下会看到本文要用到的链接文件event0。
err= class_register(&input_class);
if(err) {
pr_err("unableto register input_dev class\n");
returnerr;
}
//在proc目录下建立input子系统目录及交互文件,即/proc/bus/input目录下的devices文件和handlers文件。
err= input_proc_init();
if(err)
gotofail1;
//注册一个主设备号为INPUT_MAJOR(13),次设备号为0~255,文件操作符为input_fops的字符设备。所有主设备号13的字符设备的操作最终都会转入到input_fops中。例如event0的主设备号为13,对其的操作会落在input_fops中
err= register_chrdev(INPUT_MAJOR, "input", &input_fops);
if(err) {
pr_err("unableto register char major %d", INPUT_MAJOR);
gotofail2;
}
return0;
fail2: input_proc_exit();
fail1: class_unregister(&input_class);
returnerr;
}
input_fops很简单,但它里面定义了一个重要input_open_file函数,它负责打开输入设备,我们来看看这个函数:
static int input_open_file(struct inode*inode, struct file *file)
{
structinput_handler *handler;
conststruct file_operations *old_fops, *new_fops = NULL;
interr;
err= mutex_lock_interruptible(&input_mutex);
if(err)
returnerr;
/*No load-on-demand here? */
//通过识别次设备号来获得当前的输入设备的handler
handler= input_table[iminor(inode) >> 5];
//得到该handler的文件操作指针file_operation
if(handler)
new_fops= fops_get(handler->fops);
mutex_unlock(&input_mutex);
/*
* That's _really_ odd. Usually NULL ->openmeans "nothing special",
* not "no device". Oh, well...
*/
if(!new_fops || !new_fops->open) {
fops_put(new_fops);
err= -ENODEV;
gotoout;
}
old_fops= file->f_op;
file->f_op= new_fops;
//用新获得的file_operation中的open来打开设备
err= new_fops->open(inode, file);
if(err) {
fops_put(file->f_op);
file->f_op= fops_get(old_fops);
}
fops_put(old_fops);
out:
returnerr;
}
从input_open_file函数的分析可以看出,input_fops只是输入子系统通用的file_operations,对于不同的输入子系统(如键盘、鼠标、触摸屏等),通过input_open_file函数会调用各自不同的file_operations。对于按键来说,它调用的是drivers/input目录下Evdev.c文件中的evdev_fops。
核心层就介绍这么多,下面介绍事件驱动层的内容。
事件处理层文件主要是用来支持输入设备并与用户空间交互,这部分代码一般不需要我们自己去编写,因为Linux内核已经自带有一些事件处理器,可以支持大部分输入设备,比如Evdev.c、mousedev.c、joydev.c等。对按键来说,用到的是Evdev.c文件(它在drivers/input目录下)。
首先从Evdev.c文件的初始化函数evdev_init开始介绍:
static int __init evdev_init(void)
{
//注册evdev_handler
returninput_register_handler(&evdev_handler);
}
evdev_handler类型就是前面介绍过的三个重要结构体中的一个——input_handler,它的定义为:
staticstruct input_handler evdev_handler = {
.event = evdev_event,
.connect = evdev_connect,
.disconnect = evdev_disconnect,
.fops = &evdev_fops,
.minor = EVDEV_MINOR_BASE,
.name = "evdev",
.id_table = evdev_ids,
};
其中evdev_connect主要用来连接input_dev和input_handler;evdev_event是把input事件发给所有的client。另外在该结构体内还定义了一个fops集合,它被赋值为evdev_fops的指针。evdev_fops就是在介绍核心层input.c文件时,提到的当处理按键输入子系统时,file_operations所对应的就是evdev_fops。在这里,我们只分析该集合中的evdev_open函数和evdev_read函数
evdev_open函数负责打开一个输入设备:
static int evdev_open(struct inode *inode,struct file *file)
{
structevdev *evdev;
structevdev_client *client;
//得到设备列表中的序号
inti = iminor(inode) - EVDEV_MINOR_BASE;
unsignedint bufsize;
interror;
if(i >= EVDEV_MINORS)
return-ENODEV;
error= mutex_lock_interruptible(&evdev_table_mutex);
if(error)
returnerror;
evdev= evdev_table[i]; //取出设备
if(evdev)
get_device(&evdev->dev); //增加计数
mutex_unlock(&evdev_table_mutex);
if(!evdev)
return-ENODEV;
bufsize= evdev_compute_buffer_size(evdev->handle.dev);
//分配并初始化client结构体
client= kzalloc(sizeof(struct evdev_client) +
bufsize* sizeof(struct input_event),
GFP_KERNEL);
if(!client) {
error= -ENOMEM;
gotoerr_put_evdev;
}
client->bufsize= bufsize;
spin_lock_init(&client->buffer_lock);
//使client与设备相关联,并把它挂接在链表中
client->evdev= evdev;
evdev_attach_client(evdev,client);
//evdev_open_device函数的作用是打开设备,当设备是第一次被打开时,则调用input_open_device函数。
error =evdev_open_device(evdev);
if(error)
gotoerr_free_client;
file->private_data= client;
nonseekable_open(inode,file);
return0;
err_free_client:
evdev_detach_client(evdev,client);
kfree(client);
err_put_evdev:
put_device(&evdev->dev);
returnerror;
}
evdev_read函数负责读取设备:
static ssize_t evdev_read(struct file*file, char __user *buffer,
size_tcount, loff_t *ppos)
{
structevdev_client *client = file->private_data;
structevdev *evdev = client->evdev;
structinput_event event;
size_tread = 0;
interror;
//对读取的数据大小进行判断
if(count != 0 && count < input_event_size())
return-EINVAL;
for(;;) {
//如果设备不存在,则退出
if(!evdev->exist)
return-ENODEV;
//如果缓存中没有数据可读而设备又存在,并且被设置为O_NONBLOCK方式,则退出
if(client->packet_head == client->tail &&
(file->f_flags & O_NONBLOCK))
return-EAGAIN;
/*
* count == 0 is special - no IO is done but wecheck
* for error conditions (see above).
*/
if(count == 0)
break;
while(read + input_event_size() <= count &&
evdev_fetch_next_event(client,&event)) {
//读取数据
if(input_event_to_user(buffer + read, &event))
return-EFAULT;
read+= input_event_size();
}
if(read)
break;
//等待缓存中是否有数据可读,或设备是否被移走
if(!(file->f_flags & O_NONBLOCK)) {
error= wait_event_interruptible(evdev->wait,
client->packet_head!= client->tail ||
!evdev->exist);
if(error)
returnerror;
}
}
至此,基于输入子系统的按键流程就介绍完了。