ecsimsw

JNI 임베디드 프로그래밍 본문

JNI 임베디드 프로그래밍

JinHwan Kim 2022. 6. 19. 23:29

JNI-JellyBean-HBE-SM5-S4210

하드웨어 드라이버를 수정하고, 안드로이드 커널에 포함/빌드하여, 보드 내 임베디드 하드웨어를 제어한다. 

JNI를 이용하여 시스템 콜을 호출, JVM 환경 안에서 수정된 드라이버로 하드웨어를 제어한다.

 

Concepts 

1. BootLoader

 

PC에서 전원이 켜지면 ROM의 BIOS가 로드되고, BIOS는 미리 설정된 부팅 순서대로 저장 장치의 부트로더를 로드, 실행하게 된다. 부트로더는 하드웨어를 초기화하고, 커널을 메모리에 적재하는 것으로 운영체제가 구동된다.

 

저장 장치가 다수인 PC와 달리, ARM 임베디드 시스템에선 단일 플래시 메모리에 BootLoader, Kernel 이미지, 사용자 파일과 App Storage가 존재한다. 이런 임베디드 시스템의 경우 BIOS가 아닌 플래시 메모리의 고정된 위치에 부트로더가 존재하여 펌웨어에 의해 실행되며, 하드웨어 초기화, 시스템 진단을 포함한 시스템 초기화가 실행된다. kernel 이미지 역시 플래시 메모리의 고정된 위치에 존재하여 부트 로더는 이를 메모리에 로드하는 것으로 운영체제를 구동한다.

 

 

2. Fastboot

 

하드웨어 드라이버 파일을 수정하고 kernel을 빌드한 후 이를 장치에 반영해야 했다. 커널 이미지를 수정하기 위해서 kernel이 로드되지 않은, 즉 부트로더가 kernel을 로드하기 전에 interrupt를 발생시켜 OS가 구동되는 것을 중단한다. 실습에선 Serial 통신을 연결하고 bootloader가 kernel을 로드하기 전 시점에 keyboard로 interrupt를 발생시킨다.

 

1) 장치에 개발 PC를 Serial 통신한다. putty - serial 연결을 사용하였다.

2) 장치 전원 공급, 부트로더가 Kernel을 로드하기 전 3초간의 시간 동안 keyboard로 interrupt를 표시한다.

 

커널을 업데이트 하기 위해선 디바이스의 파일 시스템의 커널 위치를 수정해야 한다. 이를 위해 fastboot를 사용하였다. device를 fastboot 모드로 진입하고, pc fastboot로 커널 업데이트는 ADB USB driver가 설치된 상태에서 다음 순서로 실행한다. 

 

3) Serial : fastboot mode 진입

4) USB : /adb/fastboot를 이용해 기기의 fastboot와의 접속을 확인한다. fastboot devices로 연결 기기를 확인한다.

 

커널 이미지를 적절한 파일 시스템 위치(/kernel)에 전송하고, 기기를 reboot 하는 것으로 커널 업데이트를 진행한다.

 

5) USB : fastboot flast /kernel {zImage path}

6) USB : fastboot reboot

 

 

3. ADB

 

ADB (Android debug bridge)는 기기와 통신하고 Unix 쉘을 사용한 명령어 실행을 가능하게 하는 Android 제공 도구이다. 실습에선 안드로이드 기기 내부에 애플리케이션 실행 파일을 전송하고 이를 실행하기 위해 사용하였다. 이를 keypad 이벤트를 받아 입력된 keycode를 확인하는 테스트 코드를 C로 작성하여 컴파일, 이를 adb로 전송, 실행하여 해당 테스트 애플리케이션을 실행할 수 있었다. 애플리케이션을 실행시킬 때까지의 adb 사용 절차는 다음과 같다.

 

7) 코드 작성 및 컴파일 서버에서 컴파일

8) 개발 PC로 실행 파일 전송

9) adb를 이용한 파일 전송, adb push -p {파일 위치} /system/bin

10) adb shell 접속, 실행 파일 권한 부여 후 실행

 

 

4. Cross compiler

 

앞선 3번에서 테스트 코드를 개발 PC에서 작성하고 일반 C 컴파일러로 해당 코드를 컴파일하면 개발 PC와 같은 환경(플랫폼)에서 실행 가능한 실행 파일이 생성된다. 개발 플랫폼과 실행 플랫폼이 달라 생기는 문제이나, 그렇다고 모바일 프로세서를 사용하는 안드로이드 디바이스 내부에서 개발 환경을 만들고, 컴파일하는 것이 쉬운 일은 아닐 것이다. 이렇게 컴파일러가 컴파일한 플랫폼과 해당 실행 파일이 실행되는 플랫폼이 다른 경우에 Cross compiler를 사용한다.

 

이번 실습의 경우, ARM 프로세서의 모바일 디바이스를 실행 환경으로, 리눅스를 컴파일 환경으로 한다. 따라서 크로스 컴파일러가 필요했고, arm-linux-gnueabi-gcc를 사용했다.

 

 

5. JNI / Native C,C++

 

이렇게 드라이버 수정과 해당 파일을 포함한 커널을 빌드하는 것으로 하드웨어 컴포넌트를 조작할 수 있었고, 이를 C언어로 구현한 테스트 애플리케이션으로 시스템 콜을 호출하여 하드웨어 동작을 테스트하였다. 하드웨어 장치들을 조작할 수 있는 환경을 만들었고 이를 유저의 모바일 애플리케이션 개발에서 사용하고 싶었다.

 

JNI는 java native interface의 약어로 JVM 상의 자바 언어가 C나 C++로 개발된 라이브러리를 호출하거나, 개발할 수 있도록 하는 프로그래밍 프레임워크이다. 이를 이용하여 JVM 상에서 동작하는 안드로이드 모바일 앱에서 하드웨어를 다루기 위한 드라이버를 시스템 콜로 호출하는 것으로 앱 사용에 하드웨어 조작을 더하였다.

 

JNI는 이번 실습의 경우처럼 시스템 콜과 같은 운영체제 수준의 서비스를 사용하기 위해 사용할 수 있고, 또는 자바의 속도 문제를 해결하기 위해 보단 빠른 C/C++로 짜인 로직으로 연산을 수행하거나, 네이티브 언어를 사용해야만 하는 라이브러리를 사용하기 위해 사용된다.

 

 

6. Android Platform architecture

 

안드로이드 플랫폼은 아래 그림과 같은 다섯 계층으로 이루어져 있다. 먼저 안드로이드는 리눅스 커널을 기반으로 하고 있다. 커널에선 메모리 관리, 네트워크 시스템 관리, 하드웨어 드라이버 및 리소스를 관리한다. 하드웨어 추상화 계층은 하드웨어와 프레임워크 간의 API이다. 하드웨어의 기계어, 어셈블리어 등 저수준 언어를 감추고, 카메라 모듈, 블루투스 모듈과 같은 형태의 인터페이스를 남겨 안드로이드에서 상위 수준의 시스템을 쉽게 확장, 수정할 수 있다.

 

네이티브 C/C++ 라이브러리 계층은 저수준 언어의 라이브러리를 담고, 이를 실행한다. 연산이 복잡하고 빨라야 하는 곳에서 자바보다 더 빠른 저수준 언어로 연산을 처리하거나 C/C++로 짜여 사용되는 라이브러리를 실행할 때 사용한다. 이를 테면 그래픽이나 렌더링 등의 미디어 처리를 담당하는 프레임워크나 라이브러리가 이 계층에 존재한다.

 

런타임 계층에선 실제 자바 애플리케이션 코드들이 컴파일된다. 자바의 경우 바이트 코드가 실행되기 위해선 자바 가상 머신이 필요하고 과거에는 Dalvik VM을 사용하였지만, 최근에는 Dalvik VM을 폐지하고, ART를 공식적으로 지원한다고 한다. 실습에선 DalvikVM이 사용되었다. 자바 API 프레임워크 계층은 애플리케이션 계층과 하위 계층이 소통할 수 있도록 하는 다리 역할을 한다. 애플리케이션의 액티비티들을 관리하는 매니저, 알림을 관리하는 매니저, 리소스를 관리하는 매니저 등의 안드로이드 프레임워크가 존재하고 개발자들은 이를 활용하여 애플리케이션을 개발하게 된다.

 

 

 

7. Android의 입력 

 

이번 실습에선 안드로이드 디바이스의 터치스크린, 키패드, 키보드 등 다양한 입력 장치를 사용하였다. 특히 키패드의 이벤트 입력을 테스트하기 위해 안드로이드 입력 이벤트가 어떻게 처리되었는지 얕게나마 따라가 볼 수 있는 기회가 되었다. 

 

안드로이드 공식 레퍼런스를 통해 입력 기기 드라이버는 Linux 입력 프로토콜을 통해 기기별 신호를 이벤트 형식으로 변환한다. Linux의 입력 프로토콜은 linux/input.h 커널 헤더 파일에 이벤트 유형 및 코드 집합이 정의되어 있다는 정보를 찾게 되었다. 실제로 사용하고자 하는 키패드의 키 코드 값이 linux/input.h 파일에 정의되어 있었고, 이를 매핑하여 키패드를 사용할 수 있었다. 아래 그림의 왼쪽은 linux/input.h의 키 코드 값이 명시된 표, 오른쪽은 이를 바탕으로 코드 값을 수정한 키패드 드라이버 코드이다.

 

파일 시스템 안에 각 입력 장치의 이벤트를 다루는 엔트리가 있다. 아래 명령어를 통해 각 엔트리에 매핑된 장치 정보를 확인할 수 있다. 단, 매핑 정보는 영구적이지 않다. 부팅 시 커널에 의해 매핑된다고 생각하고 있다. 

cat /proc/bus/input/devices

 

아래는 엔트리 정보 출력의 예시이다. 아래 예시에선 사용하려는 기기 fpga_keypad가 event4에 매핑되었다는 것을 확인할 수 있다. 

I: Bus=0018 Vendor=0001 Product=0002 Version=0100
N: Name="zinitix_touch"
P: Phys=input(ts)
S: Sysfs=/devices/virtual/input/input0
U: Uniq=
H: Handlers=kbd event0
B: PROP=0
B: EV=b
B: KEY=2000000 0 40000800 40 0 0 0
B: ABS=650000 10000000

I: Bus=0018 Vendor=0000 Product=0000 Version=0000
N: Name="im3640"
P: Phys=
S: Sysfs=/devices/virtual/input/input1
U: Uniq=
H: Handlers=event1
B: PROP=0
B: EV=9
B: ABS=3043f

I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="mpu3050"
P: Phys=
S: Sysfs=/devices/virtual/input/input2
U: Uniq=
H: Handlers=event2
B: PROP=0
B: EV=5
B: REL=38

I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="proximity"
P: Phys=
S: Sysfs=/devices/virtual/input/input3
U: Uniq=
H: Handlers=event3
B: PROP=0
B: EV=9
B: ABS=2000000

I: Bus=0019 Vendor=1002 Product=1002 Version=0000
N: Name="fpga-keypad"
P: Phys=keypad/input1
S: Sysfs=/devices/virtual/input/input4
U: Uniq=
H: Handlers=sysrq kbd event4
B: PROP=0
B: EV=3
B: KEY=3ffffffe

I: Bus=0019 Vendor=16b4 Product=0701 Version=0001
N: Name="s4210-keypad"
P: Phys=s4210-keypad/input0
S: Sysfs=/devices/virtual/input/input5
U: Uniq=
H: Handlers=kbd event5
B: PROP=0
B: EV=23
B: KEY=1c0000 0 0 0
B: SW=1

I: Bus=0003 Vendor=0a5c Product=4502 Version=0111
N: Name="HID 0a5c:4502"
P: Phys=usb-s5p-ehci-1.3.1/input0
S: Sysfs=/devices/platform/s5p-ehci/usb1/1-1/1-1.3/1-1.3.1/1-1.3.1:1.0/input/input6
U: Uniq=
H: Handlers=sysrq kbd event6
B: PROP=0
B: EV=120013
B: KEY=10000 7 ff800000 7ff febeffdf f3cfffff ffffffff fffffffe
B: MSC=10
B: LED=7

I: Bus=0003 Vendor=0a5c Product=4503 Version=0111
N: Name="HID 0a5c:4503"
P: Phys=usb-s5p-ehci-1.3.2/input0
S: Sysfs=/devices/platform/s5p-ehci/usb1/1-1/1-1.3/1-1.3.2/1-1.3.2:1.0/input/input7
U: Uniq=
H: Handlers=event7
B: PROP=0
B: EV=17
B: KEY=70000 0 0 0 0 0 0 0 0
B: REL=3
B: MSC=10

 

이제 event4에 매핑된 엔트리 (/dev/input/event4)에서 입력을 받으면 해당 기기에서 입력되어 발생한 입력 이벤트를 출력할 수 있게 된다. 아래 코드를 디바이스에서 실행시키는 것으로 디바이스의 keypad 사용 가능 여부를 확인하였다.

int main(void) {
     size_t read_bytes;
     struct input_event event_buf[64];

     int fd = open("/dev/input/event4", O_RDONLY);
     
     if(fd < 0) {
         printf("application : keypad driver open fail!\n");
         exit(1);
     }

     read(fd, event_buf, sizeof(struct input_event) * 64);

     int quit = 1;
     int i = 1;
     while(quit) {
         read_bytes = read(fd, event_buf, (sizeof(struct input_event)*64) );
         for(i=0; i<(read_bytes/sizeof(struct input_event)); i++ ) {
             if ((event_buf[i].type == EV_KEY) && (event_buf[i].value == 0)) {
                 printf("\n  Button key : %d\n", event_buf[i].code);
             }
         }
     }
     close(fd);
     return 0;
}

 

Comments