ZMK setup and new board & shield definition
“I just wanna steal your code” — here you go.
This is a dump of the steps I took to build a new board definition and shield for my new keyboard in ZMK. This list explicitly omits all the details and only includes a general overview but these are all the steps I took. I'm using macOS with zsh in terminal.
Terminal basics
1. To go to a folder in Terminal, use the cd
(change directory) command (the list includes some “run command in this folder” steps)
cd path/to/zmk
2. Drag & drop the folder from Finder to insert its path at current cursor position. To quickly place the cursor at any place in command, hold Option and click.
3. To exit Vim, press Esc, type :wq
and press Enter.
ZMK Setup
2. Run setup using homebrew and install west https://zmk.dev/docs/development/setup, install Zephyr SDK
brew install cmake ninja gperf python3 ccache qemu dtc wget libmagic pip3 install -U west
3. Add export PATH=$HOME/Library/Python/3.10/bin:$PATH
to ~/.zshrc
to be able to run west
4. In terminal go to ZMK repo folder and run west init -l app/
5. west update; west zephyr-export; pip3 install --user -r zephyr/scripts/requirements-base.txt
6. source zephyr/zephyr-env.sh
— needs to be done in every terminal session to build
7. Download toolchain, put it in a permanent place (I put it in ~/Libraries
)
https://docs.zephyrproject.org/3.2.0/develop/getting_started/index.html#install-zephyr-sdk
8. Add export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
to ~/.zshrc
9. Done, now one can build: cd app
and for example west build -b planck_rev6
New board definition
Creating a new board definition (RP2040 Zero) for ZMK
1. Copy a sample board to app/boards (RP2040 Xiao)
2. Change the name of all files to new board name
3. Change the board name in all files
4. Set memory in .dts
and .yaml
files
5. Enable usb in Kconfig.defconfig
for wired connection:
config ZMK_USB default y
6. Add board to app/core-coverage.yml
(not sure if necessary)
7. In terminal to app
directory: cd zmk/app
8. Build a sample using west build -b YOURBOARD_NAME — -DSHIELD=kyria_left
(with your new board name)
9. It should fail if your board is incompatible (check both warnings and errors to find the cause but a failure is normal)
10. Always remove build directory before building (there are better methods but I haven't figured them out yet): rm -rf build; west build ...
11. Increasing the main/workqueue stack size: it was necessary for RP2040 because it was getting stack overflow on layer tap-hold. I added this in Kconfig.shield
config SYSTEM_WORKQUEUE_STACK_SIZE int "work queue stack size" default 4096 config CONFIG_MAIN_STACK_SIZE int "main stack size" default 2048
Creating a test shield
1. Create zmk-config/config
folder. I copied Pete's, it contains a single-key shield called uno (no matrix, good for testing on a breadboard) https://github.com/petejohanson/zmk-config/
2. In config/boards/shields
copy an existing keyboard shield (I copied uno)
3. If necessary, rename all files and all strings in them to your test shield name (mine is test_zero
)
4. Modify .overlay
file to set your board's pins (named gpio0
)
6. Go to the app/
directory in terminal and build using the following command. Specify your absolute config
(the one inside zmk-config
) folder path (replace text HERE
), board and shield name.
rm -rf build; west build -p auto -b waveshare_rp2040_zero \ -- -DZMK_CONFIG="HERE" -DSHIELD=sexy_acrylic -d build/test_zero
💡 Tip: drag & drop the folder from Finder when your cursor is between two quotes to automatically enter the path. To quickly place the cursor between the quotes hold Option and click. Remove the word HERE
from path in advance. The path needs to be absolute, ~/
shortcut for home is not allowed. The option should look like this:
-DZMK_CONFIG="/Users/YOURNAME/PATH/TO/zmk-config/config"
Bonus: encoders & porting from KMK
The whole idea of trying ZMK was inspired by the fact that KMK is very slow and I was tired of trying to rewrite the core to make it faster. So with the help of Pete I got ZMK working instead.
Matrix transform
To port the matrix I needed to transform an 1-dimensional array of key positions into a two-dimensional one (rows × columns). It took me several attempts and a good understanding of the underlying arithmetics to make this work. I really needed this because I already mapped the matrix in KMK with its very simple way of mapping: pressing each key outputs the number that corresponds to its place so you just need to press keys sequentially. ZMK, take notes! 📝
input = [ 4, 0, 1, 2, 3, 38, 37, 36, 35, 39, 9, 5, 6, 7, 8, 33, 32, 31, 30, 34, 14, 10, 11, 12, 13, 28, 27, 26, 25, 29, 19, 15, 17, 16, 18, 23, 21, 22, 20, 24, ] colNum = 5 input.map((n, i) => 'RC(' + (n - (n % colNum)) / colNum + ',' + n % colNum + ')').join(' ')
// RC = Row/Column RC(0,4) RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(7,3) RC(7,2) RC(7,1) RC(7,0) RC(7,4) RC(1,4) RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(6,3) RC(6,2) RC(6,1) RC(6,0) RC(6,4) RC(2,4) RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(5,3) RC(5,2) RC(5,1) RC(5,0) RC(5,4) RC(3,4) RC(3,0) RC(3,2) RC(3,1) RC(3,3) RC(4,3) RC(4,1) RC(4,2) RC(4,0) RC(4,4)
Keymap and combos
To port my keymap I used basic multi-select commands in Sublime Text. To port combos I used a bit of JS that I lost except for this piece:
inp.split(',').map(n => { let kk = n.trim() // key code // repl is an array like [`KP.Q`, `KP.W`...] let index = repl.indexOf(kk) if (index >= 0) { // if string is a keycode, replace it with index of that keycode return index + '' } return n }).join(',')
Encoders
Fortunately, ZMK supports multiple encoders sharing pins between each other — which is a great way to save pins in multi-encoder builds. Here is the process of adding encoders to your ZMK keyboard:
CONFIG_EC11=y CONFIG_EC11_TRIGGER_GLOBAL_THREAD=y
2. Add sensor bindings in keymap (in each layer) like sensor-bindings = <&inc_dec_kp UP DOWN>
3. In .dtsi
files (for split) or .overlay
file (unibody) add the following for each encoder (change LEFT_ENCODER
to any name). Pins CAN BE SHARED!!
left_encoder: encoder_left { compatible = "alps,ec11"; label = "LEFT_ENCODER"; a-gpios = <PIN_A (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; b-gpios = <PIN_B (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>; resolution = <4>; };
4. Add sensors definition with all encoders below (still in .overlay)
sensors { compatible = "zmk,keymap-sensors"; sensors = <&left_encoder &right_encoder>; };
5. Add encoders status and RESOLUTION to .keymap.
&left_encoder { status = "okay"; }; &right_encoder { status = "okay"; }; &bottom_encoder { status = "okay"; resolution = <2>; };
Note that technically it's impossible to have encoders of higher resolution (2 instead of 4 (yes, these are backwards)) sharing pins. It's not the limitation of ZMK only, it's how these encoders work.
Results
Waveshare RP2040 Zero board definition: https://github.com/zyumbik/zmk/tree/zero-update/app/boards/arm/waveshare_rp2040_zero
My test shield: https://github.com/zyumbik/zmk-config/tree/main/config/boards/shields/test_zero
My actual keyboard shield: https://github.com/zyumbik/zmk-config/tree/main/config/boards/shields/sexy_acrylic