키보드를 만듭시다. 어때요~ 참 쉽죠? (6)
지금까지는 사실 키보드 기능 자체에 대한 기능 보다 펌웨어 자체를 동작하는데 집중했다. 키보드 관련한 것이라고는 HID USB 관련 코드를 추가한 것 뿐이다. 그러나 정작 HID 모듈로 보내는 데이터는 유닛 테스트 수준으로 만들어 넣고 있다. 이제 진짜로 키보드 스위치 입력을 펌웨어에서 인식해서 키 스캔 코드를 만드는 작업을 한다.
키 매트릭스
컨트롤러 보드에 GPIO가 많다 한들 100개 이하다. 물론 GPIO 포트가 100개가 넘는 보드들도 있긴 한데 비싸다. 내가 산 컨트롤러 보드는 $1.x 짜리다. 이천원짜리 보드지만 그래도 GPIO가 40개 정도 있다. 40개면 많은 것이기는 한데 100개 가까이되는 키보드 스위치 입력을 개별적으로 처리하기엔 모자르다. 그래서 키보드 스위치를 매트릭스로 만들어서 처리한다.
위 그림은 1편에서 나왔던 그림이다. 내가 만든 키보드 레이아웃이다. 내가 만든 키보드는 82키다. 일반적인 키보드와 같이 맨 위에 펑션키열(F1~F12)이 있고 이어서 숫자(특수문자)키열, 그리고 기본 알파벳이 있는 3열과 맨 아래 모디키와 스페이스바키열이 있다. 그래서 6열이다. 그리고 숫자키 열이 가장 키가 많은 열인데 이 열에 키가 14개 있다. 그래서 6x14 매트릭스를 만들면 된다.
COL (input) +----+----------------------------------------------------------------------------------------------+ |Col#| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | +----+------+-------+-----+-----+-------+-----+------+-----+-----+-----+-----+------+-------+-------+ | 0 | ESC | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 | F10 | F11 | F12 | back | | 1 | PgUp | ~ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | - | + | ROW | 2 | PgDn | Tab | Q | W | E | R | T | Y | U | I | O | P | [ | ] | (out) | 3 | Inst | Fn | A | S | D | F | G | H | J | K | L | ; | ' | Enter | | 4 | Home | shift | Z | X | C | V | B | N | M | < | > | ? | shift | ↑ | | 5 | End | ctrl | Win | alt | space | alt | ctrl | ← | ↓ | → | \ | del | | | +----+----------------------------------------------------------------------------------------------+
매트릭스는 위와 같다. 위 키 매트릭스에 하드웨어 GPIO를 배치하면 된다. 펌웨어 동작은 이렇다. Row0 ~ Row5에 지정한 GPIO에서 신호를 순서대로 쏜다(output). 그리고 각 Row마다 Col0 ~ Col13까지 GPIO에 입력을 확인한다(input). 그래서 (R3, C5)가 입력을 인식하면 F 키에 해당하는 HID 키보드 스캔 코드인 0x9를 HID 키보드 레포트에 넣는 것이다.
하드웨어 GPIO 배정
쉽다. 위 매트릭스의 행과 열에 적당히 GPIO를 배치하면 끝이다. 컨트롤러 보드를 보고 납땜하기 편한 GPIO를 골라서 배치하면 된다.
COL (input) +--------+----------------------------------------------------------------------------------------------+ | Col# | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | +--------+------+-------+-----+-----+-------+-----+------+-----+-----+-----+-----+------+-------+-------+ |R# | | PA6 | PA7 | PB0 | PB1 | PB3 | PB4 | PB5 | PB6 | PB7 | PB8 | PB9 | PB13 | PB14 | PB15 | +---+----+------+-------+-----+-----+-------+-----+------+-----+-----+-----+-----+------+-------+-------+ | 0 |PA0 | | | 1 |PA1 | | ROW | 2 |PA2 | DEFAULT KEY MAP defined by Keymap.c | (out) | 3 |PA3 | or User custom keymap | | 4 |PA4 | | | 5 |PA5 | | +-- +---------------------------------------------------------------------------------------------------+
Row에는 GPIO Port A0부터 PortA5까지 연속으로 배치했다. 실제 컨트롤러 보드에도 연속으로 핀 아웃이 나와있다. 누군가 나와 다른 컨트롤러 보드를 쓴다해도 본인이 가진 컨트롤러 보드에 맞춰서 위 배치를 수정하면 된다. Col은 14개나 되기 때문에 GPIO Port 번호를 연속으로 쓸 수 없었다. 그래서 Port A6, A7을 연속으로 쓰고 B0, B1을 또 연속으로 쓴다. 이어서 B3 ~ B9 까지를 쭉 이어서 쓰고 B13, B14, B15를 이어서 쓴다. 이렇게 Col에 GPIO 14개를 배치했다.
펌웨어 코딩
위와 같이 설계한 것을 어떻게 코드로 옮기냐면, 그냥 그대로 코딩하면 된다. 우선 자료 구조를 설계한다. 하드웨어 GPIO 매트릭스 테이블의 구성 요소는 뭘까를 생각해보자. GPIO 포트 이름과 번호. 이렇게 딱 두 개다.
typedef struct Keypin { uint32_t port; uint32_t num; } Keypin_t;
위 Keypin_t 구조체를 배열로 만들면 배열 인덱스 번호가 행과 열 번호다.
Keypin_t sRowPin[KEYMAP_ROW_NUM] = { {(uint32_t)GPIOA, GPIO_PIN_0}, {(uint32_t)GPIOA, GPIO_PIN_1}, {(uint32_t)GPIOA, GPIO_PIN_2}, {(uint32_t)GPIOA, GPIO_PIN_3}, {(uint32_t)GPIOA, GPIO_PIN_4}, {(uint32_t)GPIOA, GPIO_PIN_5} };
Row의 실제 값을 작성한 배열이다. GPIO 포트 이름은 모두 A고 핀 번호는 0부터 5다. 배열 인덱스도 포트 번호와 같다.
Keypin_t sColPin[KEYMAP_COL_NUM] = { {(uint32_t)GPIOA, GPIO_PIN_6}, {(uint32_t)GPIOA, GPIO_PIN_7}, {(uint32_t)GPIOB, GPIO_PIN_0}, {(uint32_t)GPIOB, GPIO_PIN_1}, {(uint32_t)GPIOB, GPIO_PIN_3}, {(uint32_t)GPIOB, GPIO_PIN_4}, {(uint32_t)GPIOB, GPIO_PIN_5}, {(uint32_t)GPIOB, GPIO_PIN_6}, {(uint32_t)GPIOB, GPIO_PIN_7}, {(uint32_t)GPIOB, GPIO_PIN_8}, {(uint32_t)GPIOB, GPIO_PIN_9}, {(uint32_t)GPIOB, GPIO_PIN_13}, {(uint32_t)GPIOB, GPIO_PIN_14}, {(uint32_t)GPIOB, GPIO_PIN_15} };
위 코드는 Col의 실제 값을 작성한 배열이다. GPIO 포트 이름은 앞에서 설계한 것에 맞춰 배열 인덱스 [0, 1]은 A고 나머지 [2~13]은 B다. 핀번호도 설계한 대로 작성했다. 그래서 sColPin[8]은 Port B에 7번 핀이다.
하드웨어 키 입력 폴링
앞에서 말했듯, 키보드 키 스위치 입력을 확인하는 방법은 Row에서 순서대로 신호를 출력하고 Col에서 순서대로 신호를 확인하는 것이다. 아래는 실제 펌웨어 코드다.
uint32_t KeyHw_polling(KeyHwAddr_t* keyHwAddrBuff, uint32_t max_count) { uint32_t cnt = 0; SetAllRowToLow(); SetAllColToInput(); HAL_Delay(1); bool pressed = false; for (uint32_t row = 0 ; row < KEYMAP_ROW_NUM ; row++) { SetRowToHigh(row); HAL_Delay(1); for (uint32_t col = 0 ; col < KEYMAP_COL_NUM ; col++) { pressed = GetColInput(col); if (pressed) { if (cnt > 0) { if (keyHwAddrBuff->bit.row == (uint8_t)row && keyHwAddrBuff->bit.col == (uint8_t)col) { continue; } else { keyHwAddrBuff++; } } keyHwAddrBuff->bit.row = (uint8_t)row; keyHwAddrBuff->bit.col = (uint8_t)col; debug_printf("HW Polling (R:%u C:%u) %u\n", row, col, cnt); cnt++; if (cnt > max_count) { return cnt; } } } SetRowToLow(row); } return cnt; }
설명은 간단하다고 해놓고 코드가 좀 길다. 하지만 코드를 조금만 진지하게 보면 별거 없다는 것을 알 수 있다. 위 코드를 의사 코드로 축약하면 이렇다.
for R in sRowPin { GPIO_Out_High(R); for C in sColPin { pressed = GPIO_Get_Input(C); } }
위 코드가 핵심이고 나머지 코드는 GPIO 초기화 코드, 오버플로우 방지 코드, 폴링 결과를 저장하는 코드, 딜레이 넣어주는 코드 같은 것들이다.
이렇게 Row와 Col을 한 바퀴 돌면서 입력을 확인하면 키보드에 달린 스위치 전체를 다 검사해서 어떤 키가 눌렸는지 알 수 있다. 이 결과를 키맵 관리하는 모듈에 넘기면 HID 키보드 스캔 코드를 얻는다. 이 부분은 다음 글에서 다루겠다.
댓글 달기