← How-Tos
electronics 1 hr ago ◯ 7 min read

ZMK Firmware for Wireless Split Keyboards: Nice!Nano, BLE, and Config-as-Code

zmkwireless keyboardsplit keyboardnice!nanoblebluetooth low energynrf52840config as codekeymapzephyr

If you've built a hand-wired mechanical keyboard with QMK, you already know the drill: a wired controller, an Arduino-style IDE flow or QMK MSYS build, and a USB cable tethering you to the desk. ZMK (Zephyr Mechanical Keyboard firmware) exists for a different goal entirely — wireless, battery-powered split keyboards that sip power for weeks between charges. It runs on the Zephyr RTOS rather than QMK's own kernel, targets nRF52840-based controllers like the nice!nano and Seeed XIAO BLE almost exclusively, and treats your keymap as a text file in a Git repository that a cloud build server compiles for you. None of that is obvious if you're coming from QMK, so this guide covers the parts that actually trip people up: picking hardware that ZMK supports well, setting up the config-as-code workflow, getting a split pair to bond reliably over Bluetooth Low Energy, and tuning power settings so the thing lasts more than a day.

Why ZMK Instead of QMK for a Wireless Build

QMK can technically do Bluetooth on a handful of boards, but it's not where the project's engineering attention goes — BLE support is bolted on, power management is an afterthought, and documentation assumes wired USB HID as the default. ZMK was built around Bluetooth from day one. It handles split keyboard pairing, deep sleep between keystrokes, and battery level reporting to your host OS as core, well-tested features rather than community patches. The trade-off is a smaller board ecosystem (effectively nRF52-series microcontrollers only) and a steeper initial setup, since you're working with the Zephyr build system instead of a single `qmk compile` command.

Hardware: What You Actually Need

Most ZMK builds center on the nice!nano v2, a small nRF52840 module shaped to drop into the same footprint as a Pro Micro, which makes it compatible with most existing split keyboard PCBs designed for QMK controllers. You'll also want a nice!nano-compatible battery connector (JST-PH 1.25mm, 2-pin) and a 3.7V LiPo cell sized to fit your case — 110mAh to 400mAh pouch cells are typical for keyboard halves. Diodes for your switch matrix are the same 1N4148-style parts you'd use with any keyboard controller; ZMK doesn't change the matrix wiring itself, only what reads it. If you're starting from bare PCBs and switches rather than a pre-wired kit, the general hand-wiring and matrix-diode process is the same one covered in this site's guide to building a custom mechanical keyboard from scratch — ZMK is a drop-in replacement for the firmware step, not the soldering.

Setting Up Your ZMK Config Repository

ZMK's documentation calls this "config as code," and in practice it means your entire keyboard configuration lives in a small GitHub repository, not on your machine. The standard path is to use ZMK's config template repository (generated through their setup script or by forking the template), which gives you a `config/` folder containing a `.keymap` file and a `.conf` file, plus a GitHub Actions workflow already wired up to build firmware automatically on every push. You never install the Zephyr SDK locally unless you want to — commit a change to your keymap, and GitHub's runners produce a `.uf2` firmware file as a build artifact a few minutes later. This is the single biggest workflow difference from QMK and the thing that confuses people coming from a local-compile mental model: there is no "flash button" in an IDE, just git push and a wait.

Keymap and Devicetree: Reading the Config Files

