So QMK is like the gold standard for custom keyboard firmware. I’ve used it on many keyboards before, and even used it to write a weird custom arpeggio keyboard. It works great and I like it and I have no complaints.1

But QMK doesn’t support the nice!nano (or possibly can’t support it, for some kind of weird licensing reason? I don’t know). So if you want Bluetooth, you have to use ZMK instead.

ZMK is no QMK.

When you’re writing QMK, you’re writing a little C program that you compile like any other code. You can write functions to send keystrokes manually, but typically you use some built-in macros to declare a simple layout. A simple QMK keymap might look like this:

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
[_QWERTY] = LAYOUT_ortho_4x12(
   KC_TAB , KC_Q   , KC_W   , KC_E   , KC_R   , KC_T   ,      KC_Y   , KC_U   , KC_I   , KC_O   , KC_P   , KC_MINS, \
   KC_ESC , KC_A   , KC_S   , KC_D   , KC_F   , KC_G   ,      KC_H   , KC_J   , KC_K   , KC_L   , KC_SCLN, KC_QUOT, \
   LOWER  , KC_Z   , KC_X   , KC_C   , KC_V   , KC_B   ,      KC_N   , KC_M   , KC_COMM, KC_DOT , KC_SLSH, RAISE  , \
   KC_LCTL, KC_LCTL, _______, KC_LALT, KC_LGUI, KC_LSFT,      KC_SPC , KC_BSPC, KC_ENT , _______, _______, _______  \

When you’re writing ZMK, you’re… declaring some kind of something. It’s not really clear… what it is, or what format it’s in, or why.

/ {
  keymap {
    compatible = "zmk,keymap";

    default_layer {
      bindings = <
  &kp TAB  &kp Q &kp W &kp E &kp R &kp T                                        &kp Y &kp U  &kp I     &kp O   &kp P    &kp MINUS
  &kp ESC  &kp A &kp S &kp D &kp F &kp G                                        &kp H &kp J  &kp K     &kp L   &kp SEMI &kp SQT
  &mo NNUM &kp Z &kp X &kp C &kp V &kp B &kp LALT  &kp LGUI   &kp RET &kp SPACE &kp N &kp M  &kp COMMA &kp DOT &kp FSLH &mo PUNC
             &kp LCTRL &kp LALT &kp LGUI &kp LSHFT &kp LCTRL  &kp RET &kp SPACE &kp BSPC     &none     &none

No idea. Truly no idea. I wrote this, but I don’t really know what I wrote.

I looked it up and apparently it’s a normal familiar thing if you’re used to writing drivers, maybe. “Device tree source” format. No idea.

Anyway, the declarativeness is sort of nice, if you’re just defining a keyboard, but annoying if you’re trying to do something more complicated. I’m not sure if it has to be declarative for like… energy efficiency? Or if this is an explicit design choice. Or something else.

I know very little about ZMK.

The only thing I really know about ZMK is that it has the worst error messages I have ever seen.

Let’s take a look at the error you get if you typo a key – possibly the easiest mistake to make, when you’re writing your first keymap.

This is a real thing that I did: I typed C_VOL_DOWN instead of C_VOL_DN. And this is the real error message that ZMK gave me:

$ west build -d build/right -b nice_nano -- -DSHIELD=kyria_right
-- Application: /Users/ian/src/zmk/app
-- Zephyr version: 2.5.0 (/Users/ian/src/zmk/zephyr)
-- Found west (found suitable version "0.12.0", minimum required is "0.7.1")
-- git describe failed: fatal: No tags can describe '68c11efba6e1eb2d689b3039553868b1b1c57f94'.
Try --always, or create some tags.;
   BUILD_VERSION is left undefined
-- Board: nice_nano, Shield(s): kyria_right
-- Cache files will be written to: /Users/ian/Library/Caches/zephyr
-- Found dtc: /nix/store/jh3yvyxhrspb9mwxwzwp0h799ds2jmn5-dtc-1.6.1/bin/dtc (found suitable version "1.6.1", minimum required is "1.4.6")
-- Found toolchain: gnuarmemb (/nix/store/l4lsff9wm46j29b69ji2r9rr4wpxdby9-gcc-arm-embedded-10.3.1)
-- Found BOARD.dts: /Users/ian/src/zmk/app/boards/arm/nice_nano/nice_nano.dts
-- Found devicetree overlay: /Users/ian/src/zmk/app/boards/shields/kyria/kyria_right.overlay
-- Found devicetree overlay: /Users/ian/src/zmk/app/boards/shields/kyria/boards/nice_nano.overlay
-- Found devicetree overlay: /Users/ian/src/zmk/app/boards/shields/kyria/kyria.keymap
Error: nice_nano.dts.pre.tmp:834.205-206 syntax error
FATAL ERROR: Unable to parse input tree
CMake Error at /Users/ian/src/zmk/zephyr/cmake/dts.cmake:205 (message):
  command failed with return code: 1
Call Stack (most recent call first):
  /Users/ian/src/zmk/zephyr/cmake/app/boilerplate.cmake:535 (include)
  /Users/ian/src/zmk/zephyr/share/zephyr-package/cmake/ZephyrConfig.cmake:24 (include)
  /Users/ian/src/zmk/zephyr/share/zephyr-package/cmake/ZephyrConfig.cmake:40 (include_boilerplate)
  CMakeLists.txt:17 (find_package)

-- Configuring incomplete, errors occurred!
See also "/Users/ian/src/zmk/app/build/right/CMakeFiles/CMakeOutput.log".
See also "/Users/ian/src/zmk/app/build/right/CMakeFiles/CMakeError.log".
FATAL ERROR: command exited with status 1: /nix/store/3lbch64fradcsks9vmxdxlhjhz2lf58s-cmake-3.21.2/bin/cmake -DWEST_PYTHON=/nix/store/5rpfiqanhg1gi1a8k4qq69mxbhz3nxnq-python3-3.8.12/bin/python3.8 -B/Users/ian/src/zmk/app/build/right -S/Users/ian/src/zmk/app -GNinja -DSHIELD=kyria_right

Note that there is absolutely nothing there about C_VOL_DOWN. Or really anything at all about what went wrong. There is a line number, if you look carefully:

Error: nice_nano.dts.pre.tmp:834.205-206 syntax error

But that’s not a file that I wrote. The line number doesn’t correspond at all to any source, but by running find . -name nice_nano.dts.pre.tmp I was able to locate some random temporary build file, and look in at, and see what I guess is a macro expansion of the actual line. It looked like this:

&none &kp ((((0x07) << 16) | (0x44))) &kp ((((0x07) << 16) | (0x45))) &kp ((((0x07) << 16) | (0x68))) &kp ((((0x07) << 16) | (0x69))) &kp ((((0x07) << 16) | (0x6A))) &kp ((((0x0C) << 16) | (0xE2))) &kp C_VOL_DOWN &kp (((((0x0C) << 16) | (0xE9)))) &kp (((((0x0C) << 16) | (0x70)))) &kp (((((0x0C) << 16) | (0x6F)))) &none

If you scroll far enough to the right there you will eventually notice that C_VOL_DOWN has not been replaced by a hex code, which was the only indication I had of my mistake.

So… not the best error messages.

But by not making any mistakes, I was able to flash it, and everything worked fine.

Something very cool about using ZMK is that you can manually switch between different Bluetooth profiles.

My keyboard is simultaneously “paired” with my laptop, my phone, and my iPad. But I can press a button to decide which one receives keystrokes. And press another button to switch between USB output and Bluetooth output, in the case that I have it plugged in to charge.

This works a lot better than the current state of the art, which appears to be some kind of distributed automata situation where all of my Bluetooth-connected devices independently conspire to be as frustrating to use as possible. At this point I’m just used to my phone stealing my headphone output because it has a notification sound to play while I’m trying to listen to music, so it’s very nice to feel like I have some kind of control over my life when I’m using my keyboard.

Some other things: you can still use the keyboard as a regular USB keyboard, by connecting the queen half to your computer – but the drone half still has to speak Bluetooth to the queen. You can’t currently connect the halves with a TRRS cable, but in theory you could convince ZMK to allow this, if you really wanted to.

One place where the declarative-only nature of the config bothered me is that I can’t set ZMK to automatically switch key layouts when I select a different Bluetooth device. I would like to use a different default layer when I’m using my computer than when I’m using my iDevices, but right now I have to manually toggle outputs and then manually toggle keymaps. It’s annoying, but definitely not as annoying as the error messages.


Okay, look. Keymaps are personal, and part of the fun of using a weird keyboard is coming up with your own keymap. My keymap won’t work for you. No one else’s keymap will work for you. But it’s nice to have a starting point that you can branch off from.

My own keymap is largely influenced by merging my first ever “portable” keyboard – the Planck EZ – with the first ever ergonomic keyboard I used – the Kinesis Advantage2. I got used to a particular thumb layout on the Advantage2, and I tried to keep as much of that as possible with Planck’s compactness. I got used to having two different layer buttons, and I still keep the Planck’s punctuation row mostly unchanged – but I’ve made quite a few changes.

Normal typing layer:

I have rebound caps lock to escape for as long as I can remember, so that’s natural to me. I don’t hit tab very much, except for cmd + tab, but that’s such a common shortcut for me that I keep it on the base layer.

Note that every modifier key is on the left thumb: this matches how I type on my Macbook keyboard, with the exception that I will sometimes press control with the knuckle of my curled pinky finger.

The “main” modifiers on my Kyria appear in the same order that they do on my Macbook keyboard, to the left of my thumb’s resting position (shift). But I also have some redundant modifier keys to the “right” of shift. This makes it easy to press weird modifier combos – cmd + ctrl requires multiple fingers on a regular keyboard, but on this keymap every modifier key is right next to each other.

The redundant modifier situation is a new thing I’m trying with the Kyria, and so far I really like it. I’ve never had so many thumb keys that I could actually reach before, and I’ve basically stopped using the leftmost ctrl and alt keys in lieu of the closer alternatives. I might just get rid of those, but what else would I put there? It’s kind of wasted space.

I still have a lot of empty space on the right thumb. Still not sure what to do about those keys.

Holding what would normally be left shift gives me my “numbers and navigation” layer:

When I first switched to a number pad on my Planck, I put 0 in the “correct” spot – under the 1 key. But that meant overwriting my backspace key, and I just hated it. It really helps me to keep space and backspace and return in the same place across all keymaps, so the pinky position is the best spot I could find for 0.

I still don’t like that the . key has to move over for this layout, but I think it’s better than moving the entire numpad over by one. (Although I’m thinking about it). But I still sometimes find myself leaving the number layer to put in a decimal point, and feeling silly about it.

I keep some redundant punctuation to make it easier to type dates and times.

Holding “right shift” gives me my “punctuation” layer:

Pretty similar to the Planck, except that I moved the brackets to the home keys. And I find putting backslash in the same place as forward slash works well with my brain.

And lastly holding both of these gives my “meta” layer:

Which is the only place that I rebind a thumb key on a layer – but only the reduntant modifiers. Hopefully I never have to press cmd + ctrl + F7

I don’t use any one-shot layers, mod-taps, or anything fancy like that.

I’m afraid of one-shot layers, because I don’t like my keyboard having state that I have to keep track of – I don’t want to accidentally press a key and get into an unexpected state; I always want to know what is going to happen as the result of a keypress.

And mod-taps seem inferior to my thumb-heavy modifier layout.

Space-cadet shift, and other dual-function keys… I’m a little bit afraid of them, I guess. I might experiment further into the firmware world one day. But I’m pretty happy with where I am right now.

  1. This is a lie. I have a complaint.

    I hate that the official QMK repository for some reason contains every single human being’s key layout and weird keyboard that you’ve never heard of. Why is this? Why do we have to merge every single keyboard into this one repository? Can’t we just maintain forks, or write a library or something? Shouldn’t there be some sort of relevancy standard here? ↩︎