← today i learned

펌웨어를 직접 굽게 될 줄은 몰랐다

남이 3년 전에 구워 둔 바이너리를 그대로 쓰던 매크로패드 펌웨어에 처음으로 직접 손을 댔다. 훅 10줄을 위해 베이스 커밋을 md5 로 특정해 고정하고, OLED 레이아웃을 다시 그리고, 전부 패치 3장의 재현 빌드로 남겼다.

DOIO KB16 · vial-qmk · 2026-07-21 · 관련: QMK 레이어는 왜 아래를 못 보나


KB16 은 키 16개에 노브 2개, 작은 OLED 가 달린 매크로패드다. 이 물건에 지금까지 한 튜닝은 적지 않다. 12레이어 키맵, 네비게이션·넘패드 레이어 재배치, RGB 제어 레이어, 키맵 치트시트 생성기까지. 그런데 그 전부가 Vial GUI 와 .vil 파일 수준의 작업이었다. 펌웨어 자체는 thompson 이라는 분이 2023년 1월에 구워 공개한 12레이어 빌드본을 그대로 썼다. 이 패드가 내 책상에 온 지는 사실 얼마 되지 않았는데, 그 위에서 도는 바이너리는 이미 3년 묵은 물건이었던 셈이다. GUI 로 되는 것까지가 암묵의 선이었고, 그 선을 넘을 생각은 딱히 없었다.

이번에 그 선을 넘었다. 발단은 사소한 욕심이었다. 키를 누르고 있는 동안 그 키의 LED 가 켜져 있으면 좋겠다는 것.

리브

관리자님이 "누르는 동안 켜져 있으면 좋겠는데"라고 하셨을 때만 해도 Vial 설정 어딘가에 있을 줄 알았습니다. 없더군요. 여기서부터 일이 커졌습니다.

스톡으로 안 된다는 것부터 확정했다

QMK RGB Matrix 의 리액티브 계열 이펙트(SOLID_REACTIVE 시리즈, SPLASH, MULTISPLASH 등)는 전부 시간 감쇠 방식이다. 소스를 읽어 보면 g_last_hit_tracker 가 최근 히트 8개의 "눌린 지 얼마나 됐나"만 들고 있고, 이펙트는 그 경과 시간으로 밝기를 계산한다. 키가 지금 눌려 있는지(matrix 상태)는 아예 참조하지 않는다. 그래서 어떤 리액티브 모드를 골라도 홀드는 탭과 똑같이 취급된다. 잠깐 빛나고, 손가락이 아직 키 위에 있어도 사그라든다.

스톡 리액티브 모드 LED 누름 아직 누르고 있는데 이미 꺼졌다 홀드 하이라이트 (패치 후) LED 누름 떼는 순간까지 유지 시간 →
같은 길이로 키를 눌러도 스톡 리액티브는 시간이 지나면 꺼진다. 눌림 상태를 보는 이펙트는 펌웨어에 하나도 없었다.

Vial GUI 가 고를 수 있는 것은 펌웨어에 컴파일돼 들어간 이 목록뿐이라, GUI 쪽에는 우회로가 없다. 결론이 확정됐다. 이 요구는 펌웨어를 다시 굽는 일이 된다.

진짜 비용은 코드가 아니라 빌드 환경이었다

구현 자체는 뒤에 나오듯 실질 10줄이다. 문제는 이 머신에 vial-qmk 체크아웃이 아예 없었다는 것. 남이 구운 바이너리를 쓰는 동안에는 필요가 없었으니까. 빌드 환경을 처음부터 세우면서 베이스를 어디에 둘지부터 갈렸다.

베이스 후보결과
최신 vial-qmk + thompson 의 kb16 폴더 빌드 실패. 3년치 API 드리프트(RGB_DI_PIN 폐기 등)로 소스가 그대로는 안 올라간다.
최신 vial-qmk 에 내장된 doio/kb16 빌드는 되지만 키보드 UID 와 기능 구성이 달라, 지금 쓰는 12레이어 .vil 프로필과 비호환. 키맵 이식이 통째로 필요하다.
thompson 빌드 시점(2023-01-08)의 vial-qmk 로 고정 채택. 호환이 통째로 보장된다.

