2026-07-11

CH552T로 넘패드 만들기

CH552T를 사용해 넘패드를 만드는 내용에 대한 게시글입니다.

EmbeddedCH552T8051

이전에 남는 키보드 스위치와 키캡으로, Thingiverse에서 찾은 넘패드 모델링을 사용해 기구부까지만 조립해 놓았다. 그러고 나서 잊고 있다가, 최근에 완성할 계획을 세우게 되며 MCU를 골라 보던 중 CH552T를 선택하게 되었다. 이번 글에서는, 처음 접해보는 MCU인 CH552T를 사용해 넘패드를 만드는 과정을 기록하려고 한다.

완성한 모습은 아래와 같다. 사용한 보드의 크기와 배선 경로를 함께 고려하면 원본 모델링의 내부 공간에 수납하기 어려워, USB 단자를 노출할 수 있도록 부득이하게 상단 외장의 일부를 잘라냈다.

상단 외장의 일부를 잘라 CH552T 보드의 USB 단자를 노출한 완성된 넘패드

CH552T 소개 및 선택 이유

CH552T는 WCH에서 제조한 8051 호환(E8051) MCU로, 저렴한 가격에 저속/전속 USB 디바이스 컨트롤러가 내장되어 있다는 특징이 있다. 이를 이용해 HID 클래스를 구현하면 키보드, 마우스, 게임패드 등의 장치로 인식시킬 수 있다. 또한 Arduino IDE에서 사용할 수 있는 유저 제작 보드 패키지인 ch55xduino가 존재하기 때문에 비교적 쉽게 개발을 시작할 수 있다.

USB HID 장치를 자작할 때는 Arduino Leonardo/Pro Micro나, 요즘에는 Raspberry Pi Pico 등을 사용하는 편이다. 하지만 넘패드 수준의 단순한 장치에는 다소 사양이 높다고 생각해 더 저렴한 대안을 찾아보았다. 먼저 검토한 것은 V-USB로, USB 하드웨어가 없는 AVR에서 GPIO와 소프트웨어만으로 저속 USB 디바이스를 구현하는 방식이다.

이미 가지고 있던 Arduino Pro Mini에 V-USB를 적용할 수도 있었지만 전압과 클럭을 함께 고려해야 했다. 5V로 구동한다면 USB 데이터 라인의 전압을 제한하기 위해 D+와 D-에 3.6V 제너 다이오드 등을 추가해야 한다. 반대로 3.3V로 구동하면 16MHz 동작은 AVR의 보장 동작 범위를 벗어나며, 일반적인 8MHz 설정으로 낮추면 V-USB가 요구하는 최소 12MHz에 미치지 못한다. 12MHz 클럭으로 변경하는 방법도 있지만 결국 추가 부품이나 보드 개조가 필요했다. 그러던 중 USB 컨트롤러가 내장된 CH552T도 상당히 저렴하다는 것을 확인하고 최종적으로 사용하기로 결정했다.

개발 환경 구축

간단한 수준의 프로젝트라 Arduino IDE를 사용하기로 했다. 앞서 링크한 ch55xduino를 참고해 초기 설정을 했다. 또한 나의 경우 부트로더 장치의 드라이버가 자동으로 설치되지 않아, WCHISPTool을 설치해서 해결했다.

CH552에는 출고 시점부터 USB ISP 부트로더가 들어 있으므로 별도의 부트로더를 먼저 기록할 필요는 없다. 다만 ch55xduino를 처음 업로드할 때는 실행 중인 펌웨어가 아직 IDE의 요청을 받아 부트로더로 전환해 줄 수 없으므로, 경우에 따라 수동으로 부트로더에 진입해야 한다. 내가 사용한 WeAct Studio CH552 Core Board는 하단의 P36 버튼을 누른 상태로 USB를 연결하면 진입할 수 있었다. 한 번 ch55xduino 펌웨어를 업로드한 뒤에는 USB 코드가 정상적으로 동작하는 한 IDE가 자동으로 부트로더 진입을 요청할 수 있다.

IDE 버전 주의!

