August 5, 2022

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

1. Clone the ZMK repo

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)

5. Modify .keymap file

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(' ')

Output matrix transform:

// 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:

1. Add this to .conf file:

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