测试平台
本文介绍的代码在以下平台进行测试:
- Host:Ubuntu14.04
- Target:Firefly-rk3288
- Compiler:arm-linux-android-gcc
架构
gpio-keys驱动基于Linux内核的input子系统实现,设备驱动以platform_device的方式注册到系统中。驱动对于按键基于中断的处理方式实现,并且通过input子系统将按键事件上报到应用层,供应用程序解析使用。
DTS配置
位于 Documentation/devicetree/bindings/gpio/gpio-keys.txt介绍了对于gpio-keys驱动程序的Device-Tree bingdings。其支持的属性定义如下,关于DTS基本语法的总结可以参见。
Required properties
- - compatible = "gpio-keys";
该属性定义了设备的兼容性。
Optional properties
- -autorepeat: Boolean,启动input子系统的auto repeat特性。
Subnode properties
每一个button(key)都对应为gpio-keys的一个子节点,子节点的属性包括:
- - gpios: device-tree gpio规格属性。
- - label: key的描述性名称。
- - linux,code: input子系统所定义的按键代码,参见:include/dt-bindings/input/input.h关于keys和buttons的code定义。
Optional subnode-properties
- -linux,input-type:定义该key/button所依赖的event type(input子系统定义),默认为1 == EV_KEY。
- -debounce-interval:定义该key/button的去抖间隔,默认为5ms。
- -gpio-key,wakeup:Boolean,标识该key可以唤醒系统,例如,Android系统的power-key。
Example nodes:
gpio_keys_test { compatible = "gpio-keys"; #address-cells = <1>; #size-cells = <0>; autorepeat; powerkey { label = "power key"; linux,code = <116>; gpios = <&gpio0 GPIO_A5 GPIO_ACTIVE_LOW>; gpio-key,wakeup; debounce-interval = <5>; }; };
基本数据结构
/* key/button的基本配置参数 */ struct gpio_keys_button { unsigned int code; /* input event code (KEY_*, SW_*) */ int gpio; /* -1 if this key does not support gpio */ int active_low; const char *desc; unsigned int type; /* input event type (EV_KEY, EV_SW, EV_ABS) */ int wakeup; /* configure the button as a wake-up source */ int debounce_interval; /* debounce ticks interval in msecs */ bool can_disable; int value; /* axis value for EV_ABS */ unsigned int irq; /* Irq number in case of interrupt keys */ }; /*key/button控制逻辑配置参数*/ struct gpio_button_data { const struct gpio_keys_button *button; struct input_dev *input; struct timer_list timer; struct work_struct work; unsigned int timer_debounce; /* in msecs */ unsigned int irq; spinlock_t lock; bool disabled; bool key_pressed; }; /*key/button platform配置参数*/ struct gpio_keys_platform_data { struct gpio_keys_button *buttons; int nbuttons; unsigned int poll_interval; /* polling interval in msecs - for polling driver only */ unsigned int rep:1; /* enable input subsystem auto repeat */ int (*enable)(struct device *dev); void (*disable)(struct device *dev); const char *name; /* input device name */ }; /*key/button plaform_device data配置参数,该结构作为platform data注册到platform设备总线*/ struct gpio_keys_drvdata { const struct gpio_keys_platform_data *pdata; struct input_dev *input; struct mutex disable_lock; struct gpio_button_data data[0]; };
设备注册
gpio-keys驱动是以platform_driver的身份注册到系统中的,所以其需要定义platfrom_driver结构,如下:
static struct platform_driver gpio_keys_device_driver = { .probe = gpio_keys_probe,//gpio-keys驱动初始化函数 .remove = gpio_keys_remove,//gpio-keys驱动卸载处理函数 .driver = { .name = "gpio-keys", .owner = THIS_MODULE, .pm = &gpio_keys_pm_ops, .of_match_table = of_match_ptr(gpio_keys_of_match),//定义驱动的兼容属性,具体定义如下: } }; static struct of_device_id gpio_keys_of_match[] = { { .compatible = "gpio-keys", }, { }, };
设备probe流程
下面主要分析一下驱动的probe主要流程,较为细节的代码请参照内核代码。
static int gpio_keys_probe(struct platform_device *pdev) { ... ... if (!pdata) { pdata = gpio_keys_get_devtree_pdata(dev);------------------------------------------->(1) 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();--------------------------------------------------------(2) if (!ddata || !input) { dev_err(dev, "failed to allocate state\n"); error = -ENOMEM; goto fail1; } 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; input->close = gpio_keys_close; ... ... /* Enable auto repeat feature of Linux input subsystem */ if (pdata->rep) __set_bit(EV_REP, input->evbit); for (i = 0; i < pdata->nbuttons; i++) {--------------------------------------------(3) 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);----------------(4) if (error) { dev_err(dev, "Unable to export keys/switches, error: %d\n", error); goto fail2; } error = input_register_device(input);---------------------------------------------(5) if (error) { dev_err(dev, "Unable to register input device, error: %d\n", error); goto fail3; } device_init_wakeup(&pdev->dev, wakeup); return 0; ... ... }
- (1)解析DTS关于gpio-keys的属性定义,创建、初始化gpio_keys_platform_data。
- (2)分配、初始化input设备。
- (3)遍历所有key/button,注册key/buton所需的资源(gpio、irq等)。
- (4)注册gpio-keys在sys文件系统下的访问接口属性,gpio-keys设备在sys文件系统路径为:/sys/devices/gpio_keys_test.32,其中gpio_keys_test为DTS中设备设备节点名称。
- (5)注册input设备。
设备资源解析
gpio_keys_get_devtree_pdata函数完成将DTS节点的设备属性翻译成gpio_keys_platform_data结构,具体执行流程如下。
gpio_keys_get_devtree_pdata(struct device *dev) { ... ... nbuttons = of_get_child_count(node);-----------------------------------------------(1) if (nbuttons == 0) { error = -ENODEV; goto err_out; } pdata = kzalloc(sizeof(*pdata) + nbuttons * (sizeof *button), GFP_KERNEL); if (!pdata) { error = -ENOMEM; goto err_out; } pdata->buttons = (struct gpio_keys_button *)(pdata + 1); pdata->nbuttons = nbuttons; pdata->rep = !!of_get_property(node, "autorepeat", NULL); i = 0; for_each_child_of_node(node, pp) {------------------------------------------------(2) int gpio; enum of_gpio_flags flags; if (!of_find_property(pp, "gpios", NULL)) { pdata->nbuttons--; dev_warn(dev, "Found button without gpios\n"); continue; } gpio = of_get_gpio_flags(pp, 0, &flags); if (gpio < 0) { error = gpio; if (error != -EPROBE_DEFER) dev_err(dev, "Failed to get gpio flags, error: %d\n", error); goto err_free_pdata; } button = &pdata->buttons[i++]; button->gpio = gpio; button->active_low = flags & OF_GPIO_ACTIVE_LOW; if (of_property_read_u32(pp, "linux,code", &button->code)) { dev_err(dev, "Button without keycode: 0x%x\n", button->gpio); error = -EINVAL; goto err_free_pdata; } button->desc = of_get_property(pp, "label", NULL); if (of_property_read_u32(pp, "linux,input-type", &button->type)) button->type = EV_KEY; button->wakeup = !!of_get_property(pp, "gpio-key,wakeup", NULL); if (of_property_read_u32(pp, "debounce-interval", &button->debounce_interval)) button->debounce_interval = 5; } if (pdata->nbuttons == 0) { error = -EINVAL; goto err_free_pdata; } return pdata; }
- (1)获取keys/button的节点数量,初始化input系统的autorepeat属性。
- (2)遍历DTS所有子节点,依次读取key/button的gpios、flags、linux,code、linux,input-type、gpio-key,wakeup、debounce-interval属性字段。
按键注册
gpio_keys_setup_key主要完成gpio的申请、配置以及gpio所关联的irq的申请、初始化配置功能,具体执行流程如下。
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) { ...... if (gpio_is_valid(button->gpio)) { error = gpio_request_one(button->gpio, GPIOF_IN, desc);----------------------------->(1) if (error < 0) { dev_err(dev, "Failed to request GPIO %d, error %d\n", button->gpio, error); return error; } if (button->debounce_interval) {---------------------------------------------------->(2) 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);--------------------------------------------------->(3) 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);------------------------------>(4) setup_timer(&bdata->timer, gpio_keys_gpio_timer, (unsigned long)bdata); isr = gpio_keys_gpio_isr; irqflags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING; } else { ... ... } input_set_capability(input, button->type ?: EV_KEY, button->code); /* * 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; error = request_any_context_irq(bdata->irq, isr, irqflags, desc, bdata);------------>(5) if (error < 0) { dev_err(dev, "Unable to claim irq %d; error %d\n", bdata->irq, error); goto fail; } ... ... }
- (1)申请gpio资源,注意gpio_request_one的参数,由于key/button都为GPIO属性信号所以其第二个参数为GPIOF_IN。
- (2)初始化key/button去抖所需要的定时器,注意gpio_set_debounce可能会失败,如果失败的话(4)的setup_timer会完成key/button的去抖功能。
- (3)获取gpio所对应的irq,该irq为系统维护该gpio中断相关的所有操作的句柄参数。
- (4)初始化key/button中断处理的bottom level处理workqueue,初始化key/button去抖定时器,gpio_keys_gpio_timer为定时器的超时处理函数,该函数十分的简单的,其调用schedule_work(&bdata->work);来调度中断的workqueue。初始化中断触发方式为:IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,即边沿触发。
- (5)申请中断。request/_any/_context/_irq申请中断处理所需的资源,并激活该interrupt line。注意该函数会根据中断描述的配置选择hartirq或者threaded方式的中断top level处理。
中断处理
中断处理-top level
gpio-keys驱动的上半部处理十分的简单,处理过程如下
static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id) { struct gpio_button_data *bdata = dev_id; BUG_ON(irq != bdata->irq); if (bdata->button->wakeup)------------------------------------------------>(1) pm_stay_awake(bdata->input->dev.parent); if (bdata->timer_debounce) mod_timer(&bdata->timer, jiffies + msecs_to_jiffies(bdata->timer_debounce));--------------->(2) else schedule_work(&bdata->work); return IRQ_HANDLED; } static void gpio_keys_gpio_timer(unsigned long _data) { struct gpio_button_data *bdata = (struct gpio_button_data *)_data; schedule_work(&bdata->work);------------------------------------------->(3) }
- (1)如果key/button具有系统唤醒功能,调用电源相关的处理过程。
- (2)key/button的timer_debounce肯定为大于0,所以,调用mod_timer启动去抖处理定时器。
- (3)去抖定时器超时后会调用gpio_keys_gpio_timer定时器超时处理函数,该函数的实现十分的简单,其就做一件事,即调度key/button的workqueue。
中断处理-bottom level
上文提到过gpio-keys中断下半部的处理方式为workqueue,中断上半部的去抖定时器如果超时的话,会触发workqueue调度,workqueue会在合适的时间点执行。下面为workqueue的处理流程。
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);-------------------------------------------------->(1) if (bdata->button->wakeup) pm_relax(bdata->input->dev.parent); } 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;--->(2) if (type == EV_ABS) { if (state) input_event(input, type, button->code, button->value); } else { input_event(input, type, button->code, !!state);--------------------------------->(3) } input_sync(input); }
- (1)上报key/button的gpio状态。
- (2)读取gpio的I/O状态,并根据key/button的active_low状态将其转换为key/button的state。
- (3)通过input子系统上报key/button的按键事件。
应用测试
下面举一个例子,讲解如何通过DTS配置gpio-keys驱动,以及如何通过应用程序监测key/button的按键事件。
设备DTS配置
` gpio_keys_test { compatible = "gpio-keys"; #address-cells = <1>; #size-cells = <0>; autorepeat; powerkey { label = "power key"; linux,code = <116>; gpios = <&gpio0 GPIO_A5 GPIO_ACTIVE_LOW>; gpio-key,wakeup; debounce-interval = <5>; }; };`
上面的DTS配置一个gpio0 GPIO_A5为一个按键,配置按键事件,启用wake-up功能。
gpio-keys驱动使能
使能gpio-keys驱动的驱动配置路径如下:
Device Driver---> Input device support---> Keyboards-------------> GPIO buttons
保存内核配置,重新编译内核,将DTB和zImage文件下载到开发板。
按键事件应用测试
经过上述的配置之后,系统启动之后,我们会在/dev/input目录下看到对应于设备的设备文件,本例为event2。通过下面的应用程序就可以读取设备的按键事件了,应用程序如下:
#include <linux/input.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <stdio.h> #define INPUT_DEV "/dev/input/event2" int main(int argc, char * const argv[]) { int fd = 0; struct input_event event; int ret = 0; fd = open(INPUT_DEV, O_RDONLY); while(1){ ret = read(fd, &event, sizeof(event)); if(ret == -1) { perror("Failed to read.\n"); exit(1); } if(event.type != EV_SYN) { printf("type:%d, code:%d, value:%d\n", event.type, event.code, event.value); } } return 0; }
程序的输出结果如下:
type:1, code:116, value:1//type:EV_KEY, code:116:power key, value:1,按键按下 type:1, code:116, value:0 type:1, code:116, value:1 type:1, code:116, value:0 type:1, code:116, value:1 type:1, code:116, value:0
总结
gpio-keys驱动基本统一了Linux系统所有按键相关的驱动模式,我们开发按键驱动时可以直接配置使用该驱动。另外,该驱动借助input子系统与用户空间的应用程序进行交互,省去了编写文件系统相关的接口(省去了file_operations结构的配置,input子系统已经做了这部分工作)的工作。可以使驱动专注于key/button按键事件的处理,简化了驱动的处理流程。