gpio-key驅動分析

前言

Linux內核中的gpio-keys.c(driver/input/keyboard/gpio-keys.c)統一了所有關於按鍵的驅動實現方式。其良好的代碼架構可以兼容幾乎所有平臺的關於按鍵的處理流程。如果需要在目標平臺實現關於按鍵的驅動程序,完全可以直接使用該驅動,幾乎不用自己實現任何代碼。

測試平臺

本文介紹的代碼在以下平臺進行測試:

  • 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按鍵事件的處理,簡化了驅動的處理流程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章