키보드를 만듭시다. 어때요~ 참 쉽죠? (7)

나빌레라의 이미지

하드웨어 GPIO에서 전기 신호를 읽어서 몇 행, 몇 열에 있는 키보드 스위치가 눌렸는지 이제 펌웨어는 알 수 있다. 그러면 다음에 할 일은 이 행열 정보를 가지고 어떤 키가 눌렸는지 변한하는 것이다. 다시 말해 그저 (x, y)로 전달 받은 매트릭스 위치 정보를 호스트 운영체제가 활용하는 HID 키보드 스캔 코드로 변환하는 것이다.

키맵 자료 구조

하드웨어로 고정된 키 매트릭스는 하드웨어 배선을 물리적으로 바꾸지 않는한 변하지 않는다. 소프트웨어로는 어찌 할 수 없다는 말이다. 키 매트릭스를 폴링해서 행열 키 입력 정보를 배열로 받아서 다시 키맵 배열과 매칭해서 키 스캔 코드를 구한다. 키 스캔 코드는 8비트 값이다. 그래서 단순하게 8비트 2차원 배열로 만들면 된다.

static uint8_t sKeymap_buffer_layer0[KEYMAP_ROW_NUM][KEYMAP_COL_NUM] =
{           /* Col#0    Col#1       Col#2       Col#3       Col#4   Col#5       Col#6       Col#7   Col#8   Col#9   Col#10      Col#11      Col#12      Col#13 */
/* Row#0 */ {kEsc,      kF1,        kF2,        kF3,        kF4,    kF5,        kF6,        kF7,    kF8,    kF9,    kF10,       kF11,       kF12,       kBackspace},
/* Row#1 */ {kPageup,   kGrave,     k1,         k2,         k3,     k4,         k5,         k6,     k7,     k8,     k9,         k0,         kMinus,     kEqual},
/* Row#2 */ {kPagedown, kTab,       kQ,         kW,         kE,     kR,         kT,         kY,     kU,     kI,     kO,         kP,         kLeftbrace, kRightbrace},
/* Row#3 */ {kInsert,   kFunction,  kA,         kS,         kD,     kF,         kG,         kH,     kJ,     kK,     kL,         kSemicolon, kApostrophe,kEnter},
/* Row#4 */ {kHome,     kLeftshift, kZ,         kX,         kC,     kV,         kB,         kN,     kM,     kComma, kDot,       kSlash,     kRightshift,kUp},
/* Row#5 */ {kEnd,      kLeftctrl,  kLeftmeta,  kLeftalt,   kSpace, kRightalt,  kRightctrl, kLeft,  kDown,  kRight, kBackslash, kDelete,    kNone,      kNone}
};

위 코드에서 kF1, kZ 이렇게 k어쩌구 하는 값은 내가 enum으로 따로 선언해 놓은 HID 키 스캔 코드 값이다. 이 값은 HID 스팩에 있는 값 그대로니까 궁금하신 분들은 인터넷에서 찾아보기 바란다.

이 배열 값은 하드웨어에 의존하지 않는 순수 소프트웨어 구현이다. 따라서 적절한 기능 처리를 하면 런타임에 배치를 바꿀 수 있다. 이 말은 무슨 뜻이냐면 키보드 키맵을 사용자가 원하는 대로 바꿀 수 있다는 뜻이다. 예를 들어 Ctrl, Alt 키 배치 순서가 마음에 들지 않는 다면 이 키맵 배열 값을 바꿔서 순서를 바꿀 수 있다. 같은 원리로 A 자리에 K를 넣을 수도 있다. 즉, 쿼티가 아니라 드보락 배치를 원하는 사람은 운영체제 입력기에서 설정을 하는 대신 키보드 키맵을 바꿔서 드보락 전용 키보드를 만들 수도 있다는 말이다.

키맵 매칭 코드

원리는 간단하다. 하드웨어 폴링 모듈에서 넘어온 결과가 (R2, C4)라면 그대로 sKeymap_buffer_layer0[2, 4]에 있는 스캔 코드 값을 레포트에 추가하면 된다. HID 스팩에 따르면 키보드 레포트에 일반키는 최대 6개까지므로 6개가 넘어가면 무시한다.

void KeyMap_getReport(bool isPressedFnKey, uint8_t* hidKeyboardReport, KeyHwAddr_t* hwPollingAddrs, uint32_t pollingCount)
{
    uint32_t idx = HID_KEY_START_IDX;
    uint32_t row = 0;
    uint32_t col = 0;
 
    uint8_t modifierKeyBitmap = 0;
    uint8_t scancode = 0;
 
    while (pollingCount--)
    {
        row = hwPollingAddrs->bit.row;
        col = hwPollingAddrs->bit.col;
        hwPollingAddrs++;
 
        if (hwPollingAddrs->bit.fn == 1) // NOT report Fn key code
        {
            continue;
        }
 
        if (!isPressedFnKey)
        {
            scancode = sKeymap_buffer_layer0[row][col];
        }
        else
        {
            scancode = sKeymap_buffer_layer1[row][col];
        }
 
        if (scancode == 0)
        {
            continue;
        }
 
        modifierKeyBitmap = GetModifierKeyBitmap(scancode);
 
        debug_printf("MODI:%x SCANCODE:%x\n", modifierKeyBitmap, scancode);
 
        if (modifierKeyBitmap == 0) // Not modifier key pressed
        {
            hidKeyboardReport[idx++] = scancode;
 
            if (idx >= HID_KBD_REPORT_BYTE)
            {
                break;
            }
        }
        else
        {
            hidKeyboardReport[HID_MODIKEY_IDX] |= modifierKeyBitmap;
        }
    }
 
    SortReport(hidKeyboardReport);
}