The `.keymap` file uses devicetree syntax, which looks unfamiliar if you've only seen QMK's C-array keymaps, but maps onto the same concepts: layers, key positions, and behaviors (ZMK's term for what QMK calls keycodes and custom functions). A basic layer binding looks like a grid of `&kp` (key press) bindings, and layer-switching uses `&mo` (momentary) or `&to` (to-layer) behaviors in the same positions. Combos, mod-taps, and tap-dances all exist in ZMK under slightly different names and syntax — mod-morph and hold-tap being the two you'll reach for most. The `.conf` file is where Kconfig options live: `CONFIG_ZMK_SLEEP=y` enables deep sleep, `CONFIG_ZMK_BLE=y` is normally on by default, and `CONFIG_ZMK_BATTERY_REPORTING=y` turns on the battery percentage your OS will show for the keyboard.

Split Keyboard Pairing and the Central/Peripheral Relationship

In a ZMK split build, one half is the "central" and talks to your computer over BLE HID; the other is a "peripheral" that only talks to the central. This isn't symmetric, and it matters for troubleshooting: if your left half won't connect to your laptop but the right half works fine as a standalone board, you've likely got the central/peripheral role backwards in your build configuration or shield definition. Pairing is a one-time Bluetooth bond per host, held in the keyboard's own memory (not your OS's), and a fresh pair is established by holding the on-board reset or using the bootloader-adjacent "clear bonds" key combo if your keymap defines one — most config templates include a `&bt BT_CLR` binding on a function layer specifically for this. Expect the halves to re-sync within a second or two of waking from sleep; a longer stall usually points to a weak battery or a peripheral that lost its bond to the central and needs re-pairing.

Power Management and Realistic Battery Life

ZMK's deep sleep kicks in after a configurable idle period (`CONFIG_ZMK_IDLE_SLEEP_TIMEOUT`, in milliseconds) and drops current draw from a few milliamps down to low microamps. With sleep properly configured, a 300mAh cell in daily office use commonly runs one to three weeks between charges; leaving sleep disabled, or using a keyboard with per-key RGB left constantly on, can cut that to a day or two. Underglow and per-key LEDs are supported but are the single biggest power draw on a ZMK board by a wide margin — if you built in RGB, budget for it being off most of the time or for charging far more often than the "two weeks" figure you'll see quoted for bare boards.

AspectQMKZMK Primary connectionUSB wired (BLE on limited boards)Bluetooth LE native, USB also supported on most boards Supported MCUsAVR, broad ARM range (RP2040, STM32, etc.)Primarily nRF52-series (nice!nano, XIAO BLE, Seeeduino) Build methodLocal toolchain or QMK MSYS / qmk compileGitHub Actions cloud build from a config repo (or local Zephyr/west build) Keymap formatC source / keymap.c layout gridDevicetree .keymap syntax Power managementMinimal, community-addedCore feature: deep sleep, battery reporting Split keyboard syncWired serial or add-on BLE moduleNative BLE central/peripheral roles

Flashing: UF2 Bootloader and OTA Updates

Getting firmware onto the board for the first time uses the same UF2 drag-and-drop method as most RP2040 boards: double-tap reset to drop into bootloader mode, the board mounts as a USB mass-storage device, and you copy the `.uf2` file from your Actions build artifact onto it. After that first flash, ZMK Studio (a newer companion app) and some community tooling support updating keymaps without re-flashing firmware at all, though full firmware updates for behavior or Kconfig changes still go through the UF2 process. Keep a known-good firmware file saved locally — recovering a board that won't enter bootloader mode because of a bad power config is a real failure mode, and a second reset-button press timed differently, or a paperclip short across the dedicated reset pads some boards expose, is usually the fix.

Battery Safety in a Keyboard Case

A LiPo pouch cell living inside a keyboard case for months gets less attention than one in an RC battery or a power bank, but the same rules apply: use a cell with its own protection circuit or a controller board (like the nice!nano) that includes charge and discharge protection, route the battery away from sharp case edges and screw bosses that could puncture it over time, and never charge an unattended board overnight on a surface that can't tolerate a fire. If your case design clamps the battery against the PCB, add a thin foam or silicone spacer rather than letting standoffs press directly into the pouch.

ZMK's learning curve is real, but it's front-loaded — once your config repository is set up and your first pair of halves bond reliably, day-to-day use is simpler than QMK in the ways that matter most for a wireless board: no cable, predictable sleep behavior, and a battery icon in your OS that actually works. Start from an existing keyboard's community shield definition if one exists for your PCB rather than writing a devicetree from scratch, and save the deep Kconfig tuning for after you've confirmed the basics work.