用户输入系统
8.1 用户输入系统结构和移植内容
Android中,用户输入系统的结构相对简单,主要的输入硬件设备是键盘、触摸屏、轨迹球等。
在Android的上层中,可以通过获得这些设备产生的事件,并对设备的事件做出响应。在Java框架和应用程序层,通常使用运动事件获得触摸屏、轨迹球等设备的信息,用按键事件获得各种键盘的信息。
Android用户输入系统的基本层次结构如图8-1所示。
图8-1 Android用户输入系统的基本层次结构
8.1.1 用户输入系统的结构
Android用户输入系统的结构比较简单,自下而上包含了驱动程序、本地库处理部分、Java类对输入事件的处理、对Java程序的接口。Android用户输入系统的结构如图8-2所示。
图8-2 用户输入系统的结构
如图8-2所示,自下而上,Android的用户输入系统分成几个部分:
8.1.2 移植的内容
移植Android的用户输入系统,主要的工作分成以下两个部分:
由于Android用户输入部分的“硬件抽象层”就是libui库中的EventHub,这部分是系统标准的部分。因此,在实现特定硬件平台的Android系统的时候,用户输入的硬件抽象层通常情况下不做改变。
EventHub使用Linux标准的input设备作为输入设备,其中又以实用Event设备居多。在这种情况下,为了实现Android系统的输入,也必须使用Linux标准input驱动程序作为标准的输入。
由于标准化程度比较高,实现用户输入系统,在用户空间一般不需要更改代码。唯一的情况是使用不同的kl和kcm文件,使用按键的布局和按键字符映射关系。
8.2 移植的要点
8.2.1 input驱动程序
Input驱动程序是Linux输入设备的驱动程序,分成游戏杆(joystick)、鼠标(mouse和mice)和事件设备(Event queue)3种驱动程序。其中事件驱动程序是目前通用的驱动程序,可支持键盘、鼠标、触摸屏等多种输入设备。
Input驱动程序的主设备号是13,3种驱动程序的设备号分配如下所示。
实际上,每一种Input设备占用5位,因此每种设备包含的个数是32个。
Event设备在用户空间大多使用read、ioctl、poll等文件系统的接口进行操作,read用于读取输入信息,ioctl用于获得和设置信息,poll调用可以进行用户空间的阻塞,当内核有按键等中断时,通过在中断中唤醒poll的内核实现,这样在用户空间的poll调用也可以返回。
Event设备在文件系统中的设备节点为:/dev/input/eventX。
主设备号为13,次设备号递增生成,为64~95,各个具体的设备在misc、touchscreen,keyboard等目录中。
Event输入驱动的架构如图8-3所示。
输入设备驱动程序的头文件:include/linux/input.h。
输入设备驱动程序的核心和Event部分代码分别是:drivers/input/input.c和drivers/input/ evdev.c。
图8-3 Event设备驱动的架构
input.h中定义了struct input_dev结构,它表示Input驱动程序的各种信息,对于Event设备分为同步设备、键盘、相对设备(鼠标)、绝对设备(触摸屏)等。
input_dev中定义并归纳了各种设备的信息,例如按键、相对设备、绝对设备、杂项设备、LED、声音设备,强制反馈设备、开关设备等。
- struct input_dev {
- const char *name; // 设备名称
- const char *phys; // 设备在系统的物理路径
- const char *uniq; // 统一的ID
- struct input_id id; // 设备ID
- unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; // 事件
- unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; // 按键
- unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; // 相对设备
- unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; // 绝对设备
- unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; // 杂项设备
- unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; // LED
- unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; // 声音设备
- unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; // 强制反馈备
- unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; // 开关设备
- unsigned int keycodemax; // 按键码的最大值
- unsigned int keycodesize; // 按键码的大小
- void *keycode; // 按键码
- int (*setkeycode)(struct input_dev *dev, int scancode, int keycode);
- int (*getkeycode)(struct input_dev *dev, int scancode, int *keycode);
- struct ff_device *ff;
- unsigned int repeat_key;
- struct timer_list timer;
- int sync;
- int abs[ABS_MAX + 1];
- int rep[REP_MAX + 1];
- unsigned long key[BITS_TO_LONGS(KEY_CNT)];
- unsigned long led[BITS_TO_LONGS(LED_CNT)];
- unsigned long snd[BITS_TO_LONGS(SND_CNT)];
- unsigned long sw[BITS_TO_LONGS(SW_CNT)];
- int absmax[ABS_MAX + 1]; // 绝对设备相关内容
- int absmin[ABS_MAX + 1];
- int absfuzz[ABS_MAX + 1];
- int absflat[ABS_MAX + 1]; // 设备相关的操作
- int (*open)(struct input_dev *dev);
- void (*close)(struct input_dev *dev);
- int (*flush)(struct input_dev *dev, struct file *file);
- int (*event)(struct input_dev *dev, unsigned int type,
- unsigned int code, int value);
- struct input_handle *grab;
- spinlock_t event_lock;
- struct mutex mutex;
- unsigned int users;
- int going_away;
- struct device dev;
- struct list_head h_list;
- struct list_head node;
- };
在具体的Event驱动程序的实现中,如果得到按键的事件,通常需要通过以下的接口向上进行通知,这些内容也在input.h中定义如下所示:
- void input_event(struct input_dev *dev, unsigned int type,
- unsigned int code, int value);
- void input_inject_event(struct input_handle *handle,
- unsigned int type, unsigned int code, int value);
- static inline void input_report_key(struct input_dev *dev,
- unsigned int code, int value)
- { input_event(dev, EV_KEY, code, !!value); }
- static inline void input_report_rel(struct input_dev *dev,
- unsigned int code, int value)
- { input_event(dev, EV_REL, code, value); }
- static inline void input_report_abs(struct input_dev *dev,
- unsigned int code, int value)
- { input_event(dev, EV_ABS, code, value); }
- static inline void input_report_ff_status(struct input_dev *dev,
- unsigned int code, int value)
- { input_event(dev, EV_FF_STATUS, code, value); }
- static inline void input_report_switch(struct input_dev *dev,
- unsigned int code, int value)
- { input_event(dev, EV_SW, code, !!value); }
- static inline void input_sync(struct input_dev *dev)
- { input_event(dev, EV_SYN, SYN_REPORT, 0); }
事实上,对不同设备内容的报告均是通过input_event()函数来完成的,选择使用了不同参数而已。
在手机系统中经常使用的键盘(keyboard)和小键盘(kaypad)属于按键设备EV_KEY,轨迹球属于相对设备EV_REL,触摸屏属于绝对设备EV_ABS。
关于按键数值的定义的片断如下所示:
- #define KEY_RESERVED 0
- #define KEY_ESC 1
- #define KEY_1 2
- #define KEY_2 3
- #define KEY_3 4
- #define KEY_4 5
- #define KEY_5 6
- #define KEY_6 7
- #define KEY_7 8
- #define KEY_8 9
- #define KEY_9 10
- #define KEY_0 11
- #define KEY_MINUS 12
- #define KEY_EQUAL 13
- #define KEY_BACKSPACE 14
- #define KEY_TAB 15
- #define KEY_Q 16
- #define KEY_W 17
- #define KEY_E 18
- #define KEY_R 19
- #define KEY_T 20
可以使用getevent对Event设备进行调试,在Android的模拟器环境中,使用getevent的情况如下所示:
- # getevent
- add device 1: /dev/input/event0
- name: "qwerty2"
- could not get driver version for /dev/input/mouse0, Not a typewriter
- could not get driver version for /dev/input/mice, Not a typewriter
- /dev/input/event0: 0001 0002 00000001
- /dev/input/event0: 0001 0002 00000000
点击数字按键1,出现了上面的信息,0002是按键的扫描码,00000001和00000000分别是按下和抬起的附加信息。最前面的0001实际上是输入设备的类型。
使用getevent可以最直接地获得按键的扫描码,对于Android系统中用户输入设备的调试,可以从源头确定底层输入设备传递上来的信息。
8.2.2 用户空间的处理
1.处理的内容和流程
触摸屏和轨迹球上报的是坐标、按下、抬起等信息,信息量比较少。按键处理的过程稍微复杂,从驱动程序到Android的Java层受到的信息,键表示方式经过了两次转化,如图8-4所示。
键扫描码Scancode是由Linux的Input驱动框架定义的整数类型。键扫描码Scancode经过一次转化后,形成按键的标签KeycodeLabel,是一个字符串的表示形式。按键的标签KeycodeLabel经过转换后,再次形成整数型的按键码keycode。在Android应用程序层,主要使用按键码keycode来区分。
图8-4 Android按键输入的两次转化
在本地框架层libui的头文件中KeycodeLabels.h,按键码为整数值的格式,其定义KeyCode(枚举值)如下所示:
- typedef enum KeyCode {
- kKeyCodeUnknown = 0,
- kKeyCodeSoftLeft = 1,
- kKeyCodeSoftRight = 2,
- kKeyCodeHome = 3,
- kKeyCodeBack = 4,
- // ...... 省略中间按键码
- } KeyCode;
进而在定义了KeycodeLabels.h中定义了从字符串到整数的映射关系,数组KEYCODES,定义如下所示:
- static const KeycodeLabel KEYCODES[] = { // {字符串,整数}
- { "SOFT_LEFT", 1 },
- { "SOFT_RIGHT", 2 },
- { "HOME", 3 },
- { "BACK", 4 },
- { "CALL", 5 },
- { "ENDCALL", 6 },
- { "0", 7 }, // ...... 数字按键
- { "1", 8 },
- { "2", 9 },
- { "3", 10 },
- { "4", 11 },
- { "5", 12 },
- { "6", 13 },
- { "7", 14 },
- { "8", 15 },
- { "9", 16 },
- { "STAR", 17 },
- // ...... 省略中间按键映射
- { "MENU", 82 },
- // ...... 省略中间按键映射
- { NULL, 0 }
- };
数组KEYCODES表示的映射关系,左列的内容即表示按键标签KeyCodeLabel,右列的内容为按键码KeyCode(与KeyCode的数值对应)。实际上,在按键信息第二次转化的时候就是将字符串类型KeyCodeLabel转化成整数的KeyCode。
KeycodeLabel的Flags的定义如下所示:
- static const KeycodeLabel FLAGS[] = {
- { "WAKE", 0x00000001 }, // 可以唤醒睡眠,并通知应用层
- { "WAKE_DROPPED", 0x00000002 }, // 可以唤醒睡眠,不通知应用层
- { "SHIFT", 0x00000004 }, // 自动附加SHIFT
- { "CAPS_LOCK", 0x00000008 }, // 自动附加CAPS_LOCK
- { "ALT", 0x00000010 }, // 自动附加ALT
- { "ALT_GR", 0x00000020 },
- { "MENU", 0x00000040 },
- { "LAUNCHER", 0x00000080 },
- { NULL, 0 }
- };
KeycodeLabel表示按键的附属标识。
提示:frameworks/base/core/Java/android/view/KeyEvent.Java中定义了类android.view. KeyEvent类,其中定义整数类型的数值与KeycodeLabels.h中定义的KeyCode枚举值是对应的。
在本地框架层libui的头文件中KeyCharacterMap.h,定义了按键的字符映射关系,KeyCharacterMap类的定义如下所示:
- class KeyCharacterMap
- {
- public:
- ~KeyCharacterMap();
- unsigned short get(int keycode, int meta);
- unsigned short getNumber(int keycode);
- unsigned short getMatch(int keycode, const unsigned short* chars,
- int charsize, uint32_t modifiers);
- unsigned short getDisplayLabel(int keycode);
- bool getKeyData(int keycode, unsigned short *displayLabel,
- unsigned short *number, unsigned short* results);
- inline unsigned int getKeyboardType() { return m_type; }
- bool getEvents(uint16_t* chars, size_t len,
- Vector* keys, Vector* modifiers);
- static KeyCharacterMap* load(int id);
- enum {
- NUMERIC = 1,
- Q14 = 2,
- QWERTY = 3 // or AZERTY or whatever
- };
- }
KeyCharacterMap用于将按键的码映射为文本可识别的字符串(例如,显示的标签等)。KeyCharacterMap是一个辅助的功能:由于按键码只是一个与UI无关整数,通常用程序对其进行捕获处理,然而如果将按键事件转换为用户可见的内容,就需要经过这个层次的转换了。
KeyCharacterMap需要从本地层传送到Java层,JNI的代码路径如下所示:
frameworks/base/core/jni/android_text_KeyCharacterMap.cpp
KeyCharacterMap Java框架层次的代码如下所示:
frameworks/base/core/Java/android/view/KeyCharacterMap.Java
android.view.KeyCharacterMap类是Android平台的API可以在应用程序中使用这个类。
android.text.method中有各种Linstener,可以之间监听KeyCharacterMap相关的信息。DigitsKeyListener NumberKeyListener TextKeyListener。
以上关于按键码和按键字符映射的内容是在代码中实现的内容,还需要配合动态的配置文件来使用。在实现Android系统的时候,有可能需要更改这两种文件。
动态的配置文件包括:
Donut及其之前配置文件的路径为:
development/emulator/keymaps/
Eclair及其之后配置文件的路径为:
sdk/emulator/keymaps/
这些配置文件经过系统生成后,将被放置在目标文件系统的/system/usr/keylayout/目录或者/system/usr/keychars/目录中。
提示:kl文件将被直接复职到目标文件系统中;由于尺寸较大,kcm文件放置在目标文件系统中之前,需要经过压缩处理。KeyLayoutMap.cpp负责解析处理kl文件,KeyCharacterMap.cpp负责解析kcm文件。
2.kl:按键布局文件
Android默认提供的按键布局文件主要包括qwerty.kl和AVRCP.kl。qwerty.kl为全键盘的布局文件,是系统中主要按键使用的布局文件;AVRCP.kl用于多媒体的控制,ACRCP的含义为Audio/Video Remote Control Profile。
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 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 16 Q
- key 17 W
- key 18 E
- key 19 R
- key 20 T
- key 115 VOLUME_UP
- key 114 VOLUME_DOWN
在按键布局文件中,第1列为按键的扫描码,是一个整数值;第2列为按键的标签,是一个字符串。即完成了按键信息的第1次转化,将整型的扫描码,转换成字符串类型的按键标签。第3列表示按键的Flag,带有WAKE字符,表示这个按键可以唤醒系统。
扫描码来自驱动程序,显然不同的扫描码可以对应一个按键标签。表示物理上的两个按键可以对应同一个功能按键。
例如,上面的扫描码为158的时候,对应的标签为 BACK ,再经过第二次转换,根据KeycodeLabels.h的KEYCODES数组,其对应的按键码为4。
提示:按键布局文件其实同时兼顾了input驱动程序的定义和Android中按键的定义。例如:input驱动程序中定义的数字扫描码KEY_1的数值为2,这里2对应的按键标签也为“1”;input驱动程序中定义字母扫描码KEY_Q的数值为16,这里对应的按键标签也为“Q”。然而移动电话的全键盘毕竟有所不同,因此有一些按键是和input驱动程序的定义没有对应关系的。
kl文件将以原始的文本文件的形式,放置于目标文件系统的/system/usr/keylayout/目录或者/system/usr/keychars/目录中。
3.kcm:按键字符映射文件
kcm表示按键字符的映射关系,主要功能是将整数类型按键码(keycode)转化成可以显示的字符。
qwerty.kcm表示全键盘的字符映射关系,其片断如下所示:
- [type=QWERTY]
- # keycode display number base caps fn caps_fn
- A 'A' '2' 'a' 'A' '#' 0x00
- B 'B' '2' 'b' 'B' '<' 0x00
- C 'C' '2' 'c' 'C' '9' 0x00E7
- D 'D' '3' 'd' 'D' '5' 0x00
- E 'E' '3' 'e' 'E' '2' 0x0301
- F 'F' '3' 'f' 'F' '6' 0x00A5
- G 'G' '4' 'g' 'G' '-' '_'
- H 'H' '4' 'h' 'H' '[' '{'
- I 'I' '4' 'i' 'I' '$' 0x0302
- J 'J' '5' 'j' 'J' ']' '}'
- K 'K' '5' 'k' 'K' '"' '~'
- L 'L' '5' 'l' 'L' ''' '`'
- M 'M' '6' 'm' 'M' '!' 0x00
- N 'N' '6' 'n' 'N' '>' 0x0303
第一列是转换之前的按键码,第二列之后分别表示转换成为的显示内容(display),数字(number)等内容。这些转化的内容和KeyCharacterMap.h中定义的getDisplayLabel(),getNumber()等函数相对应。
这里的类型,除了QWERTY之外,还可以是Q14(单键多字符对应的键盘),NUMERIC(12键的数字键盘)。
kcm文件将被makekcharmap工具转化成二进制的格式,放在目标系统的/system/usr/keychars/目录中。
8.2.3 移植需要注意的情况
1.EventHub中基本的处理
libui库中frameworks/base/libs/ui中的EventHub.cpp文件是用户输入系统的中枢,主要的功能都是在这个文件中实现的。
EventHub.cpp中定义设备节点所在的路径,内容如下所示:
- static const char *device_path = "/dev/input"; // 输入设备的目录
在处理过程中,将搜索路径下面的所有Input驱动的设备节点,这在openPlatformInput()中通过调用scan_dir()来实现,scan_dir()将会从目录中查找设备,找到后调用open_device()将其打开。
- bool EventHub::openPlatformInput(void)
- {
- // ...... 省略其他部分的内容
- res = scan_dir(device_path);
- return true;
- }
EventHub的getEvent()函数负责处理中完成,处理过程是在一个无限循环之内,调用阻塞的函数等待事件到来。
- bool EventHub::getEvent(int32_t* outDeviceId, int32_t* outType,
- int32_t* outScancode, int32_t* outKeycode, uint32_t *outFlags,
- int32_t* outValue, nsecs_t* outWhen)
- {
- while(1) {
- // ...... 省略部分内容
- pollres = poll(mFDs, mFDCount, -1); // 使用poll处理设备节点,进行阻塞
- // ...... 省略部分内容
- for(i = 1; i < mFDCount; i++) {
- if(mFDs[i].revents) {
- if(mFDs[i].revents & POLLIN) {
- res = read(mFDs[i].fd, &iev, sizeof(iev)); // 读取信息
- // ...... 省略部分内容
- }
- }
- }
- }
poll()函数将会阻塞程序的运行,此时为等待状态,无开销,直到Input设备的相应事件发生,事件发生后poll()将返回,然后通过read()函数读取Input设备发生的事件代码。
注意,EventHub默认情况可以在/dev/input之中扫描各个设备进行处理,通常情况下所有的输入设备均在这个目录中。
实际上,系统中可能有一些input设备可能不需要被Android整个系统使用,也就是说不需要经过EventHub的处理,在这种情况下可以根据EventHub中open_device()函数的处理,设置驱动程序中的一些标志,屏蔽一些设备。open_device()中处理了键盘,轨迹球和触摸屏等几种设备,对其他设备可以略过。另外一个简单的方法就是将不需要EventHub处理的设备的设备节点不放置在/dev/input之中。
open_device()函数还将打开system/usr/keylayout/中的kl文件来处理,处理的过程如下所示:
- int EventHub::open_device(const char *deviceName) {
- // ...... 省略部分内容
- const char* root = getenv("ANDROID_ROOT");
- snprintf(keylayoutFilename, sizeof(keylayoutFilename),
- "%s/usr/keylayout/%s.kl", root, tmpfn);
- bool defaultKeymap = false;
- if (access(keylayoutFilename, R_OK)) {
- snprintf(keylayoutFilename, sizeof(keylayoutFilename),
- "%s/usr/keylayout/%s", root, "qwerty.kl");
- defaultKeymap = true;
- }
- // ...... 省略部分内容
- }
由此可见,默认情况下使用的就是qwerty.kl,这里只是扫描各个后缀名为kl的文件,然后交由KeyLayoutMap去解析处理,KeyLayoutMap是一个内部使用的类。
2.按键的增加
Android已经定义了比较丰富、完整的标准按键。在一般情况下,不需要为Android系统增加按键,只需要根据kl配置按键即可。在系统中有比较奇特按键的时候,需要更改Android系统的框架层来更改按键。
增加按键需要更改的文件较多,主要的文件如下所示。
框架层增加完成后,只需要更改kl文件,增加按键的映射关系即可。
提示:在系统需要增加按键的时候,一种简易的做法是使用Android中已经定义的“生僻”按键码作为这个新增按键的键码。使用这种方式Android的框架层不需要做任何改动。这种方式的潜在问题是当某些第三方的应用可能已经使用那些生僻按键时,将意外激发系统的这种新增的按键。
8.3 模拟器中的实现
8.3.1 驱动程序
GoldFish虚拟处理器键盘输入部分的驱动程序是event驱动程序,在标准的路径中,相关文件如下所示:
drivers/input/keyboard/goldfish_events.c
这个驱动程序是一个标准的event驱动程序,在用户空间的设备节点为/dev/event/ event0,其核心的内容为:
- static irqreturn_t events_interrupt(int irq, void *dev_id)
- {
- struct event_dev *edev = dev_id;
- unsigned type, code, value;
- type = __raw_readl(edev->addr + REG_READ); // 类型
- code = __raw_readl(edev->addr + REG_READ); // 码
- value = __raw_readl(edev->addr + REG_READ); // 数值
- input_event(edev->input, type, code, value);
- return IRQ_HANDLED;
- }
events_interrupt实现的是按键事件的中断处理函数,当中断发生后,读取虚拟寄存器的内容,将信息上报。实际上,虚拟寄存器中的内容由模拟器根据主机环境键盘按下的情况得到。
8.3.2 用户空间的配置文件
在模拟器环境中,使用了默认的所有的KL和KCM文件,由于模拟器环境支持全键盘,因此基本上包含了大部分的功能。在模拟器环境中,实际上按键的扫描码对应的是桌面电脑的键盘(效果和鼠标点击模拟器的控制面板类似),键盘的某些按键按下后,转化为驱动程序中的扫描码,然后再由上层的用户空间处理。这个过程和实际系统中是类似的。显然,通过更改默认的KL文件,又可以更改实际按键的映射关系。
8.4 MSM中的实现
MSM的mahimahi平台具有触摸屏,轨迹球和简易的按键,这些功能在Android中的实现包括了驱动程序和用户空间的内容。
8.4.1 触摸屏,轨迹球和按键驱动程序
MSM的Mahimahi平台的用户输入设备包括了以下几个Event设备。
MSM触摸屏的驱动程序在drivers/input/touchscreen目录中的synaptics_i2c_rmi.c,这是一个i2c触摸屏的驱动程序。
MSM系统包含了按键和轨迹球的功能,具体的驱动程序在arch/arm/mach-msm/目录board-mahimahi-keypad.c文件中实现。
board-mahimahi-keypad.c中的全局定义如下所示:
- static struct gpio_event_info *mahimahi_input_info[] = {
- &mahimahi_keypad_matrix_info.info, // 键盘矩阵
- &mahimahi_keypad_key_info.info, // 键盘信息
- &jogball_x_axis.info.info, // 轨迹球X方向信息
- &jogball_y_axis.info.info, // 轨迹球Y方向信息
- };
- static struct gpio_event_platform_data mahimahi_input_data = {
- .names = {
- "mahimahi-keypad", // 按键设备
- "mahimahi-nav", // 轨迹球设备
- NULL,
- },
- .info = mahimahi_input_info,
- .info_count = ARRAY_SIZE(mahimahi_input_info),
- .power = jogball_power,
- };
- static struct platform_device mahimahi_input_device = {
- .name = GPIO_EVENT_DEV_NAME,
- .id = 0,
- .dev = {
- .platform_data = &mahimahi_input_data,
- },
- };
按键和轨迹球是通过GPIO系统来实现的,因此定义了gpio_event_info类型的数组。"mahimahi-keypad"和"mahimahi-nav"分别是两个设备的名称。gpio_event_info 指针各式的数组mahimahi_input_info中包含了mahimahi_keypad_matrix_info.info,mahimahi_keypad_key_ info.info,jogball_x_axis.info.info和jogball_y_axis.info.info。
按键驱动是一个利用GPIO矩阵的驱动,由gpio_event_matrix_info矩阵定义,定义还需要包含按键的GPIO矩阵和input设备的信息,内容如下所示:
- static unsigned int mahimahi_col_gpios[] = { 33, 32, 31 };
- static unsigned int mahimahi_row_gpios[] = { 42, 41, 40 };
- #define KEYMAP_INDEX(col, row) ((col)*ARRAY_SIZE(mahimahi_row_gpios) + (row))
- #define KEYMAP_SIZE (ARRAY_SIZE(mahimahi_col_gpios) * \
- ARRAY_SIZE(mahimahi_row_gpios))
- static const unsigned short mahimahi_keymap[KEYMAP_SIZE] = { // 按键映射关系
- [KEYMAP_INDEX(0, 0)] = KEY_VOLUMEUP, /* 115 */
- [KEYMAP_INDEX(0, 1)] = KEY_VOLUMEDOWN, /* 114 */
- [KEYMAP_INDEX(1, 1)] = MATRIX_KEY(1, BTN_MOUSE),
- };
- static struct gpio_event_matrix_info mahimahi_keypad_matrix_info = {
- .info.func = gpio_event_matrix_func, // 关键函数实现
- .keymap = mahimahi_keymap,
- .output_gpios = mahimahi_col_gpios,
- .input_gpios = mahimahi_row_gpios,
- .noutputs = ARRAY_SIZE(mahimahi_col_gpios),
- .ninputs = ARRAY_SIZE(mahimahi_row_gpios),
- .settle_time.tv.nsec = 40 * NSEC_PER_USEC,
- .poll_time.tv.nsec = 20 * NSEC_PER_MSEC,
- .flags = (GPIOKPF_LEVEL_TRIGGERED_IRQ |
- GPIOKPF_REMOVE_PHANTOM_KEYS |
- GPIOKPF_PRINT_UNMAPPED_KEYS),
- };
- static struct gpio_event_direct_entry mahimahi_keypad_key_map[] = { // Power按键
- {
- .gpio = MAHIMAHI_GPIO_POWER_KEY,
- .code = KEY_POWER,
- },
- };
- static struct gpio_event_input_info mahimahi_keypad_key_info = {
- .info.func = gpio_event_input_func, // 关键函数实现
- .info.no_suspend = true,
- .flags = 0,
- .type = EV_KEY,
- .keymap = mahimahi_keypad_key_map,
- .keymap_size = ARRAY_SIZE(mahimahi_keypad_key_map)
- };
mahimahi_keypad_key_matrix _info和mahimahi_keypad _info是gpio_event_matrix_info类型的结构体,分别负责两个和一个按键的处理,实际上,MSM的Mahimahi平台基本上只有三个按键:Power,音量增加按键和音量减少按键。音量增加和音量减少的扫描码分别是KEY_VOLUMEUP(=115)和KEY_VOLUMEDOWN(=114)。
提示:音量控制的两个按键在全键盘的qwerty.kl有所定义,同时符合Linux的input设备和Android的按键标准。
轨迹球部分也是由GPIO实现的,由X方向和Y方向两部分组成,内容如下所示:
- static uint32_t jogball_x_gpios[] = {
- MAHIMAHI_GPIO_BALL_LEFT, MAHIMAHI_GPIO_BALL_RIGHT,
- };
- static uint32_t jogball_y_gpios[] = {
- MAHIMAHI_GPIO_BALL_UP, MAHIMAHI_GPIO_BALL_DOWN,
- };
- static struct jog_axis_info jogball_x_axis = { // X轴的内容
- .info = {
- .info.func = gpio_event_axis_func, // 关键函数实现
- .count = ARRAY_SIZE(jogball_x_gpios),
- .dev = 1,
- .type = EV_REL,
- .code = REL_X,
- .decoded_size = 1U << ARRAY_SIZE(jogball_x_gpios),
- .map = jogball_axis_map,
- .gpio = jogball_x_gpios,
- .flags = GPIOEAF_PRINT_UNKNOWN_DIRECTION,
- }
- };
- static struct jog_axis_info jogball_y_axis = { // Y轴的内容
- .info = {
- .info.func = gpio_event_axis_func, // 关键函数实现
- .count = ARRAY_SIZE(jogball_y_gpios)
- .dev = 1,
- .type = EV_REL,
- .code = REL_Y,
- .decoded_size = 1U << ARRAY_SIZE(jogball_y_gpios),
- .map = jogball_axis_map,
- .gpio = jogball_y_gpios,
- .flags = GPIOEAF_PRINT_UNKNOWN_DIRECTION,
- }
- };
这里的轨迹球是用jog_axis_info类型的结构体进行定义的,这种设备的类型(type)是相对设备EV_REL。
8.4.2 用户空间的配置文件
除了默认的AVRCP.kl和qwerty.kl之外,MSM的mahimahi平台增加了h2w_headset.kl和mahimahi-keypad.kl。
8.5 OMAP中的实现
8.5.1 触摸屏和键盘的驱动程序
Omap的Zoom平台的输入设备包含了触摸屏和键盘(Qwerty全键盘)。
Omap的Zoom平台的触摸屏驱动程序在drivers/input/touchscreen目录中的synaptics_ i2c_rmi.c,这是一个i2c的触摸屏的驱动程序。
Omap的Zoom平台的键盘驱动程序在drivers/input/keyboard/目录的twl4030_keypad.c文件中实现。twl4030 使用的是i2c的接口。因此这个驱动程序本身是经过一次封装。
twl4030_keypad.c中核心的内容是中断处理的相关内容,do_kp_irq就是标准Linux的中断的处理函数,其内容如下所示:
- static irqreturn_t do_kp_irq(int irq, void *_kp)
- {
- struct twl4030_keypad *kp = _kp;
- u8 reg;
- int ret;
- ret = twl4030_kpread(kp, ®, KEYP_ISR1, 1); // 调用twl4030_i2c_read
- if ((ret >= 0) && (reg & KEYP_IMR1_KP))
- twl4030_kp_scan(kp, 0); // 非释放所有的处理
- else
- twl4030_kp_scan(kp, 1); // 释放所有的处理
- return IRQ_HANDLED;
- }
twl4030_kp_scan()函数是核心的处理功能,它负责找到按键的行列,然后调用input_report_key()汇报信息,其主要的实现部分如下所示:
- static void twl4030_kp_scan(struct twl4030_keypad *kp, int release_all)
- {
- u16 new_state[MAX_ROWS];
- int col, row;
- // ...... 省略部分内容
- for (row = 0; row < kp->n_rows; row++) {
- int changed = new_state[row] ^ kp->kp_state[row];
- // ...... 省略部分内容
- for (col = 0; col < kp->n_cols; col++) {
- int key;
- key = twl4030_find_key(kp, col, row);
- // ...... 省略部分内容
- input_report_key(kp->input, key, // 上报按键消息
- new_state[row] & (1 << col));
- }
- kp->kp_state[row] = new_state[row];
- }
- input_sync(kp->input);
- }
根据行列进行键盘信息的扫描,其中twl4030_find_key()是核心处理的功能,其实现如下所示:
- static int twl4030_find_key(struct twl4030_keypad *kp, int col, int row)
- {
- int i, rc;
- rc = KEY(col, row, 0);
- for (i = 0; i < kp->keymapsize; i++)
- if ((kp->keymap[i] & ROWCOL_MASK) == rc)
- return kp->keymap[i] & (KEYNUM_MASK | KEY_PERSISTENT);
- return -EINVAL;
- }
以上实用的kp->keymap 数组是定义的按键映射关系,这个数组就是定义于arch/arm/mach-omap2/board-zoom2.c中的zoom2_twl4030_keymap数组,这个数组的内容如下所示:
- static int zoom2_twl4030_keymap[] = {
- KEY(0, 0, KEY_E),
- KEY(1, 0, KEY_R),
- KEY(2, 0, KEY_T),
- KEY(3, 0, KEY_HOME),
- KEY(6, 0, KEY_I),
- KEY(7, 0, KEY_LEFTSHIFT),
- // ……省略部分内容
- KEY(7, 7, KEY_DOWN),
- KEY(0, 7, KEY_PROG1),
- KEY(1, 7, KEY_PROG2),
- KEY(2, 7, KEY_PROG3),
- KEY(3, 7, KEY_PROG4),
- 0
- };
twl4030_keypad.c文件中调用的twl4030_i2c_read和twl4030_i2c_write 是在drivers/ mfd/twl4030-core.c中实现的,实际上就是对i2c总线的操作的封装。
8.5.2 用户空间的配置文件
Omap的Zoom平台的键盘基本上是全键盘,但是其数字键和字母键是共用的。因此使用全键盘的配置文件基本上可以。
8.6 虚拟按键的实现
虚拟按键(Virtual Key)是Eclair版本开始增加的新特性。Virtual Key的功能是利用触摸屏,模拟按键发生的事件,这样就可以利用触摸屏的边缘,实现一些可以自定义的按键效果。
虚拟按键的实现效果如图8-5所示。
图8-5 虚拟按键的实现效果
在Android系统中,触摸屏设备发送的是RawInputEvent(原始输入事件),而按键发送的是KeyEvent(按键事件)。KeyEvent直接发送给应用程序层,RawInputEvent在Android的Java框架中被转换成MotionEvent发送给应用程序层。
在Android系统中虚拟按键的实现方法是:在某种情况下,将RawInputEvent转换成KeyEvent。
frameworks/base/services/Java/com/android/server目录中的InputDevice.Java文件负责处理虚拟按键的主要文件。
虚拟按键的处理相对简单,需要根据以下文件对虚拟按键的内容进行配置:
/sys/board_properties/virtualkeys.{devicename}
在InputDevice.Java文件中通过readVirtualKeys,对进行消息的转化。根据配置文件将RawInputEvent转换成按键相关的内容。
virtualkeys.{devicename}是虚拟按键的适配文件,需要在目标文件系统的/sys/board_ properties/目录中。
虚拟按键配置文件的格式如下所示:
0x1:扫描码:X:Y:W:H:0x1: ……
例如,在MSM的mahimahi平台上查看虚拟按键的配置文件如下所示:
# cat /sys/board_properties/virtualkeys.synaptics-rmi-touchscreen
0x01:158:55:835:90:55:0x01:139:172:835:125:55:0x01:102:298:835:115:55:0x01:217:412:835:95:55
由此可见,其中定义了4个区域的虚拟按键,它们的Y坐标相同,可见4个按键的矩形区域位于水平的一排。其转换的扫描码分别为158,139,102,217,分别对应于BACK(返回),MENU(菜单),HOME(主界面),SEARCH(搜索)这4个按键。
另外一个系统的虚拟按键的配置文件如下所示:
$ cat /sys/board_properties/virtualkeys.qtouch-touchscreen
0x01:139:90:936:116:104:0x01:102:252:936:116:104:0x01:158:402:936:116:104
其转换的扫描码分别为:139,102,158,分别对应于MENU(菜单),HOME(主界面),BACK(返回)这3个按键。
提示:使用虚拟按键转换成为的是按键的扫描码,不是按键码,因此依然需要经过按键布局文件的转化才能得到按键码。