역시 긴 것 같지만 곁다리 코드가 많아서 그렇지, 핵심 코드는 간단하다. 핵심만 추려서 의사 코드를 써보면 이렇다.

hwPollingAddrs = KeyHw_polling();
pollingCount = len(hwPollingAddrs);
 
modiIdx = 0;
keyIdx = 2;
 
while pollingCount-- {
    row = hwPollingAddrs.row;
    col = hwPollingAddrs.col;
    hwPollingAddrs++;
 
    scancode = sKeymap_buffer_layer0[row][col];
    modikey = GetModifierKeyBitmap(scancode);
 
    hidKeyboardReport[modiIdx] |= modikey;
    hidKeyboardReport[keyIdx++] = scancode;
 
    if keyIdx >= 8 {
        break;
    }
}
 
InMemorySort(hidKeyboardReport);

hwPollingAddrs는 저번 글에서 설명했던 GPIO 하드웨어 폴링 결과 배열이다. 키 스캔 코드 매칭은 하드웨어 폴링 결과로 받아온 동시 입력 개수만큼만 돌면 되기 때문에 pollingCount에 개수를 받아 놓는다. 그리고 그 개수 만큼만 while 루프를 돈다. row와 col은 매 루프마다 hwPollingAddrs 배열에서 가져온 값이다. 그리고 row, col 그대로 이차원 배열 인덱스로 넣어서 sKeymap_buffer_layer0 테이블에서 스캔 코드를 받는다.

GetModifierKeyBitmap() 함수는 스캔 코드가 모디키인지 확인하고 모디키라면 비트맵 값으로 바꿔서 리턴한다. HID 스팩에 보면 모디키는 0번째 8비트에 비트맵으로 정보를 보내라고 정해져있다.

  • kModLctrl = 0x01,
  • kModLshift = 0x02,
  • kModLalt = 0x04,
  • kModLmeta = 0x08,
  • kModRctrl = 0x10,
  • kModRshift = 0x20,
  • kModRalt = 0x40,
  • kModRmeta = 0x80

위와 같이 변환한다. 이렇게 변환한 값을 OR 연산으로 합치면 비트맵으로 호스트에 레포트를 보낸다. 예를 들어 왼쪽 Ctrl(0b00000001)과 Alt(0b00000100)를 동시에 눌렀다면 레포트 0번 바이트 값은 0x05(0b00000101)가 된다.

그리고 일반 키는 HID 스팩에 따라 2번째 바이트부터다. 그래서 idx 변수의 초기값은 2다. 스캔 코드를 추가할 때마다 하나씩 값을 올리다가 idx가 8이상이 되면 더이상 루프를 돌지 않고 루프른 종료한다. 이건 스팩이므로 여러분이 쓰고 있는 키보드도 최대 6개까지만 동시 입력이 처리된다. 지금 당장 텍스트 에디터 아무거나 열고 열 손가락을 동시에 눌러보라. 화면엔 6개만 찍힐 것이다.

HID 스팩에 키보드 레포트는 정렬해서 보내라고 되어 있으므로 while 루프를 다 처리하고 나면 소팅을 한다. 소팅은 그냥 가장 단순한 버블 소팅이다. 최대 6개 소팅하는데 복잡한 알고리즘을 쓸 필요는 없다.

키 맵 폴링 태스크

키보드 펌웨어의 핵심은 다 설명했다. 못 믿겠지만 진짜다. 이후로는 부가기능들을 구현하는 것들 뿐이다. 그래서 키보드 펌웨어 핵심 부분은 키 코드 폴링 관련 함수들을 무한 루프 돌면서 계속 호출하는 사용자 태스크에서 관리한다. 지난 번에 만들었던 UART 디버그 콘솔 태스크 이후로 두 번째 사용자 태스크다.