세 번째를 고를 수 있었던 결정타는 md5 였다. thompson 저장소에 동봉된 빌드본이 실기기에서 돌고 있는 바이너리와 md5 가 정확히 일치했다. 즉 지금 기기 펌웨어의 소스와 빌드 시점이 커밋 단위로 특정된다는 뜻이다. vial-qmk 를 그 시점의 커밋으로 고정하고 thompson 의 키보드 폴더를 얹으면, 라이브 프로필·EEPROM 레이아웃·RGB 모드 번호가 전부 그대로 호환된다.

그리고 코드에 손대기 전에 무수정 빌드부터 했다. 고정한 베이스에서 아무것도 바꾸지 않고 빌드해 플래시하고, 정상 동작을 확인한 다음에야 수정을 시작했다. 이 순서를 건너뛰고 수정본을 바로 구우면, 안 될 때 내 코드 탓인지 빌드 환경 탓인지 가려낼 수 없다.

플래시 작업 자체에도 지뢰가 몇 개 있다. 이전 조사들에서 이미 밟아 본 것들이라 이번에는 규칙으로 깔고 갔다.

패치 1: 홀드 하이라이트, 실질 10줄

이펙트를 새로 만드는 대신 인디케이터 훅을 썼다. 매 프레임 배경 이펙트가 그려진 위에 덧그리는 자리라, 배경 애니메이션을 포기하지 않아도 된다.

bool rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) {
    for (uint8_t row = 0; row < MATRIX_ROWS; row++) {
        for (uint8_t col = 0; col < MATRIX_COLS; col++) {
            if (!matrix_is_on(row, col)) continue;
            uint8_t idx = g_led_config.matrix_co[row][col];
            if (idx == NO_LED || idx < led_min || idx >= led_max) continue;
            rgb_matrix_set_color(idx, 255, 255, 255);
        }
    }
    return false;
}

matrix_is_on() 이 키의 실제 눌림 상태라, 홀드 중에는 매 프레임 계속 켜지고 떼면 다음 프레임에 꺼진다. 리액티브 이펙트들이 안 보던 그 값을 보는 것이 전부다. 빌드, 플래시, 그리고 처음으로 이 패드가 내가 시킨 대로 빛났다.

치비 비율의 리브가 사무용 의자에 앉아 책상 위의 4x4 매크로패드 키 하나를 손가락으로 꾹 누르고 있다. 누른 키만 하얗게 빛나고 나머지 키들은 무지개색으로 은은하게 빛난다. 옆에는 머그컵이 놓여 있다.
누르고 있는 동안은 계속 켜져 있다. 이 한 줄을 위해 빌드 환경이 생겼다. 삽화: Codex 이미지 생성

패치 2: OLED 가 모드 번호를 말하게 한다

빌드 환경이 생기니 묵혀 둔 불만이 딸려 나왔다. 이 패드의 RGB 모드는 29개인데, 치트시트에는 오래된 주석이 하나 붙어 있었다. "장치가 현재 모드를 알려주지 않으므로, 눈앞의 보드와 대조해 자기 위치를 찾는 것이 번호가 쓸모를 갖는 유일한 방법이다." 모드를 바꿀 때마다 LED 움직임을 눈으로 맞춰 보며 29개 목록에서 현재 위치를 찾아야 했다는 뜻이다.

OLED 그리기 코드를 열어 보니 구조가 단순했다. 레이어마다 손으로 그린 128x32 전체 화면 비트맵이 12장 있고, 현재 레이어의 것을 통째로 띄우는 것이 전부다. 비트맵을 디코드해 보면 왼쪽에 큰 레이어 숫자, 오른쪽 3분의 2는 12칸 그리드에 현재 레이어 한 칸만 채워지는 그림이다. 그런데 그 12칸이 가리키는 정보는 왼쪽의 큰 숫자와 완전히 같다. 화면의 3분의 2가 중복 정보에 쓰이고 있었다.

개편 전: 화면 2/3 가 중복 정보 3 현재 레이어 12칸 중 현재 레이어 한 칸만 점등: 왼쪽과 같은 정보 개편 후: 그 자리에 RGB 모드 번호 패널이 가리는 영역 ↓ 3 28 레이어 글리프 유지 12칸은 압축 RGB 모드 번호 상시 표시
128x32 화면의 개편 전후. 손으로 그린 레이어 글리프는 살리고, 중복이던 12칸을 압축해 RGB 모드 번호 자리를 만들었다.

그래서 화면을 레이어 글리프, 압축한 12칸 그리드, RGB 모드 번호 박스의 3열로 재구성했다. 스프라이트 12장은 손으로 다시 그리는 대신 파이썬 생성기를 만들어 뽑았고, 번호는 키맵 코드가 rgb_matrix_get_mode() 값을 그때그때 그린다. 이 값이 치트시트의 모드 번호와 같은 체계라는 것도 서로 떨어진 모드 3개 이상을 실기기로 대조해 확인했다. 이제 치트시트의 그 주석은 지워졌다. 보드가 자기 모드를 말해 주므로, 번호를 보고 치트시트에서 바로 찾으면 된다.

여기서 함정을 두 개 밟았는데, 둘 다 실기기에서만 드러나는 것들이었다.

리브

시뮬레이션에서는 멀쩡했는데 실기기 화면 하단에 부팅 로고 조각이 계속 남아 있었습니다. 3년 전 드라이버의 사정까지 떠안는 것이 베이스 고정의 대가라는 걸 이렇게 배웠습니다.

패치 3: 하이라이트 색을 모드에 연동

흰색 고정이던 홀드 하이라이트는 하루 만에 아쉬워졌다. 무지개 계열 모드에서 홀드한 키만 흰색이면 겉돈다. 그래서 세 번째 패치를 얹었다. 무지개 계열 모드(CYCLE 시리즈, MULTISPLASH 등)에서는 사이클 이펙트와 같은 공식으로, 타이머를 속도 설정으로 스케일해 hue 를 연속 순환시킨다. 홀드 중인 키가 무지개색으로 흐른다. 단색 계열 모드에서는 Vial 에 설정해 둔 HSV 색을 밝기만 최대로 올려 켠다. 어느 쪽이든 하이라이트가 배경과 같은 문법으로 빛난다.

남긴 것: 클린 클론에서 같은 바이너리가 나온다

이 모든 변경은 패치 3장과 재현 빌드 스크립트로 dotfiles 저장소에 남았다. 고정된 커밋을 클론하고, 패치를 git am 으로 적용하고, 빌드하면 같은 바이너리가 나온다. 실제로 클린 클론에서 재현 빌드를 돌려 검증했는데, 산출물이 기존 검증본과 딱 6바이트 달랐다. 추적해 보니 VIA EEPROM magic 이 빌드 시각(__DATE__/__TIME__)에서 유래해 빌드마다 바뀌는 것이었다. 무해하지만, "같은 소스인가"를 바이너리로 검증하려면 그 6바이트를 감안하고 비교해야 한다는 각주가 하나 생겼다.

자잘한 함정도 하나. git format-patch 는 새 패치를 뽑을 때마다 번호를 0001 부터 다시 매긴다. 기존 스택에 이어 붙일 때 --start-number 를 빼먹으면 glob 적용 순서가 꼬여 git am 이 깨진다. 이것도 스크립트 헤더에 적어 뒀다.

예전 같으면 여기까지 오지 않았다

예전의 나에게 펌웨어는 남이 구운 것을 받아 쓰는 물건이었다. 잘못 구우면 벽돌이 되고, 빌드 환경은 세우다 지치고, 소스는 읽다 포기하는 영역. 그래서 GUI 로 되는 것까지만 하고 멈추는 것이 합리적인 선이었다.

이번에는 그 선의 안쪽 일을 전부 했다. 스톡 이펙트 소스를 읽어 왜 안 되는지를 추측이 아니라 코드로 확정했고, 실기기 바이너리의 소스 커밋을 md5 로 특정해 베이스를 고정했고, OLED 비트맵을 디코드해 화면 구조를 알아냈고, 3년 전 드라이버의 렌더링 버그를 실기기에서 잡아냈고, 결과를 패치 스택과 재현 빌드로 남겼다. 어느 단계도 혼자였으면 "여기까지 할 일인가" 하고 접었을 일이다. AI 와 함께라 조사 비용이 확 줄었고, 그래서 하나씩 다 넘어갔다.

결과물만 보면 "누르면 켜지는 키보드"다. 그런데 남이 구운 것을 받아 쓰던 관성을 끊고, 내 물건이 정확히 내가 원하는 대로 동작하는 상태를 내 손으로 만들었다는 것. 요즘 이 패드를 볼 때마다 꽤 즐겁다.

리브

이번 보고서는 결국 "키를 누르고 있으면 그 키가 계속 빛난다"는 이야기를 길게 쓴 것입니다. 그 사소한 것이 정확히 원하는 대로 움직이는 상태는, 인정하기 싫지만 꽤 기분이 좋습니다.