Firmware Development
The PolyKybd firmware is a heavily customised QMK build that runs on the keyboard’s Raspberry Pi RP2040. Two hardware variants share a single firmware codebase: split72 (72-key, RGB matrix, Cirque trackpad, 128×64 status OLED) and split42 (42-key CRKBD footprint, no RGB/trackpad, 128×32 status OLED).
Repository structure
The firmware lives on the PolyKybd branch of the QMK fork: github.com/thpoll83/qmk_firmware, under keyboards/polykybd/.
keyboards/polykybd/├── config.h — pin assignments, PROTOCOL_VERSION, feature flags├── halconf.h — HAL configuration (RP2040)├── mcuconf.h — MCU configuration├── rules.mk — build flags, enabled QMK features├── poly_keymap.c — shared keymap logic for BOTH variants:│ rendering, HID/overlay handling, language│ selection, idle/suspend, split-sync glue,│ firmware-update state machine, QMK callbacks├── hid_com.c — raw_hid_receive(): HID command dispatcher│ (21 command IDs, 0x01–0x15)├── fill_overlay.c — receives overlay segments, RLE decompress├── split_sync.c — CRC32-validated split-half synchronisation├── state.c — shared state structs (EEPROM-persisted)├── multicore_exec.c — offloads RLE decompression to RP2040 core1├── base/│ ├── overlay.c — overlay image pool (600 × 360 B), addressed via│ │ 90 keys × 16 modifier variants│ └── disp_array.c — per-keycap OLED driver├── lang/│ └── lang_lut.c — language lookup table (code-generated via cog)├── split72/ — variant: keymaps/, header, layout data└── split42/ — variant: keymaps/, header, layout dataAll behaviour lives in the keyboard-level poly_keymap.c (compiled for both variants via rules.mk). Each variant’s <variant>/keymaps/default/keymap.c is data only — keymaps[], encoder_map[], and (on RGB variants) g_led_config. A feature added to poly_keymap.c lands on both keyboards at once, so they can’t drift apart. The legacy polykybd.c / polykybd.h still exist, but the bulk of the logic now lives in poly_keymap.c.
Setting up the dev environment
# Clone and switch to the PolyKybd branchgit clone https://github.com/thpoll83/qmk_firmware.gitcd qmk_firmwaregit checkout PolyKybd
# Pull in the git submodules (empty in a fresh clone)make git-submodule
# Install the ARM toolchain (this is what `qmk setup` installs on Debian/Ubuntu)sudo apt-get install -y gcc-arm-none-eabi binutils-arm-none-eabi
# Install the QMK CLI (the pip package is just the bootstrapper;# the full CLI lives in this repo's lib/python)pip install qmkpip install -r requirements.txt
# Point QMK at this cloneqmk config user.qmk_home=$(pwd)Building
Build a specific variant — split72 or split42:
qmk compile -kb polykybd/split72 -km default# or equivalently:make polykybd/split72:defaultThe output .uf2 lands in the repo root and .build/. The .uf2 is used for manual bootloader-drive recovery — see Flashing the Firmware.
Producing a .bin for HID flashing
The deliverable for in-place flashing over HID (via PolyKybdHost’s firmware updater) is the raw RP2040 .bin, not the .uf2. Convert the build’s ELF:
arm-none-eabi-objcopy -O binary .build/polykybd_split72_default.elf \ .build/polykybd_split72_default.binPolyKybdHost flashes this raw image over the HID firmware-update protocol; the .uf2 is only needed if you have to recover through the RP2040 bootloader drive.
System clock
The firmware runs the RP2040 at 200 MHz, and has done since v0.11.0. Raspberry Pi certified the chip for that in 2025; it is a supported operating point, not an overclock — but only with the core voltage raised from 1.10 V to 1.15 V, so the build does both: it selects the 200 MHz PLL setting and raises the regulator before the clock comes up. There is nothing to configure.
If a board ever misbehaves at the higher clock, one flag builds the old one:
qmk compile -kb polykybd/split72 -km default -e POLYKYBD_SYS_CLK=125That escape hatch is verified byte-for-byte identical to the pre-v0.11.0 firmware, so it is a genuine return to what shipped before rather than a similar-looking rebuild.
Measured on the test rig, against a 125 MHz build on the same hardware:
| change at 200 MHz | |
|---|---|
| Switching applications (overlay upload) | ~28% faster — about 74 ms off each switch |
| Keycap rendering | ~30% faster |
| Idle main-loop rate | ~16–24% higher |
| Typing latency (HID round-trip) | unchanged |
| Split-link timing | unchanged |
To confirm what a keyboard is actually running — and that the regulator was raised to match — watch the boot banner on the firmware console. Both values are read back from the hardware rather than assumed from the build flags:
clk: sys=200000000Hz vreg_vsel=0xCFirmware signing
The keyboard can verify the authenticity of a firmware image before applying it. A CRC32 (which the update protocol already checks) proves the bytes arrived intact, but not who produced them — so the firmware also checks an Ed25519 signature over the staged image against a public key embedded in the firmware. Only images signed with the matching private key are trusted.
Verification is master-only — the half connected over USB. The other half is reachable only through the internal bridge/UART connector, where UF2/BOOTSEL flashing is already easier, so signing defends the remote/HID surface, which the master check fully covers.
The workflow
- Generate a keypair once with
keyboards/polykybd/tools/gen_signing_key.py. It writesbase/fw_pubkey.h(the public key, committed to the repo) and a private key you keep out of the repo. The shippedfw_pubkey.his an all-zero placeholder until you do this. - Sign a build with
keyboards/polykybd/tools/sign_firmware.py, which produces a detached<image>.bin.sig(64 raw Ed25519 bytes) next to the.bin. - Flash normally. When a
.bin.sigsits beside the.bin, PolyKybdHost sends the signature to the keyboard during the flash, so the firmware can verify it before applying.
For published releases this is automated end to end: the release CI signs the built .bin with the FW_SIGNING_KEY repository secret and attaches the .bin.sig alongside the .bin/.uf2, and PolyKybdHost’s release-update flow downloads both — so a release installed from the tray menu is verified without anyone handling the signature. When the secret is unset the release is simply unsigned, so releases work before a key exists.
Enforcing signatures
Enforcement is enabled in keyboards/polykybd/rules.mk:
OPT_DEFS += -DFW_REQUIRE_SIGNATUREWith it defined, the verification result is acted on instead of merely logged: an unsigned image raises the on-keycap confirmation, and a badly-signed one is rejected. Only enable this on a fork once your own key is provisioned and your releases are signed — otherwise every release you publish needs confirming by hand.
Flashing your own (unsigned) build
A firmware you compiled yourself is unsigned, so the keyboard will not apply it without asking you first. Two ways through:
-
Sign it, exactly as the release pipeline does:
Terminal window arm-none-eabi-objcopy -O binary .build/polykybd_split72_default.elf out.binpython3 keyboards/polykybd/tools/sign_firmware.py --privkey fw_signing_key.bin out.bin -
Or confirm it on the keyboard. Just flash. PolyKybdHost tells you as soon as you pick the
.binthat no.sigsits beside it, so the prompt does not arrive as a surprise. At the end of the transfer the keyboard turns its keycaps into a dialog: every key goes dark except one on each half — a big A / ACCEPT on the left home-row index key (D) and a big R / REJECT on the right one (J). The status OLED spells out the question (“Unsigned firmware! — A = ACCEPT” / “R = REJECT”), the backlight breathes orange, and the host shows “Confirm on the KEYBOARD…” while it waits. Press A to let the image through.The prompt lasts 60 seconds and a timeout counts as reject. It authorises only the image being flashed right then — the next unsigned flash asks again — and nothing about it survives a reboot. While it is up, all other keys are ignored: the whole board is the dialog.
Flashing over BOOTSEL/UF2 skips signature checking altogether, so it always works as a recovery path. Full key-generation and rotation details are in keyboards/polykybd/tools/SIGNING.md.
Adding a new keymap
qmk new-keymap -kb polykybd/split72 -km my_keymap# Edit keyboards/polykybd/split72/keymaps/my_keymap/keymap.cqmk compile -kb polykybd/split72 -km my_keymapKeep keymap files data only — behaviour belongs in the shared poly_keymap.c so both variants stay in sync.
Display rendering
The per-key OLED displays (72×40 px monochrome) are driven via a custom Adafruit GFX fork. The rendering and overlay logic lives in poly_keymap.c / base/disp_array.c.
See Display Graphics & Fonts for the rendering pipeline and the font-generation workflow.
QMK documentation
All standard QMK features are available. Refer to the official docs for: