키보드를 만듭시다. 어때요~ 참 쉽죠? (4)
HID USB 키보드 펌웨어를 만들기로 했으니까 당연히 제일 먼저 HID USB 코드를 작성한다. 그래서 컴퓨터에 꼽아보고 HID 키보드로 인식되는 모습을 먼저 봐야 동기부여가 될 것이다. 프로그램을 만드는 작업은 일단 작업 내용을 바로바로 눈으로 볼 수 있어야 재미있게 할 수 있다.
HID USB keyboard 펌웨어
구글이나 깃허브에서 "stm32 hid keyboard firmware"로 검색하면 결과가 꽤 많이 나온다. 그리고 다들 코드가 한결같이 비슷하다. 혹시나 해서 찾아봤더니 SDK에 HID 관련 예제가 있었다. 다들 SDK에 있는 HID 예제 기반으로 조금씩 고친걸 인터넷에 공개한 모양이다. 그래서 아무거나 받아서 써도 된다. 나도 아무거나 받았다. 다만, 이미 UART를 사용하려고 HAL을 받았기 때문에 HAL이 충돌나지 않는 것을 골랐다. 걱정할 것 없다. 인터넷에서 구할 수 있는 STM32 펌웨어 코드는 거의다 같은 HAL 코드를 쓰고 있다. 나는 HAL 버전이라든가 심하게 바뀐 API라든가하는 것들을 한 번 더 확인했을 뿐이다.
STM32에 USB 컨트롤러 하드웨어를 제어하는 펌웨어는 거의 공통적으로 MiddleWare라는 디렉터리에 USB 관련 설정 코드를 넣어 놨다. 아마 SDK 툴이 그렇게 만들어 주나보다. USB 컨트롤러 하드웨어를 레지스터 수준에서 제어하는 일은 HAL에서 맡고 USB 스펙에 맞춰 디스크립터나 인터럽트, 콜백을 제어하는 코드는 MiddleWare로 분리했나보다.
나도 이 MiddleWare 디렉터리에 있는 파일들을 복사해서 나빌로스 디렉터리 구조 어딘가에 넣어야 한다. 똑같이 MiddleWare라고 이름 붙여서 넣거나 다른 구조를 디자인 해야한다. 나는 다른 구조를 디자인했다.
app └── USB ├── usbd_hid.c ├── usbd_hid_desc.c ├── usbd_hid_desc.h ├── usbd_hid.h ├── usb_hid_keyboard.c ├── usb_hid_keyboard.h ├── usbd_conf.c ├── usbd_conf.h ├── usbd_core.c ├── usbd_core.h ├── usbd_ctlreq.c ├── usbd_ctlreq.h ├── usbd_def.h ├── usbd_ioreq.c └── usbd_ioreq.h
미들웨어와 태스크를 다 퉁쳐서 어플리케이션 레이어로 묶었다. 그래서 App이라는 이름으로 디렉터리를 만들고 그 아래에 USB 라는 이름으로 USB 관련 코드를 모아놨다. 앞으로 이 디렉터리에 사용자 태스크 구현 코드가 자리할 것이다.
HID USB keyboard report format
이 글을 쓰고 있는 시점에서 나는 HID 스펙이나 USB 스펙을 제대로 모른다. 이 글을 내가 만든 HID USB 펌웨어가 동작하고 있는 키보드로 쓰고 있음에도 모른다. 그냥 인터넷에서 예제 코드 긁어서 SDK 샘플 코드랑 적절히 섞어 펌웨어를 만들었더니 잘 동작한다. 전형적인 전문성없는 프로그래머의 개발 장면 아니겠는가! 자료가 얼마나 많은지 호스트 인터페이스 스팩 한 줄 안 읽고도 호스트 인터페이스 펌웨어를 만들 수 있다는 말을 하고 싶었다.
내가 HID에 대해 공부한 건 딱 하나 Keyboard input report 스펙 본 것이 전부다. 나머지는 SDK에 있는 샘플 코드 함수 구조 보고 흐름 파악해서 어떤 함수에 어떤 형태로 데이터를 보내면 되겠구나를 찾아낸 것 뿐이다.
HID USB 키보드 레포트가 어떻게 생겼는지 공부하려고 구글에 "hid keyboard report"라고 입력하면 많이 나온다. 그 중 내가 참고한 사이트는 아래 두 개다.
- https://files.microscan.com/helpfiles/ms4_help_file/ms-4_help-02-46.html
- https://wiki.osdev.org/USB_Human_Interface_Devices
두 사이트의 내용을 보면 설명이 조금 다르다. 이것 때문에 내가 HID 스펙 문서를 살짝 자세히 봤다. 물론 그래도 나보고 HID 잘 아냐고 물으면 여전히 모른다고 대답할 것이다. 어떻게 다르냐면,
첫 번째 사이트에서는
REPORT ID (1) MODIFIER (1) RESERVED (1) KEYCODES (6)
키보드 레포트 구조가 이렇게 생겼다고 설명한다. 그러나 두 번째 사이트는
MODIFIER (1) RESERVED (1) KEYCODES (6)
이렇게 생겼다고 설명한다. 맨 앞에서 REPORT ID라는 1바이트가 다르다. 도대체 뭐가 맞는 거냐...
결론부터 말하면 둘 다 맞다. 왜 둘 다 맞냐면, HID USB 디스크립터를 만들어 보낼 때, 설정하는 값에 따라 호스트는 레포트 포멧을 다르게 인식하기 때문이다.
HID USB 디스크립터
USB는 수 많은 장치를 디스크립터로 구분하고 관리한다. 그래서 USB로 연결되는 장치는 모두 자기가 속한 class 스펙에 맞춰 디스크립터를 USB 호스트에 보내야 한다. 이 디스크립터 포멧은 당연히 디바이스 클래스마다 다르다. USB 스팩에서는 최상위 헤더와 기타 정보를 정의하고 하위 페이로드는 reserved로 둔 다음 각각 디바이스 클래스 스펙에서 해당 reserved 영역에 또 헤더와 기타 정보를 정의하는 식이다. 같은 방법으로 클래스에 속하는 디바이스 스펙은 또 헤더와 정보를 정의하고..
이걸 다 알 필요는 없다. 필요한 정보만 최소한으로 조사한 다음 어디를 고치면 되는지만 알면 된다.
내가 최초로 사용한 코드에서 HID report descriptor 부분 코드는 아래와 같다.
__ALIGN_BEGIN static uint8_t HID_CUSTOM_ReportDesc[HID_CUSTOM_REPORT_DESC_SIZE] __ALIGN_END = { // 130 bytes 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x85, 0x01, // Report ID (1) 0x05, 0x07, // Usage Page (Kbrd/Keypad) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x19, 0xE0, // Usage Minimum (0xE0) 0x29, 0xE7, // Usage Maximum (0xE7) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 0x95, 0x06, // Report Count (6) 0x75, 0x08, // Report Size (8) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x7f, // Logical Maximum (127) 0x05, 0x07, // Usage Page (Kbrd/Keypad) 0x19, 0x00, // Usage Minimum (0x00) 0x29, 0x7f, // Usage Maximum (0x7f) 0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) 0xC0, // End Collection 0x05, 0x0C, // Usage Page (Consumer) 0x09, 0x01, // Usage (Consumer Control) 0xA1, 0x01, // Collection (Application) 0x85, 0x02, // Report ID (2) 0x05, 0x0C, // Usage Page (Consumer) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x09, 0xB5, // Usage (Scan Next Track) 0x09, 0xB6, // Usage (Scan Previous Track) 0x09, 0xB7, // Usage (Stop) 0x09, 0xB8, // Usage (Eject) 0x09, 0xCD, // Usage (Play/Pause) 0x09, 0xE2, // Usage (Mute) 0x09, 0xE9, // Usage (Volume Increment) 0x09, 0xEA, // Usage (Volume Decrement) 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 0xC0, // End Collection 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x02, // USAGE (Mouse) 0xa1, 0x01, // COLLECTION (Application) 0x85, 0x03, // Report ID (3) 0x09, 0x01, // USAGE (Pointer) 0xa1, 0x00, // COLLECTION (Physical) 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (Button 1) 0x29, 0x03, // USAGE_MAXIMUM (Button 3) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x95, 0x03, // REPORT_COUNT (3) 0x75, 0x01, // REPORT_SIZE (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x05, // REPORT_SIZE (5) 0x81, 0x03, // INPUT (Cnst,Var,Abs) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x30, // USAGE (X) 0x09, 0x31, // USAGE (Y) 0x15, 0x81, // LOGICAL_MINIMUM (-127) 0x25, 0x7f, // LOGICAL_MAXIMUM (127) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x02, // REPORT_COUNT (2) 0x81, 0x06, // INPUT (Data,Var,Rel) 0xc0, // END_COLLECTION 0xc0, // END_COLLECTION // 69 bytes 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x05, // Usage (Game Pad) 0xa1, 0x01, // Collection (Application) 0x85, 0x04, // Report ID (4) 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x00, // Usage (Undefined) 0x75, 0x08, // Report Size (8) 0x95, 0x01, // Report Count (1) 0x81, 0x03, // Input (Constant, Variable, Absolute) 0xa1, 0x00, // Collection (Physical) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (Button 1) 0x29, 0x10, // Usage Maximum (Button 16) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x10, // Report Count (16) 0x81, 0x02, // Input (Data, Variable, Absolute) 0x75, 0x10, // Report Size (16) 0x16, 0x00, 0x80, // Logical Minimum (-32768) 0x26, 0xff, 0x7f, // Logical Maximum (32767) 0x36, 0x00, 0x80, // Physical Minimum (-32768) 0x46, 0xff, 0x7f, // Physical Maximum (32767) 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x01, // Usage (Pointer) 0xa1, 0x00, // Collection (Physical) 0x95, 0x02, // Report Count (2) 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x81, 0x02, // Input (Data, Variable, Absolute) 0xc0, // End Collection 0xc0, // End Collection 0xc0, // End Collection };
숫자가 많다. 그리고 엄청 길다. 옆에 주석으로 써 놨듯 각자 다 의미가 있는 숫자다. 그러나 나에겐 의미 없다. 알아서 적당한 숫자가 들어가 있겠지. 중요한건 디스크립터에 키보드, 마우스, 게임 패드를 다 정의해 놨다는 거다. 나는 키보드만 필요한데.
따라서 위 디스크립터 설정을 사용하면 HID 장치가 키보드와 마우스를 동시에 지원하는 장치라고 호스트에 알린다. (게임 패드도!) 키보드들 중에 Fn 키와 특정 키를 누르면 마우스 커서를 제어하는 제품이 있다. 이 제품들이 위 값을 HID configuration descriptor로 보내는 것이다. 혹은 키보드와 터치패트가 일체형으로 나오는 제품들은 반드시 위 값으로 HID configuration descriptor를 만들어야 한다. 키보드와 트랙볼이 일체형인 제품도 마찬가지다.
이렇게 키보드, 마우스가 한 장치에 같이 있으면 같은 장치에서 보내는 레포트지만 이게 키보드 레포트인지 마우스 레포트인지 구분해야 한다. 그래서 HID 스팩에 보면 이럴 때 쓰라고 REPORT ID라는 것을 만든 것이다.
그런데 만약 장치가 키보드 전용 혹은 마우스 전용이라면 REPORT ID는 필요 없다. HID configuration descriptor에 "나는 키보드입니다"라고 표시하면 호스트는 앞으로 이 장치는 무조건 키보드라고 인식하면 되는 것이다. 그래서 REPORT ID 필드가 없는 레포트 포멧도 맞다.
나는 키보드 전용으로 만들 생각이다. 나는 터치패드가 달린 무선 키보드와 마우스 에뮬레이팅 기능이 있는 키보드를 다 가지고 있다. 써 봤더니 터치패드나 트랙볼처럼 진짜 마우스 장치를 다는 것이 아닌한 키보드 키로 아무리 마우스를 흉내내봤자 딱히 그리 유용하지 않았다. 그래서 내가 만드는 펌웨어는 그냥 키보드 전용으로 만들기로 했다. 나중에 바꿀일 있으면 그때 바꾸면 되니까. 방법은 간단하다. 위 레포트 디스크립터에서 키보드 부분을 제외하고 나머지 디스크립터 코드를 지워버리면 된다.
__ALIGN_BEGIN static uint8_t HID_KEYBOARD_ReportDesc[HID_KEYBOARD_REPORT_DESC_SIZE] __ALIGN_END = { 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x06, // USAGE (Keyboard) 0xa1, 0x01, // COLLECTION (Application) 0x05, 0x07, // USAGE_PAGE (Keyboard)(Key Codes) 0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)(224) 0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)(231) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x08, // REPORT_COUNT (8) 0x81, 0x02, // INPUT (Data,Var,Abs) ; Modifier byte 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x08, // REPORT_SIZE (8) 0x81, 0x03, // INPUT (Cnst,Var,Abs) ; Reserved byte 0x95, 0x05, // REPORT_COUNT (5) 0x75, 0x01, // REPORT_SIZE (1) 0x05, 0x08, // USAGE_PAGE (LEDs) 0x19, 0x01, // USAGE_MINIMUM (Num Lock) 0x29, 0x05, // USAGE_MAXIMUM (Kana) 0x91, 0x02, // OUTPUT (Data,Var,Abs) ; LED report 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x03, // REPORT_SIZE (3) 0x91, 0x03, // OUTPUT (Cnst,Var,Abs) ; LED report padding 0x95, 0x06, // REPORT_COUNT (6) 0x75, 0x08, // REPORT_SIZE (8) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x65, // LOGICAL_MAXIMUM (101) 0x05, 0x07, // USAGE_PAGE (Keyboard)(Key Codes) 0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))(0) 0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)(101) 0x81, 0x00, // INPUT (Data,Ary,Abs) 0xc0 // END_COLLECTION };
변수 이름도 성격에 맞게 바꿨다. 이러면 이제 호스트는 위 값을 읽어서 해당 장치를 키보드만 있는 HID 장치로 인식한다. 그러므로 펌웨어에서도 아래 포멧으로 키보드 레포트를 보내면 된다.
struct hid_scan_report { uint8_t modifier; uint8_t reserved; uint8_t key[6]; };
샘플 스캔 코드 보내기
코드 구현을 보니, 키보드 레포트를 구성하고 HID_Send() 함수로 구조체 인스턴스 포인터를 보내면 하드웨어가 레포트 데이터를 호스트로 보내는 것 같다. 진짜 그런지 코드를 만들고 테스트해보자.
main() 함수를 아래처럼 고친다.
struct hid_scan_report test; while(1) { memclr(&test, sizeof(struct hid_scan_report)); HID_Send(&test, sizeof(struct hid_scan_report)); HAL_Delay(100); test.key[0] = 0x11; HID_Send(&test, sizeof(struct hid_scan_report)); HAL_Delay(10); }
위 코드는 0x11 (HID 스팩에서 정의한 알파벳 n 키 스캔 코드)를 키보드 레포트에 넣고, 이 키보드 레포트 값을 HID_Send() 함수로 보내는 코드다. 무한 루프를 돌면서 계속 반복하는데, 자연스럽게 호스트에서 보이게 하고 싶었다. 그래서 n을 보내고 (key pressed) 약 10ms 대기한 다음 0x00으로 채운 키보드 레포트를 보낸다. 0x00으로 채운 키보드 레포트는 방금 누른 키를 뗏다(key released)는 의미다. 그리고 한 100ms 대기한 다음 다시 n을 보내는 것을 반복한다. 그래서 호스트 입장에서 보면 100ms 간격으로 n키를 계속 눌렀다 뗏다하는 수퍼맨이 보인다. 사람은 100ms 간격으로 키를 눌렀다 뗏다하지 못한다.
댓글
감사...압도적 감사...
키보드 제작에 관심이 있어서 키보드매냐에서 나빌레라님 포스트를 읽었는데,
펌웨어도 직접 개발하셨다는 글귀에 감동을 받았고, 관련 내용을 검색하니 펌웨어 개발에 대한 포스팅이..!
임베디드에 관심이 있어서 덜컥 Cortex m2 개발보드 사놓고 아무것도 몰라 RTOS는 커녕 먼지만 올려놓고 있었는데...
디바이스 내부에 이런 코드가 올라가서 작동하는게 너무 신기합니다.
포스팅 잘 읽었습니다 너무 감사합니다.
별말씀을요. vertex님도 키보드 한 개 만들어
별말씀을요. vertex님도 키보드 한 개 만들어 보세요. 의외로 할만 하고 재미있습니다.
----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라
대단하시네요. 질문하나 있습니다.
무한동시입력을 구현하는거는 어떻게 하나요? 프로토콜을 좀 비튼다 하는데
키코드가 동시에 6개밖에 안가는데 어떻게 구현할까요...
가상의 HID 한개 더 추가하는 방식은 아두이노에서 가능할까요?
답장은 apple191246@gmail.com으로 주세요...
나중에 되면 여기 링크도 까먹음 ㅜㅜ
???? 딱히 대답하고 싶지 않은 질문 태도입니다.
????
딱히 대답하고 싶지 않은 질문 태도입니다.
----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라
감사합니다.
f103 의로 작업을 하고 있습니다.
덕분에 코드를 완성할수 있었습니다.
저그런데 혹시 키보드에 대한 키값 정보를 더 알수있을까요?
혹시 키값에 자료를 좀더 알고싶은데.
엔터와 스페이스,esc 같은 값이 없어서요
헤더파일에 키코드값 전체를 define 해 놓은
헤더파일에 키코드값 전체를 define 해 놓은 코드가 있습니다.
----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라
댓글 달기