작성 시점 기준 최신 버전인 2.3.10에서는 이 보드 패키지로 스케치를 컴파일하지 못했다. 조사해 보니 Windows 환경에서 Arduino IDE 2.3.9부터 CH552 빌드가 실패한다는 이슈가 보고되어 있었고, Arduino IDE 2.3.8을 설치하니 해결되었다.

핀 번호와 관련해

CH552T의 핀은 기본적으로 P3.2와 같은 형식으로 표기한다. 여기서 사용한 ch55xduino에서는 포트 번호 * 10 + 핀 번호 규칙에 따라 P3.2를 32번 핀으로 표기한다. 이 글에서는 코드와 설명을 일치시키기 위해 ch55xduino의 표기 방식을 따르도록 하겠다.

예제 업로드

키보드 예제로 Generic_Examples > USB > HidKeyboard를 업로드해 봤다. 기본 설정으로 컴파일하면 다음 구문에 의해 오류가 난다.

#ifndef USER_USB_RAM
#error "This example needs to be compiled with a USER USB setting"
#endif

주석을 보면 cli board options: usb_settings=user148이라는 설명이 있다. 이에 따라 도구 > USB Settings > USER CODE w/ 148B USB ram을 선택하면 된다. 기본값인 Default CDC는 코어가 USB CDC 구현을 사용하므로 사용자 코드용 USER_USB_RAM 매크로를 정의하지 않는다. 반면 148B 옵션은 사용자 USB 코드에 엔드포인트 메모리 148바이트를 예약하고 USER_USB_RAM=148을 정의하므로 이 HID 예제를 컴파일할 수 있다.

업로드했다면 CH552T가 USB 키보드로 인식되는 것을 볼 수 있다. 예제는 버튼 1~3을 각각 30/31/32번 핀에 할당해 두었는데, 아직 실제 스위치를 연결하지는 않았으니 간단하게 핀셋으로 30번 핀을 GND와 연결해 보았다.

bool button1Press = !digitalRead(BUTTON1_PIN);
if (button1PressPrev != button1Press) {
    button1PressPrev = button1Press;
    if (button1Press) {
        Keyboard_press('a');
    } else {
        Keyboard_release('a');
    }
}

위 구문에 따라 a가 잘 입력되었다.

설계

이 단계에서는 아직 실제 배선 작업은 하지 않았고, 먼저 사용 가능한 핀 수를 계산한 뒤 구성 방법을 결정했다. 넘패드에 필요한 버튼은 총 17개이다. 이 보드는 양쪽 헤더를 합쳐 20개의 접점을 제공하지만, 여기에는 VBUS(USB 5V), 3.3V, GND, RST도 포함되어 있다. 여기에 USB 데이터 라인으로 사용하는 36(P3.6/D+)과 37(P3.7/D-), 보드의 외부 클럭 회로에 연결된 12(P1.2)와 13(P1.3)을 제외하면 별도의 개조 없이 GPIO로 사용하기 편한 핀은 12개가 남는다. P1.2와 P1.3은 CH552T에서 일반 GPIO로도 사용할 수 있지만, 그러려면 보드의 외부 클럭 연결을 변경하고 내부 클럭을 사용해야 한다. 또한 이 보드의 30(P3.0)은 GPIO로 사용할 수 있지만 내장 LED에도 연결되어 있으므로 유의해야 한다. 사용할 수 있는 핀 수보다 버튼 수가 많기 때문에 여기서는 키 매트릭스를 사용하기로 했다.

핀 할당

여기서는 5행 4열 구조를 쓰도록 하겠다. 이렇게 하면 9개 핀으로 최대 20개의 버튼을 연결할 수 있으니 넘패드를 만들기에 충분하다. 사용할 핀은 다음과 같다. 행: 10(P1.0), 11(P1.1), 14(P1.4), 15(P1.5), 16(P1.6) 열: 31(P3.1), 32(P3.2), 33(P3.3), 34(P3.4)

실제 스위치는 아래와 같이 행과 열을 공유하도록 배선했다.

