android系統移植之按鍵驅動篇
平臺:MX53_QSB開發板
MX53_QSB開發板上一起有四個按鍵,分別為RESET,POWER,USER1,USER2。其中RESET為純硬體復位按鍵,無須軟體控制。POWER,USER1,USER2三個按鍵均需要程式控制。預設BSP包中將三個按鈕全設定為上升和下降沿觸發,當系統起來後,按下POWER鍵,進入睡眠狀態,這時再按下POWER鍵喚醒時,系統系統被喚醒,但是一旦手鬆下,又觸發了POWER鍵的中斷,系統又睡下去了。在進入睡眠狀態後,只有按USER1和USER2這兩個鍵,才能正常喚醒。因此,這裡有BUG需修復。
按鍵驅動有兩個,一個為矩陣鍵盤驅動,路徑為:
\drivers\input\keyboard\mxc_keyb.c
一個為GPIO介面的鍵盤驅動,路徑為:
\drivers\input\keyboard\gpio_keys.c
前者用於多按鍵的情況,如果按鍵比較少,後者就可以了,一般情況下,android系統只需幾個按鍵就可以了,所以大多數情況下,都是使用的gpio_keys.c。下面我們將詳細分析該驅動的工作流程。
在module_init函式中,在總線上註冊名為gpio-keys的驅動,這時將夫在總線上查詢是否存在同名的裝置。系統初始化時,mx53_loco.c中,mxc_board_init函式已呼叫了按鍵初始化函式loco_add_device_buttons(),同時pdev的資料結構體中定義了按鍵的相關資訊如下:
#define GPIO_BUTTON(gpio_num, ev_code, act_low,descr, wake) \
{ \
.gpio = gpio_num, \
.type = EV_KEY, \
.code = ev_code, \
.active_low = act_low, \
.desc = "btn " descr, \
.wakeup = wake, \
}
static struct gpio_keys_button loco_buttons[] = {
GPIO_BUTTON(MX53_nONKEY,KEY_POWER, 1, "power", 0),
GPIO_BUTTON(USER_UI1,KEY_BACK, 1, "back", 0),
GPIO_BUTTON(USER_UI2,KEY_HOME, 1, "home", 0),
};
static struct gpio_keys_platform_dataloco_button_data = {
.buttons = loco_buttons,
.nbuttons = ARRAY_SIZE(loco_buttons),
};
可見,結構體定義了三個GPIO,分別為power,back以及home。注意GPIO_BUTTON函式中的實參,第一個為對應的GPIO,純硬體特性,第二個為按鍵的鍵值,在linux/input.h中定義:
#defineKEY_POWER 116
#defineKEY_HOME 102
#defineKEY_BACK 158
第三個引數為1,表明按下去為1,擡起為0;第四個引數為按鍵名稱描述,無關緊要;第5個引數為wakeup,看名稱好像與休眠喚醒有關,實際測試修改為1後,沒有發現有什麼異常。這幾個引數是後續新增新的按鍵,或者更改按鍵功能的關鍵。
再回到gpio-keys.c中,找到同名裝置後,探測函式gpio_keys_probe得到執行,呼叫input_allocate_device函式建立一個input裝置,再通過一個for迴圈,呼叫gpio_keys_setup_key和input_set_capability函式,設定上表中列出的三個IO口的中斷函式以及按鍵功能。後面用到了sysfs_create_group函式建立了基於sys系統的檔案屬性組,具體可以在?sys/devices/platform/gpio-keys目錄下找到gpio_keys_attr_group結構體中attrs組對應的gpio_keys_attrs結構體中的幾個屬性檔案,gpio_keys_attrs結構體描述如下:
static struct attribute *gpio_keys_attrs[] = {
&dev_attr_keys.attr,
&dev_attr_switches.attr,
&dev_attr_disabled_keys.attr,
&dev_attr_disabled_switches.attr,
NULL,
};
dev_attr_disabled_keys和dev_attr_disabled_switches在前面做了如下宣告:
static DEVICE_ATTR(disabled_keys, S_IWUSR |S_IRUGO,
gpio_keys_show_disabled_keys,
gpio_keys_store_disabled_keys);
static DEVICE_ATTR(disabled_switches, S_IWUSR |S_IRUGO,
gpio_keys_show_disabled_switches,
gpio_keys_store_disabled_switches);
在android系統終端,我們可以進入該路徑檢視是否存在,以及他們的檔案屬性如下:
注意上面四個檔案的讀寫屬性。
可見,這裡留有在系統中操作按鍵的後門,具體以後再分析。接下來,呼叫input_register_device函式向輸入子系統註冊input_dev,結束探測函式初始化。
探測函式的關鍵點在gpio_keys_setup_key函式中,相關程式碼如下:
static int __devinit gpio_keys_setup_key(structplatform_device *pdev,
struct gpio_button_data *bdata,
struct gpio_keys_button *button)
{
char *desc= button->desc ? button->desc : "gpio_keys";//從loco_buttons陣列中獲得按鍵的描述名稱
structdevice *dev = &pdev->dev;
unsignedlong irqflags;
intirq, error;
//傳入引數: 過期時間,回撥函式,上下文
//當計時器過期時,回撥函式gpio_keys_timer將得到執行
setup_timer(&bdata->timer,gpio_keys_timer, (unsigned long)bdata);//初始化計時器
INIT_WORK(&bdata->work,gpio_keys_work_func);
error= gpio_request(button->gpio, desc);//請求使用GPIO
if(error < 0)
{
dev_err(dev,"failed to request GPIO %d, error %d\n",button->gpio, error);
gotofail2;
}
error= gpio_direction_input(button->gpio);//設定指定的GPIO為輸入模式
if(error < 0)
{
dev_err(dev,"failed to configure direction for GPIO %d, error %d\n",
button->gpio,error);
gotofail3;
}
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);
gotofail3;
}
irqflags= IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;
//irqflags= IRQF_TRIGGER_FALLING;//lqm changed.
/*
* 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;
error= request_irq(irq, gpio_keys_isr, irqflags, desc, bdata);//請求按鍵中斷
……
}
該函式使用了帶定時器延時的中斷機制,用於按鍵去抖動。即中斷的執行在定時器到來之後執行,這樣能夠有效的去除按鍵抖動。同時,中斷使用工作佇列的機制。
整個按鍵中斷的工作呼叫有點複雜,下面逐步解析:
首先,上面函式中setup_timer函式初始化定時器,第二個實參為一個名為gpio_keys_timer的函式,一旦計時器過期,該函式將得到執行。setup_timer函式需和mod_timer函式配合使用,在按鍵中斷的頂半部,即gpio_keys_isr函式,會判斷debounce_interval是否為0,若為非0,則呼叫mod_timer函式延時debounce_interval ms,再觸發定時器中斷,即gpio_keys_timer函式得到執行。否則,直接排程工作佇列,執行中斷底半部。
回到gpio_keys_setup_key函式,在初始化完計時器後,再呼叫INIT_WORK函式初始化工作佇列,初始化函式有一個實參函式gpio_keys_work_func,即一旦排程相應佇列名,該函式將得到執行。
接下來是IO口的中斷初始化,呼叫gpio_request函式申請相應的GPIO,呼叫gpio_direction_input函式將對應的GPIO設定為輸入,gpio_to_irq函式通過指定GPIO口對映到指定的IRQ中斷號,request_irq函式申請中斷。
值得注意的是,三個按鍵通過request_irq申請中斷時,共用了同一個中斷函式gpio_keys_isr,那麼程式是怎麼判斷具體是哪個按鍵觸發的呢?
帶著這個問題,我們將整個中斷的執行過程疏理一遍:
第一步:硬體板按下按鍵,電平由高變低,中斷被觸發,中斷函式gpio_keys_isr被呼叫。程式碼如下:
static irqreturn_t gpio_keys_isr(int irq, void*dev_id)
{
structgpio_button_data *bdata = dev_id;
structgpio_keys_button *button = bdata->button;
BUG_ON(irq!= gpio_to_irq(button->gpio));
if(button->debounce_interval)//產生按鍵中斷後,用計時器延時button->debounce_interval ms之後,再執行按鍵處理
mod_timer(&bdata->timer,jiffies+ msecs_to_jiffies(button->debounce_interval));
else
schedule_work(&bdata->work);
returnIRQ_HANDLED;
}
中斷函式會判斷debounce_interval是否為0,debounce_interval代表計時器需要延時的時間,單位為毫秒。為了確定程式具體如何執行,我們首先需分析出它的值。以下是推理邏輯:
button->debounce_interval à 找到button結構體來源
*bdata = dev_id&& *button = bdata->button à *button = dev_id->button
staticirqreturn_t gpio_keys_isr(int irq, void *dev_id);
error =request_irq(irq, gpio_keys_isr, irqflags, desc, bdata);
上面兩個函式,gpio_keys_isr為request_irq的一個實參,根到request_irq程式碼中,最終會呼叫kernel\irq\manage.c中的request_threaded_irq函式,部分程式碼如下:
int request_threaded_irq(unsigned int irq,irq_handler_t handler,
irq_handler_t thread_fn, unsigned longirqflags,
const char *devname, void *dev_id)
{
……
action= kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if(!action)
return-ENOMEM;
action->handler= handler;
action->thread_fn= thread_fn;
action->flags= irqflags;
action->name= devname;
action->dev_id= dev_id;
chip_bus_lock(irq,desc);
retval= __setup_irq(irq, desc, action);
chip_bus_sync_unlock(irq,desc);
if(retval)
kfree(action);
#ifdef CONFIG_DEBUG_SHIRQ
if(!retval && (irqflags & IRQF_SHARED)) {
/*
* It's a shared IRQ -- the driver ought to beprepared for it
* to happen immediately, so let's makesure....
* We disable the irq to make sure that a'real' IRQ doesn't
* run in parallel with our fake.
*/
unsignedlong flags;
disable_irq(irq);
local_irq_save(flags);
handler(irq,dev_id);
local_irq_restore(flags);
enable_irq(irq);
}
#endif
returnretval;
}
繼續根到__setup_irq函式,後面不繼續分析了,最終會將request_irq中的兩個實參irq和bdata賦給中斷服務函式gpio_keys_isr中的兩個實參,也就是說,*button =dev_id->button等價於*button=bdata->button。
在gpio_keys_probe函式中,bdata->button = button,*button = &pdata->buttons[i];可以推斷出*button= pdata->buttons[i],而*pdata = pdev->dev.platform_data,那麼*button= pdev->dev.platform_data-> buttons[i],也就是mx53_loco.c中的如下結構體陣列中的一組:
static struct gpio_keys_button loco_buttons[] = {
GPIO_BUTTON(MX53_nONKEY,KEY_POWER, 1, "power", 0),
//GPIO_BUTTON(USER_UI1,KEY_BACK, 1, "back", 0),
GPIO_BUTTON(USER_UI2,KEY_HOME, 1, "home", 0),
GPIO_BUTTON(USER_UI1,KEY_RIGHT, 1, "right", 0),//test by lqm.
};
由此可以推斷出,前面的button->debounce_interval即loco_buttons[i]-> debounce_interval。陣列loco_buttons的結構體如下:
struct gpio_keys_button {
/*Configuration parameters */
intcode; /* input event code(KEY_*, SW_*) */
intgpio;
intactive_low;
char*desc;
inttype; /* input event type(EV_KEY, EV_SW) */
intwakeup; /* configure thebutton as a wake-up source */
intdebounce_interval; /* debounce ticksinterval in msecs */
boolcan_disable;
};
由於程式中並沒有對debounce_interval賦值,因此預設debounce_interval為0。回到gpio_keys_isr函式,由於button->debounce_interval為0,那麼計時器機制沒有啟動,直接執行排程函式schedule_work。
第二步:中斷佇列gpio_keys_work_func函式得到執行。它又會呼叫gpio_keys_report_event函式,程式碼如下:
static void gpio_keys_report_event(structgpio_button_data *bdata)
{
structgpio_keys_button *button = bdata->button;
structinput_dev *input = bdata->input;
unsignedint type = button->type ?: EV_KEY;
intstate = (gpio_get_value(button->gpio) ? 1 : 0) ^ button->active_low;//獲得按鍵資訊,同時與1異或?
input_event(input,type, button->code, !!state);
input_sync(input);//事件同步,它告知事件的接收者驅動已經發出了一個完整的報告
}
這裡是中斷底半部,變數state經gpio_get_value函式獲得當前IO口的電平狀態,再通過input_event函式將當前電平狀態以及button->code上傳給輸入子系統。button->code即loco_buttons數組裡面GPIO_BUTTON中的第二個引數,它定義了按鍵的作用。最後呼叫input_sync函式同事事件,結束一次按鍵的操作。
由於前面分析的debounce_interval值為0,因此執行流程比較簡單,如果它不會0,將會啟用計時器機制,流程會複雜一些。
預設三個按鍵的功能為開關機,主頁和返回三個功能,如果我們需要修改對應按鍵的功能,是否修改上面的GPIO_BUTTON->ev_code就可以了呢?比如,我們需要將back鍵改為right鍵,做如下修改:
static struct gpio_keys_button loco_buttons[] = {
GPIO_BUTTON(MX53_nONKEY,KEY_POWER, 1, "power", 0),
//GPIO_BUTTON(USER_UI1,KEY_BACK, 1, "back", 0),
GPIO_BUTTON(USER_UI2,KEY_HOME, 1, "home", 0),
GPIO_BUTTON(USER_UI1,KEY_RIGHT, 1, "right", 0),//test by lqm.
};
編譯核心,這樣按鍵功能就改變了嗎?答案是否定的。因為android並沒有直接使用對映後的鍵值,而且對其再進行了一次對映,從核心標準鍵值到android所用鍵值的對映表定義在android檔案系統的/system/usr/keylayout目錄下。標準的對映檔案為qwerty.kl,定義如下:
key 399 GRAVE
key 2 1
key 3 2
key 4 3
key 5 4
key 6 5
key 7 6
key 8 7
key 9 8
key 10 9
key 11 0
key 158 BACK WAKE_DROPPED
key 230 SOFT_RIGHT WAKE
key 60 SOFT_RIGHT WAKE
key 107 ENDCALL WAKE_DROPPED
key 62 ENDCALL WAKE_DROPPED
key 229 MENU WAKE_DROPPED
key 139 MENU WAKE_DROPPED
key 59 MENU WAKE_DROPPED
key 127 SEARCH WAKE_DROPPED
key 217 SEARCH WAKE_DROPPED
key 228 POUND
key 227 STAR
key 231 CALL WAKE_DROPPED
key 61 CALL WAKE_DROPPED
key 232 DPAD_CENTER WAKE_DROPPED
key 108 DPAD_DOWN WAKE_DROPPED
key 103 DPAD_UP WAKE_DROPPED
key 102 HOME WAKE
key 105 DPAD_LEFT WAKE_DROPPED
key 106 DPAD_RIGHT WAKE_DROPPED
key 115 VOLUME_UP
key 114 VOLUME_DOWN
key 116 POWER WAKE
key 212 CAMERA
key 16 Q
key 17 W
key 18 E
key 19 R
key 20 T
key 21 Y
key 22 U
key 23 I
key 24 O
key 25 P
key 26 LEFT_BRACKET
key 27 RIGHT_BRACKET
key 43 BACKSLASH
key 30 A
key 31 S
key 32 D
key 33 F
key 34 G
key 35 H
key 36 J
key 37 K
key 38 L
key 39 SEMICOLON
key 40 APOSTROPHE
key 14 DEL
key 44 Z
key 45 X
key 46 C
key 47 V
key 48 B
key 49 N
key 50 M
key 51 COMMA
key 52 PERIOD
key 53 SLASH
key 28 ENTER
key 56 ALT_LEFT
key 100 ALT_RIGHT
key 42 SHIFT_LEFT
key 54 SHIFT_RIGHT
key 15 TAB
key 57 SPACE
key 150 EXPLORER
key 155 ENVELOPE
key 12 MINUS
key 13 EQUALS
key 215 AT
android按鍵的處理是Window Manager負責,主要的對映轉換實現在android原始碼frameworks/base/libs/ui/EventHub.cpp此檔案處理來自底層的所有輸入事件,並根據來源對事件進行分類處理,對於按鍵事件,它首先記錄驅動名稱,再獲取環境變數ANDROID_ROOT為系統路徑,預設是/system,定義在android原始碼/system/core/rootdir/init.rc檔案中。然後查詢路徑為"系統路徑/usr/keylayout/驅動名稱.kl"的按鍵對映檔案,如果不存在則預設用路徑為"系統路徑/usr/keylayout/qwerty.kl"。這個預設的按鍵對映檔案,對映完成後再把經對映得到的android按鍵碼值發給上層應用程式。所以我們可以在核心中定義多個按鍵裝置,然後為每個裝置設定不同的按鍵對映檔案,不定義則會預設用qwerty.kl。
有了上面的分析,我們不難發現,上述更改是不可能有效果的,只能越改越不能用。相反,如果僅僅只需要更改某個按鍵的功能,根本就不用改核心,只需重新定義android系統的gpio-keys.kl即可。
進android系統後找到該檔案,
可以發現,在在gpio-keys.kl和qwerty.kl兩個檔案,因此a ndroid系統會從gpio-keys.kl中查詢鍵值進行對映。裡面內容如下:
key 102 HOME WAKE
key 158 BACK WAKE
這不正是開發板上android的兩個功能鍵嗎?對於第三個POWER鍵,是用於開關機的,不屬android功能鍵範疇,因此無須對映。
修改按鍵功能有兩種方法:
方法一:直接修改gpio-keys.kl檔案的內容,比如我們想將BACK鍵改成右鍵,我們只需做如下修改:
key 102 HOME WAKE
key 106 DPAD_RIGHT WAKE
注意上面的106以及按鍵名稱,都是從qwerty.kl裡面找的,千萬不要在linux核心的input.h中查詢。DPAD_RIGHT不能更改為RIGHT,否則android無法識別。
方法二:修改核心中mx53_loco.c中的陣列如下:
static struct gpio_keys_button loco_buttons[] = {
GPIO_BUTTON(MX53_nONKEY,KEY_POWER, 1, "power", 0),
//GPIO_BUTTON(USER_UI1,KEY_BACK, 1, "back", 0),
GPIO_BUTTON(USER_UI2,KEY_HOME, 1, "home", 0),
GPIO_BUTTON(USER_UI1,KEY_RIGHT, 1, "right", 0),//test by lqm.
};
再修改上面的gpio-keys.kl檔案,內容如下:
key 102 HOME WAKE
key 106 DPAD_RIGHT WAKE
方法二相比方法一,多了一個步驟,但是推薦採用方法二,這樣更容易理解,也不易混淆。