Android HAL input 1. 안드로이드 Input 시스템개요 안드로이드의 Input 시스템은최하위 Kernel 단에서부터최상위어플리케이션단까지많은단계가존재한다. 이렇게단계가많은이유는 java 가디바이스에접근하기위해서 jni 를사용하였기때문이다. 그럼먼저사용자가입력을한데이터가어플리케이션에전달되는과정을대략적으로알아보도록하자 (1) 사용자입력이일어남 (2) Kernel 에짜여진 input_device_driver 에의해 input_event 가발생함 (3) EventHub.cpp에서 /dev/input/ 의디바이스드라이버파일을읽어서발생한 input_event를읽어냄 (4) jni를통해 KeyInputQueue.java 로 input_event를넘김 (5) KeyInputQueue 에서는무한루프를돌면서 input_event 가 jni를통해넘어오는지를계속감시하다가넘어온다면 QueueEvent 리스트의 tail 에붙임.( 내부스레드로처리 ) (6) 만약상위단에서현재의 Key 값을필요로하면 QueueEvent 리스트의 head를넘겨주고삭제함. (KeyInputQueue.getEvent() 함수 ) (7) WindowManagerService에서무한루프로 KeyInputQueue.getEvent() 함수를계속호출하면서큐에이벤트가추가되면바로원하는어플리케이션으로 dispatch 해줌안드로이드 Input 시스템을공부하는이유는궁극적으로 Input 디바이스를추가하거나변경하기위해서일것이다. 그러기위해서위의모든단계를손봐줄필요는없고 Input Device Driver 만안드로이드 Input 시스템에맞춰서잘짜주면된다. 2. Input device driver 인풋디바이스드라이버는커널에포함되어있다. 그래서커널소스를수정해야하는데인풋디바이스관련소스는커널루트에서 drivers/input/keyboard/ 디렉토리에있다. 인풋디바이스드라이버의코드는아래와같은형태로짜여져있다. static void timer_callback(unsigned long data) // 하드웨어값을읽어냄 s3c_gpio_getpin() // 읽어낸값을보고함 input_report_switch(); input_report_key(); input_report_abs(); input_sync(); static int devinit driver_probe(struct platform_device *pdev) // input 디바이스생성 input_dev = input_allocate_device()
// input 디바이스초기화 // input 디바이스등록 input_register_device(input_dev) // 하드웨어초기화 s3c_set_gpio_func() set_bit() // timer 초기화및등록 init_timer() add_timer() static int devexit driver_remove(struct platform_device *pdev) // 자원해제코드 input_unregister_device(input_dev); del_timer(); static void release_device(struct device *dev) struct platform_device mep6410_joybut_device_t =.name = DRV_NAME,.id = 0,.num_resources = 0,.dev =.release = release_device,, ; static struct platform_driver driver_t =.probe = driver_probe,.remove = driver_remove,.driver =.name = DRV_NAME,.owner = THIS_MODULE,, ; int init driver_init() res = platform_device_register(&device_t); res = platform_driver_register(&driver_t); void exit driver_exit() platform_device_unregister(&device_t); platform_driver_unregister(&driver_t); module_init(driver_init); module_exit(driver_exit); MODULE_LICENSE("GPL"); 안드로이드인풋디바이스드라이버는커널디바이스모델을따라작성해줘야한다. 결국커널디바이스모델의개념을이해하고있어야하는데이에대한자세한내용은이책의범위를벗어나기때문에설명하지않는다. 위의소스는결과적으로 input_report_xxx() 함수에의해서하드웨어에서읽혀진값이올라가도록되어있는구조이다. 이렇게올라가는값은 input_event 라는커널상의구조체형식으로올라가는데이구조체
는아래와같은형태로 linux/input.h 파일에선언되어있다. struct input_event struct timval time; unsigned short type; unsigned short code; unsigned int value; ; 이구조체의각필드는다음과같은역할을한다. 필드설명 time 입력이벤트가발생한시간정보다양한입력장치에의해입력된정보를구분하는타입을나타낸다. 이것을구분하는이유는각각의장치의형태가다르기때문이다. type 예를들어터치는값이정해져있기때문에절대값위치정보 (EV_ABS) 를제공하고마우스는상대적이기때문에변화량을위한위치정보 (EV_REL) 를제공한다. 또한키보드는위치가없기때문에키에관한정보 (EV_KEY) 를제공한다. 이필드는입력한정보를세분화하는필드이다. 예를들어마우스는 X 좌표와 Y 좌표를각각따로따로전송한다. 결국이를 code 구분하기위한구분자가필요한데이필드가그러한역할을한다. 또한키입력에서는이필드가 keycode 가된다. 이값은세분화된필드의데이터를저장하는역할을한다. value 이값역시입력장치마다쓰임세가다른데마우스에서는 X,Y 좌표를입력받고키에서는키가눌리어있는지를확인할때쓰인다. 결국안드로이드시스템에서는커널에서넘겨주는 input_event 형태의값만파싱해서쓰면되는것이다. 3. 안드로이드에서의 input 시스템 가. 안드로이드 native 단에서 input 값읽어내기 커널에서 input_event를얻어오기위해서는디바이스파일을읽어내야한다. 디바이스파일경로는 /dev/input/* 형태로되어있다. 여기서 "/dev/input" 이라는디렉토리는마우스나키보드, 트랙볼등인풋에관련된디바이스를모아놓은부분이다. 안드로이드에서이를읽어내서처리하는부분을담당하는파일은 frameworks/base/libs/ui/eventhub.cpp" 이다. struct pollfd *mfds; bool EventHub::getEvent() if (!mopened) merror = openplatforminput()? NO_ERROR : UNKNOWN_ERROR; // "/dev/input/*" 디바이스읽어내기
mopened = true; while(1) release_wake_lock(wake_lock_id); pollres = poll(mfds, mfdcount, -1); acquire_wake_lock(partial_wake_lock, WAKE_LOCK_ID); for(i = 1; i < mfdcount; i++) if(mfds[i].revents) if(mfds[i].revents & POLLIN) res = read(mfds[i].fd, &iev, sizeof(iev)); if (res == sizeof(iev)) [ 읽어낸값파라메터로넘겨주기 ] return true; <EventHub.cpp : getevent()> 가장먼저 openplatforminput() 함수가보이는데이부분에 input 디바이스를 open 하는부분이들어있다. 이렇게 open 된인풋디바이스 fd 들에게서 POLLIN 이벤트가일어나게되면넘어온 input_event 의내용을파라메터로넘겨주게된다. 넘겨준값을어떻게처리하는지는다음장에서알아보기로하고이번장에서는제목과마찬가지로디바이스를여는데초점을맞추기로하겠다. static const char *device_path = "/dev/input"; bool EventHub::openPlatformInput(void) res = scan_dir(device_path); if(res < 0) LOGE("scan dir failed for %s n", device_path); return true; <EventHub.cpp : openplatforminput()> device_path 변수가 /dev/input" 로잡혀있다. 이것은 input 관련된 device 가모여있는디렉토리이다. 즉 scan_dir() 함수는이러한디렉토리를열어서모든디바이스파일을오픈하도록해주는기능을한다.
int EventHub::scan_dir(const char *dirname) dir = opendir(dirname); while((de = readdir(dir))) // devname= 열린파일절대경로 open_device(devname); closedir(dir); return 0; <EventHub.cpp : scan_dir()> 이와같이디렉토리자체를열어서 readdir 로순회하면서각파일마다 open_device() 함수를호출하게되어있다. 이러한내용으로미루어볼때 open_device() 함수에 open() 시스템콜이들어가있을확률이높다. 그럼확인해보도록하자. struct pollfd *mfds; int mfdcount; int EventHub::open_device(const char *devicename) fd = open(devicename, O_RDWR); new_mfds = (pollfd*)realloc(mfds, sizeof(mfds[0]) * (mfdcount + 1)); mfds = new_mfds; mfds[mfdcount].fd = fd; mfds[mfdcount].events = POLLIN; mfdcount++; return 0; <EventHub.cpp : open_device()> 사실이함수의내용은상당히길다. 거기서중요한기능을간추리면위와같다. 가장먼저 open() 시스템콜로디바이스를오픈하고전역변수 mfds를 realloc() 으로늘려준다음내용을채운다. 마지막으로 mfdcount 변수를증가시키고리턴하게된다. 이로서 mfds 변수에는모든 input 디바이스의 fd 가들어가게되고이를 getevent() 함수에서 poll 하여서사용할수있는것이다. 나. 안드로이드 framework 에서 input 값읽어내기 사실 framework 와 native 사이에는 jni 가존재하지만이것은그저데이터의이동통로
만을정의해줄뿐새로운기능을하는것은아니다. 그래서이부분은설명하지않도록하겠다. 그럼먼저 getevent() 함수가어떤특징이있는지간략하게알아보고 Framework에서어떤식으로처리하는지알아보도록하자. struct pollfd *mfds; bool EventHub::getEvent() while(1) for(i = 1; i < mfdcount; i++) if(mfds[i].revents) if(mfds[i].revents & POLLIN) res = read(mfds[i].fd, &iev, sizeof(iev)); if (res == sizeof(iev)) [ 읽어낸값파라메터로넘겨주기 ] return true; <EventHub.cpp : getevent()> EventHub.cpp 의 getevent() 함수를다시한번보게되면무한루프를돌고있는것을알수있다. 이것으로 getevent() 함수가계속 input 디바이스를감시하고있는것으로오해하기쉽다. 그러나여기서는제대로읽어내기만하면 true를리턴해서바로무한루프를끝내준다. 즉 getevent() 는제대로읽어내기전까지는무한루프 (block) 이고제대로읽어낸다면리턴이되게된다. 이 getevent() 함수와 JNI를통해연결되어진 java 메소드는 KeyInputQueue 클래스의 readevent() 메소드이다. 그럼이메소드를사용하는방법에대해서알아보도록하자. package com.android.server; public abstract class KeyInputQueue private static native boolean readevent(rawinputevent outevent); Thread mthread = new Thread("InputDeviceReader") public void run() RawInputEvent ev = new RawInputEvent(); while(true) readevent(ev);
[input 이벤트처리 ] ; KeyInputQueue() mthread.start(); <KeyInputQueue.java> readevent() 메소드는 native 함수로등록되어있고이는 JNI 와통신하여 EventHub.cpp 의 getevent() 함수를부르게된다. readevent() 메소드의파라메터로 RawInputEvent 라는클래스가있는데이것은 getevent() 함수에서넘어온데이터를한군데모아놓은일종의구조체라고보면된다. 즉 JNI 에서는 getevent() 함수를호출해서데이터를받은다음이것을가지고 RawInputEvent 라는클래스를구성해서 readevent() 메소드로돌려주게된다. getevent() 함수는 block 함수이기때문에이를호출하는 readevent() 함수역시 block 함수가된다. java에서이를처리하기위해위처럼 thread를사용하였다. 먼저무한루프를돌면서 readevent() 메소드를호출하여처리하는 mthread를만들고이를 KeyInputQueue 클래스가생성될때시작되도록하였다. 여기서 KeyInputQueue 클래스는 abstract 클래스이기때문에이를상속한어떤클래스가생성되면이스레드가실행되게되는것이다. 다. input 이벤트처리하기 KeyInputQueue 에서는무한루프를돌면서 readevent() 를호출하면서 input 이벤트를가지고온다. 이렇게가져온이벤트의처리방식은서로연결된 QueuedEvent List를유지하면서새로운이벤트가일어나면 List tail 에추가하고어플리케이션에서이벤트를사용하면 head에서삭제하면서처리한다. 이와같은형태로사용되게되며 getevent() 함수는유지하고있는 List에서 head 에있는 Event를리턴하고삭제하는기능을한다. 또한 addlocked() 는유지하고있는 List 의 tail 에다가 Event를추가하는기능을한다. 결국어플리케이션에서는 getevent() 함수만호출해서사용하면된다.
라. 안드로이드에서키값매핑 안드로이드는키이벤트가일어나면이를안드로이드의키로맵핑을해주는데이를설정하는파일이 "build/target/board/generic/xxx.kl" 이다. 이를열어보면아래와같은형태로되어있다. key 399 GRAVE key 2 1 key 3 2 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 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 가장처음숫자는 input_event에서넘어오는키코드에비례하고그옆의문자는안드로이드에서어떤키에맵핑될지를나타낸다. 이렇게얻어낸키이벤트는캐릭터코드로맵핑되는데이를처리하는파일은 "build/target/board/generic/xxx.kcm" 이다. 이파일은아래와같은형태로되어있다. [type=qwerty] # keycode display number base caps fn caps_fn A 'A' '%' 'a' 'A' '%' 0x00 B 'B' '=' 'b' 'B' '=' 0x00 C 'C' '8' 'c' 'C' '8' 0x00E7 D 'D' '5' 'd' 'D' '5' 0x00 E 'E' '2' 'e' 'E' '2' 0x0301 Y 'Y' '!' 'y' 'Y' '!' 0x00A1 Z 'Z' '#' 'z' 'Z' '#' 0x00 AT '@' '0' '@' '0' '0' 0x2022 SLASH '/' '/' '/' '?' '?' ' ' SPACE 0x20 0x20 0x20 0x20 0xEF01 0xEF01 ENTER 0xa 0xa 0xa 0xa 0xa 0xa
이 KCM 파일은처리속도를높이기위해서컴파일시 "xxx.kcm.bin" 과같은바이너리형태로바뀌게된다. 이렇게안드로이드에서는최하위디바이스드라이버와 kl,kcm 파일만시스템에맞게수정해주면쉽게포팅가능하다.