넘패드 스위치를 5행 4열 매트릭스로 배선한 모습

참고: 스태빌라이저는 여기서는 생략했다. 다소 흔들리기는 하지만 누르는 데 큰 지장은 없었고, 이 프로젝트의 목적은 완성도 높은 넘패드보다는 학습에 더 초점을 두고 있기 때문에 불필요하다고 판단했다.

핀이 남으므로 별도의 Num Lock LED를 추가할 수도 있지만 모델링에 공간이 없다. 여기서는 30(P3.0)에 연결된 내장 LED에 Num Lock 상태를 표시하도록 하겠다. 스위치마다 다이오드를 넣지 않은 매트릭스이므로 여러 키를 동시에 누르면 고스팅이 발생할 수 있으며, 이 구현은 N-key rollover를 목표로 하지 않는다.

코드 작성

HidKeyboard 예제를 참고해서 코드를 작성할 것이다. 그런데 예제가 사용한 라이브러리를 보면 src/userUsbHidKeyboard/USBHIDKeyboard.h임을 알 수 있는데, 이것은 예제 스케치에 내장된 라이브러리이다. 스케치->스케치 폴더 보기를 선택해, 보이는 src 폴더를 현재 작업 중인 스케치 폴더에 복사해 주면 이 라이브러리를 그대로 사용 가능하다.

전체 코드는 아래와 같다. 크게 키 매핑과 행별 배열 정의, 오픈드레인 방식의 매트릭스 스캔, 키별 디바운스와 HID 입력 처리, Num Lock LED 제어로 구성되어 있다.

코드
#ifndef USER_USB_RAM
#error "This example needs to be compiled with a USER USB setting"
#endif

#include "src/userUsbHidKeyboard/USBHIDKeyboard.h"

// Matrix pin definitions. CH55xduino pin numbers are PortNumber * 10 + PinNumber.
#define ROW1_PIN 10
#define ROW2_PIN 11
#define ROW3_PIN 14
#define ROW4_PIN 15
#define ROW5_PIN 16
#define COL1_PIN 31
#define COL2_PIN 32
#define COL3_PIN 33
#define COL4_PIN 34
#define NUMLOCK_LED_PIN 30

#define ROW_COUNT 5
#define COL_COUNT 4
#define DEBOUNCE_MS 20

// USBHIDKeyboard treats values >= 136 as raw HID usage codes.
#define HID_OFFSET 136

enum NumpadKey {
  KP_NUMLOCK = 0x53 + HID_OFFSET,
  KP_SLASH   = 0x54 + HID_OFFSET,
  KP_STAR    = 0x55 + HID_OFFSET,
  KP_MINUS   = 0x56 + HID_OFFSET,
  KP_PLUS    = 0x57 + HID_OFFSET,
  KP_ENTER   = 0x58 + HID_OFFSET,
  KP_1       = 0x59 + HID_OFFSET,
  KP_2       = 0x5A + HID_OFFSET,
  KP_3       = 0x5B + HID_OFFSET,
  KP_4       = 0x5C + HID_OFFSET,
  KP_5       = 0x5D + HID_OFFSET,
  KP_6       = 0x5E + HID_OFFSET,
  KP_7       = 0x5F + HID_OFFSET,
  KP_8       = 0x60 + HID_OFFSET,
  KP_9       = 0x61 + HID_OFFSET,
  KP_0       = 0x62 + HID_OFFSET,
  KP_DOT     = 0x63 + HID_OFFSET
};

typedef struct {
  uint8_t hidCode;
  bool rawState;
  bool stableState;
  uint32_t lastChangeTime;
} Key;

typedef struct {
  uint8_t size;
  Key *keys;
} Row;

/*
    C0  C1  C2  C3
R0  NUM /   *   -
R1  7   8   9   +
R2  4   5   6   Enter
R3  1   2   3
R4  0   .
*/