void Polling_task(void)
{
    debug_printf("Polling Task....\n");
 
    bool pressedFnKey = false;
    uint32_t pollingCount = 0;
 
    KeyHwAddr_t hwPollingAddrs[HID_MAX_MULTIPLE_INPUT];
 
    uint8_t HIDKeyboardReport[HID_KBD_REPORT_BYTE];
    uint8_t HIDKeyboardReportOld[HID_KBD_REPORT_BYTE];
 
    LoadKeymap();
 
    while (true)
    {
        USBD_Delay(HID_FS_BINTERVAL);
 
        memclr(hwPollingAddrs, HID_MAX_MULTIPLE_INPUT);
        memclr(HIDKeyboardReport, HID_KBD_REPORT_BYTE);
 
        pollingCount = KeyHw_polling(hwPollingAddrs, HID_MAX_MULTIPLE_INPUT);
 
        pressedFnKey = KeyMap_checkFnKey(hwPollingAddrs, pollingCount);
        KeyMap_getReport(pressedFnKey, HIDKeyboardReport, hwPollingAddrs, pollingCount);
 
        if (memncmp(HIDKeyboardReportOld, HIDKeyboardReport, HID_KBD_REPORT_BYTE) == false)
        {
            Kernel_send_msg(KernelMsgQ_D2hData, HIDKeyboardReport, HID_KBD_REPORT_BYTE);
            Kernel_send_events(KernelEventFlag_SendD2H);
 
            memncpy(HIDKeyboardReportOld, HIDKeyboardReport, HID_KBD_REPORT_BYTE);
        }
 
        Kernel_yield();
    }
}

위 코드가 실제 펌웨어 코드다. while(true)로 무한 루프 돌면서 하드웨어 폴링 정보를 읽고 레포트를 만들어 KernelMsgQ_D2hData 메시지 큐에 레포트 데이터를 넣는다. 그리고 KernelEventFlag_SendD2H 이벤트를 보낸다. 호스트와 통신하는 부분은 사용자 태스크를 따로 분리했다. 그래서 호스트 통신 사용자 태스크에서 KernelEventFlag_SendD2H 이벤트를 처리할 것이다.

키 맵 폴링 사용자 태스크의 동작 순서를 간단히 설명하면 다음과 같다.

  1. HID 스팩에 맞춰 USB 디스크립터로 호스트에 보고한 시간만큼 대기한다. : USBD_Delay(HID_FS_BINTERVAL);
  2. 로컬 변수를 초기화 한다. : memclr() 함수 두 개
  3. 하드웨어 GPIO 스위치 입력 폴링 : pollingCount = KeyHw_polling(hwPollingAddrs, HID_MAX_MULTIPLE_INPUT);
  4. Fn 키가 눌렸는지 확인 : pressedFnKey = KeyMap_checkFnKey(hwPollingAddrs, pollingCount);
  5. HID 키보드 레포트 구성 : KeyMap_getReport(pressedFnKey, HIDKeyboardReport, hwPollingAddrs, pollingCount);
  6. 직전에 호스트에 보냈던 레포트와 방금 만든 레포트가 같은지 검사 : if (memncmp(HIDKeyboardReportOld, HIDKeyboardReport, HID_KBD_REPORT_BYTE) == false)
    1. 레포트가 다를 때만 메시지 큐와 이벤트 보냄 : KernelMsgQ_D2hData, KernelEventFlag_SendD2H
    2. 방금 호스트에 보낸 레포트를 백업 : memncpy(HIDKeyboardReportOld, HIDKeyboardReport, HID_KBD_REPORT_BYTE);
  7. 컨택스트를 커널에 넘김 : Kernel_yield();

키 맵 폴링 사용자 태스크가 Kernel_yield()를 호출하면 다른 태스크가 컨택스트를 받고 그 중 하나가 호스트 통신 사용자 태스크다. 호스트 통신 사용자 태스크는 KernelEventFlag_SendD2H를 처리한다. 그러므로 별 문제가 생기지 않는한 키 맵 폴링 사용자 태스크가 다시 커널 컨택스트를 받으면 메시지 큐는 비어있을 것이다.

호스트 통신 사용자 태스크

호스트 통신 사용자 태스크는 이름이 그 기능을 충실히 설명한다. 호스트와 통신을 전담하는 사용자 태스크다. 먼저 코드를 본다.

void Host_comm_task(void)
{
    debug_printf("Host_comm_task....\n");
 
    while (true)
    {
        KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_SendD2H);
        if (handle_event == KernelEventFlag_SendD2H)
        {
            SendUSBHID();
        }
        Kernel_yield();
    }
}

간단하다. 무한 루프 돌면서 KernelEventFlag_SendD2H 이벤트를 기다린다. 이벤트가 들어오면 SendUSBHID() 함수를 호출한다. 이게 전부다. 뭐 설명을 더 하고 싶어도 할게 없다.

SendUSBHID() 함수도 이름이 그 기능을 잘 설명한다. 코드를 보자.

static void SendUSBHID(void)
{
    uint8_t HIDKeyboardReport[HID_KBD_REPORT_BYTE];
 
    Kernel_recv_msg(KernelMsgQ_D2hData, HIDKeyboardReport, HID_KBD_REPORT_BYTE);
 
    App_hid_send(HIDKeyboardReport, HID_KBD_REPORT_BYTE);
}

코드도 간단하다. 위에 키 맵 폴링 사용자 태스크에서 KernelMsgQ_D2hData 메시지 큐에 보낸 HID 키보드 레포트 데이터를 메시지 큐에서 다시 꺼내서 HIDKeyboardReport 로컬 배열 변수에 넣는다. 그리고 이 값을 App_hid_send() 함수로 호스트에 보낸다. 이러면 USB 미들웨어와 HAL을 타고 호스트로 전달된 레포트가 호스트에서 해석되어 화면에 보인다.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.