Key row0Keys[4] = {{KP_NUMLOCK, false, false, 0}, {KP_SLASH, false, false, 0}, {KP_STAR, false, false, 0}, {KP_MINUS, false, false, 0}};
Key row1Keys[4] = {{KP_7, false, false, 0}, {KP_8, false, false, 0}, {KP_9, false, false, 0}, {KP_PLUS, false, false, 0}};
Key row2Keys[4] = {{KP_4, false, false, 0}, {KP_5, false, false, 0}, {KP_6, false, false, 0}, {KP_ENTER, false, false, 0}};
Key row3Keys[3] = {{KP_1, false, false, 0}, {KP_2, false, false, 0}, {KP_3, false, false, 0}};
Key row4Keys[2] = {{KP_0, false, false, 0}, {KP_DOT, false, false, 0}};

Row rows[ROW_COUNT] = {
  {4, row0Keys},
  {4, row1Keys},
  {4, row2Keys},
  {3, row3Keys},
  {2, row4Keys}
};

const uint8_t rowPins[ROW_COUNT] = {ROW1_PIN, ROW2_PIN, ROW3_PIN, ROW4_PIN, ROW5_PIN};
const uint8_t colPins[COL_COUNT] = {COL1_PIN, COL2_PIN, COL3_PIN, COL4_PIN};

void setup() {
  USBInit();

  for (uint8_t r = 0; r < ROW_COUNT; r++) {
    pinMode(rowPins[r], OUTPUT_OD);
    digitalWrite(rowPins[r], HIGH); // release inactive rows
  }

  for (uint8_t c = 0; c < COL_COUNT; c++) {
    pinMode(colPins[c], INPUT_PULLUP); // HIGH when not pressed
  }

  pinMode(NUMLOCK_LED_PIN, OUTPUT);
}

void loop() {
  uint32_t currentTime = millis();

  for (uint8_t r = 0; r < ROW_COUNT; r++) {
    digitalWrite(rowPins[r], LOW); // activate current row

    for (uint8_t c = 0; c < rows[r].size; c++) {
      Key *key = &rows[r].keys[c];
      bool pressed = (digitalRead(colPins[c]) == LOW);

      if (pressed != key->rawState) {
        key->rawState = pressed;
        key->lastChangeTime = currentTime;
      } else if (key->stableState != key->rawState &&
                 currentTime - key->lastChangeTime >= DEBOUNCE_MS) {
        key->stableState = key->rawState;

        if (key->stableState) {
          Keyboard_press(key->hidCode);
        } else {
          Keyboard_release(key->hidCode);
        }
      }
    }

    digitalWrite(rowPins[r], HIGH); // release current row
  }

  // Map Num Lock to LED.
  // Bit 0: Num Lock, Bit 1: Caps Lock, Bit 2: Scroll Lock,
  // Bit 3: Compose, Bit 4: Kana
  if (Keyboard_getLEDStatus() & 0x01) {
    digitalWrite(NUMLOCK_LED_PIN, HIGH);
  } else {
    digitalWrite(NUMLOCK_LED_PIN, LOW);
  }
}

후기

이번 넘패드처럼 간단한 것을 제작하기에는 실제로 무난해 보였고, 앞으로도 간단한 USB 장치를 제작해야 한다면 ATmega32U4나 RP2040 기반 보드 대신 채택할 후보에 넣어도 될 것 같았다. 다만 이번에는 ch55xduino가 제공하는 API를 사용해 추상화된 내용만 다뤘기 때문에, 아직 이 MCU를 깊이 다뤄 봤다고 하기는 어려웠다. 기회가 된다면 다음에는 다른 예제를 둘러보고, SDCC 환경에서 레지스터를 직접 다루는 방식도 살펴보고 싶다.

한편 CH552T는 USB 디바이스 컨트롤러를 내장한 초저가 MCU라는 점에서 흥미로웠다. 이것을 계기로 평소에 다뤄 볼 기회가 없었던 같은 WCH의 RISC-V 기반 CH32V003이나, 과거 조립했던 전자키트에서 보았던 STC 계열 8051 MCU에도 도전해 볼 계획이다. 초저가 MCU로 알려진 Padauk 계열도 언젠가 다뤄 보고 싶은 최종 목표로 삼고 있다.