Skip to main content

New Keyboard Shield

Overview

This guide will walk through the steps necessary to add ZMK support for a keyboard that uses an add-on MCU board (e.g. Pro Micro compatible) to provide the microprocessor.

The high level steps are:

  • From a template, create a new Zephyr module housed in a git repository containing one or more custom shields.
  • Create a new shield directory.
  • Add the base Kconfig files.
  • Add the shield overlay file to define the KSCAN driver for detecting key press/release.
  • (Optional) Add the matrix transform for mapping KSCAN row/column values to sane key positions. This is needed for non-rectangular keyboards, or where the underlying row/column pin arrangement does not map one to one with logical locations on the keyboard.
  • Add a default keymap, which users can override in their own configs as needed.
  • Add a <my_shield>.zmk.yml metadata file to document the high level details of your shield, and the features it supports.
  • Update the build.yaml file from the repository template to have some sample builds of the firmware to test.
  • Add support for features such as encoders, OLED displays, or RGB underglow.

It may be helpful to review the upstream shields documentation to get a proper understanding of the underlying system before continuing.

note

ZMK support for split keyboards requires a few more files than single boards to ensure proper connectivity between the central and peripheral units. Check the following guides thoroughly to ensure that all the files are in place.

New Zephyr Module Repository

The first step to creating the shield is to create a new Zephyr module repository from a template.

note

This guide assumes you already have a configured GitHub account. If you don't yet have one, go ahead and sign up before continuing.

Follow these steps to create your new repository:

  • Visit https://github.com/zmkfirmware/unified-zmk-config-template
  • Click the green "Use this template" button
  • In the drop down that opens, click "Use this template".
  • In the following screen, provide the following information:
    • A repository name, e.g. my-shield-module.
    • A brief description, e.g. ZMK Support For MyShield Keyboard.
    • Select Public or Private, depending on your preference.
  • Click the green "Create repository" button

New Shield Directory

note

This guide describes how to add a shield to an independently managed Zephyr module repository. This is the preferred way to handle boards and shields moving forward in ZMK, although the tooling to make this easier for users is still improving. ZMK does have a collection of boards/shields in the ZMK main repository, which are planned to be phased out, but until that is complete, there may be a few select scenarios where adding your keyboard to ZMK itself is preferred. Due the volume of PRs and the focus of ZMK development not being merging of keyboard PRs, you are highly encouraged to use an out-of-tree Zephyr module repository to manage your definitions. Should you choose to try to get your keyboard included in ZMK main repository, the paths in the rest of the guide would be nested under the app/ folder there instead. For example, boards/shields/<keyboard_name> should now be app/boards/shields/<keyboard_name>.

Shields in Zephyr module "board root" go into the boards/shields/ directory; that means the new shield directory in your module repository should be:

mkdir boards/shields/<keyboard_name>

Base Kconfig Files

Example shields

You can check out the shields folder in the ZMK repo that houses the in-tree supported shields in order to copy and modify as a starting point.

There are two required Kconfig files that need to be created for your new keyboard shield to get it picked up for ZMK, Kconfig.shield and Kconfig.defconfig.

Kconfig.shield

The Kconfig.shield file defines any additional Kconfig settings that may be relevant when using this keyboard. For most keyboards, there is just one additional configuration value for the shield itself.

config SHIELD_MY_BOARD
def_bool $(shields_list_contains,my_board)
warning

Kconfig uses only commas for delimiters, and keeps all whitespaces in the function call. Therefore do not add a whitespace after the comma when configuring your shield as this would be treated as  my_board (with a leading whitespace) and will cause issues.

This will make sure that a new configuration value named SHIELD_MY_BOARD is set to true whenever my_board is used as the shield name, either as the SHIELD variable in a local build or in your build.yaml file when using Github Actions. Note that this configuration value will be used in Kconfig.defconfig to set other properties about your shield, so make sure that they match.

For split boards, you will need to add configurations for the left and right sides. For example, if your split halves are named my_board_left and my_board_right, it would look like this:

config SHIELD_MY_BOARD_LEFT
def_bool $(shields_list_contains,my_board_left)

config SHIELD_MY_BOARD_RIGHT
def_bool $(shields_list_contains,my_board_right)

Kconfig.defconfig

The Kconfig.defconfig file is where overrides for various configuration settings that make sense to have different defaults when this shield is used. One main item that usually has a new default value set here is the ZMK_KEYBOARD_NAME value, which controls the display name of the device over USB and BLE.

The updated new default values should always be wrapped inside a conditional on the shield config name defined in the Kconfig.shield file. Here's the simplest example file.

danger

The keyboard name must be less than or equal to 16 characters in length, otherwise the bluetooth advertising might fail and you will not be able to find your keyboard from your device.

if SHIELD_MY_BOARD

config ZMK_KEYBOARD_NAME
default "My Board"

endif

For split keyboards, Kconfig.defconfig needs to specify a few more options. Which side is central (usually the left) is determined via the configuration in this file. For that side, the keyboard name is assigned and the central config is set. The peripheral side is typically not assigned a name since only the central will be advertising for connections to other devices. Finally, the split config needs to be set for both sides:

if SHIELD_MY_BOARD_LEFT

config ZMK_KEYBOARD_NAME
default "My Board"

config ZMK_SPLIT_ROLE_CENTRAL
default y

endif

if SHIELD_MY_BOARD_LEFT || SHIELD_MY_BOARD_RIGHT

config ZMK_SPLIT
default y

endif

Shield Overlays

ZMK uses the blue color coded "Arduino" pin names to generate devicetree node references. For example, to refer to the pin labeled 0 in the diagram, use &pro_micro 0 in the devicetree files.

The following node labels are available:
  • GPIO: &pro_micro
  • I2C bus: &pro_micro_i2c
  • SPI bus: &pro_micro_spi
  • UART: &pro_micro_serial

To use GPIO pins that are not part of the interconnects as described above, you can use the GPIO labels that are specific to each controller type. For instance, pins numbered PX.Y in nRF52840-based boards can be referred to via &gpioX Y labels. An example is &gpio1 7 for the P1.07 pin that the nice!nano exposes in the middle of the board.

The <shield_name>.overlay is the devicetree description of the keyboard shield that is merged with the primary board devicetree description before the build. For ZMK, this file at a minimum should include the chosen node named zmk,kscan that references a KSCAN driver instance. For a simple 3x3 macropad matrix, this might look something like:

/ {
chosen {
zmk,kscan = &kscan0;
};

kscan0: kscan_0 {
compatible = "zmk,kscan-gpio-matrix";
diode-direction = "col2row";
wakeup-source;

col-gpios
= <&pro_micro 15 GPIO_ACTIVE_HIGH>
, <&pro_micro 14 GPIO_ACTIVE_HIGH>
, <&pro_micro 16 GPIO_ACTIVE_HIGH>
;

row-gpios
= <&pro_micro 19 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>
, <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>
, <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>
;
};
};

See the Keyboard Scan configuration documentation for details on configuring the KSCAN driver.

(Optional) Matrix Transform

Internally ZMK translates all row/column events into "key position" events to maintain a consistent model that works no matter what any possible GPIO matrix may look like for a certain keyboard. This is particularly helpful when:

  1. To reduce the used pins, an "efficient" number of rows/columns for the GPIO matrix is used, that does not match the physical layout of rows/columns of the actual key switches.
  2. For non rectangular keyboards with thumb clusters, non 1u locations, etc.

A "key position" is the numeric index (zero-based) of a given key, which identifies the logical key location as perceived by the end user. All keymap mappings actually bind behaviors to key positions, not to row/column values.

Without a matrix transform, that intentionally map each key position to the row/column pair that position corresponds to, the default equation to determine that is:

($row * NUMBER_OF_COLUMNS) + $column

Which effectively amounts to numbering the key positions by traversing each row from top to bottom and assigning numerically incrementing key positions.

Whenever that default key position mapping is insufficient, the <shield_name>.overlay file should also include a matrix transform.

Here is an example for the nice60, which uses an efficient 8x8 GPIO matrix, and uses a transform:

#include <dt-bindings/zmk/matrix_transform.h>

/ {
chosen {
zmk,kscan = &kscan0;
zmk,matrix-transform = &default_transform;
};

/* define kscan node with label `kscan0`... */

default_transform: keymap_transform_0 {
compatible = "zmk,matrix-transform";
columns = <8>;
rows = <8>;
// | MX1 | MX2 | MX3 | MX4 | MX5 | MX6 | MX7 | MX8 | MX9 | MX10 | MX11 | MX12 | MX13 | MX14 |
// | MX15 | MX16 | MX17 | MX18 | MX19 | MX20 | MX21 | MX22 | MX23 | MX34 | MX25 | MX26 | MX27 | MX28 |
// | MX29 | MX30 | MX31 | MX32 | MX33 | MX34 | MX35 | MX36 | MX37 | MX38 | MX39 | MX40 | MX41 |
// | MX42 | MX43 | MX44 | MX45 | MX46 | MX47 | MX48 | MX49 | MX50 | MX51 | MX52 | MX53 |
// | MX54 | MX55 | MX56 | MX57 | MX58 | MX59 | MX60 | MX61 |
map = <
RC(3,0) RC(2,0) RC(1,0) RC(0,0) RC(1,1) RC(0,1) RC(0,2) RC(1,3) RC(0,3) RC(1,4) RC(0,4) RC(0,5) RC(1,6) RC(1,7)
RC(4,0) RC(4,1) RC(3,1) RC(2,1) RC(2,2) RC(1,2) RC(2,3) RC(3,4) RC(2,4) RC(2,5) RC(1,5) RC(2,6) RC(2,7) RC(3,7)
RC(5,0) RC(5,1) RC(5,2) RC(4,2) RC(3,2) RC(4,3) RC(3,3) RC(4,4) RC(4,5) RC(3,5) RC(4,6) RC(3,6) RC(4,7)
RC(6,0) RC(6,1) RC(6,2) RC(6,3) RC(5,3) RC(6,4) RC(5,4) RC(6,5) RC(5,5) RC(6,6) RC(5,6) RC(5,7)
RC(7,0) RC(7,1) RC(7,2) RC(7,3) RC(7,5) RC(7,6) RC(6,7) RC(7,7)
>;
};

/* potentially other overlay nodes... */
};

Some important things to note:

  • The #include <dt-bindings/zmk/matrix_transform.h> is critical. The RC macro is used to generate the internal storage in the matrix transform, and is actually replaced by a C preprocessor before the final devicetree is compiled into ZMK.
  • RC(row, column) is placed sequentially to define what row and column values that position corresponds to.
  • If you have a keyboard with options for 2u keys in certain positions, or break away portions, it is a good idea to set the chosen zmk,matrix-transform to the default arrangement, and include other possible matrix transform nodes in the devicetree that users can select in their user config by overriding the chosen node.

See the matrix transform section in the Keyboard Scan configuration documentation for details and more examples of matrix transforms.

Default Keymap

Each keyboard should provide a default keymap to be used when building the firmware, which can be overridden and customized by user configs. For "shield keyboards", this should be placed in the boards/shields/<shield_name>/<shield_name>.keymap file. The keymap is configured as an additional devicetree overlay that includes the following:

  • A node with compatible = "zmk,keymap" where each child node is a layer with a bindings array that binds each key position to a given behavior (e.g. key press, momentary layer, etc).

Here is an example simple keymap for the Kyria, with only one layer:

#include <behaviors.dtsi>
#include <dt-bindings/zmk/keys.h>

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

default_layer {
// --------------------------------------------------------------------------------------------------------------------------------------------------------------------
// | ESC | Q | W | E | R | T | | Y | U | I | O | P | \ |
// | TAB | A | S | D | F | G | | H | J | K | L | ; | ' |
// | SHIFT | Z | X | C | V | B | CTRL+A | CTRL+C | | CTRL+V | CTRL+X | N | M | , | . | / | R CTRL |
// | GUI | DEL | RETURN | SPACE | ESCAPE | | RETURN | SPACE | TAB | BSPC | R ALT |
bindings = <
&kp ESC &kp Q &kp W &kp E &kp R &kp T &kp Y &kp U &kp I &kp O &kp P &kp BSLH
&kp TAB &kp A &kp S &kp D &kp F &kp G &kp H &kp J &kp K &kp L &kp SEMI &kp SQT
&kp LSHIFT &kp Z &kp X &kp C &kp V &kp B &kp LC(A) &kp LC(C) &kp LC(V) &kp LC(X) &kp N &kp M &kp COMMA &kp DOT &kp FSLH &kp RCTRL
&kp LGUI &kp DEL &kp RET &kp SPACE &kp ESC &kp RET &kp SPACE &kp TAB &kp BSPC &kp RALT
>;

sensor-bindings = <&inc_dec_kp C_VOL_UP C_VOL_DN &inc_dec_kp PG_UP PG_DN>;
};
};
};
note

The two #include lines at the top of the keymap are required in order to bring in the default set of behaviors to make them available to bind, and to import a set of defines for the key codes, so keymaps can use parameters like N2 or K instead of the raw keycode numeric values.

Keymap Behaviors

For documentation on the available behaviors for use in keymaps, see the overview page for behaviors.

Metadata

ZMK makes use of an additional metadata YAML file for all boards and shields to provide high level information about the hardware to be incorporated into setup scripts/utilities, website hardware list, etc.

The naming convention for metadata files is {item_id}.zmk.yml, where the item_id is the board/shield identifier, including version information but excluding any optional split _left/_right suffix, e.g. corne.zmk.yml or nrfmicro_11.zmk.yml.

Here is a sample corne.zmk.yml file from the repository:

file_format: "1"
id: corne
name: Corne
type: shield
url: https://github.com/foostan/crkbd/
requires: [pro_micro]
exposes: [i2c_oled]
features:
- keys
- display
siblings:
- corne_left
- corne_right

You should place a properly named foo.zmk.yml file in the directory next to your other shield values, and fill it out completely and accurately. See Hardware Metadata Files for the full details.

Build File

To help you test/verify your firmware, update the build.yaml to list your particular board/shield combinations you want built whenever changes are published to GitHub. Open build.yaml with your editor and add a combination, e.g.:

# This file generates the GitHub Actions matrix
# For simple board + shield combinations, add them
# to the top level board and shield arrays, for more
# control, add individual board + shield combinations to
# the `include` property, e.g:
#
# board: [ "nice_nano_v2" ]
# shield: [ "corne_left", "corne_right" ]
# include:
# - board: bdn9_rev2
# - board: nice_nano_v2
# shield: reviung41
#
---
include:
- board: nice_nano_v2
shield: <my_shield>

For split keyboards, you will need to specify the halves/siblings separately, e.g.:

include:
- board: mikoto_520
shield: <my_shield>_left
- board: mikoto_520
shield: <my_shield>_right

Adding Features

Encoders

EC11 encoder support can be added to your board or shield by adding the appropriate lines to your board/shield's configuration (.conf), device tree (.dtsi), overlay (.overlay), and keymap (.keymap) files.

In your configuration file you will need to add the following lines so that the encoders can be enabled/disabled:

# Uncomment to enable encoder
# CONFIG_EC11=y
# CONFIG_EC11_TRIGGER_GLOBAL_THREAD=y

These should be commented by default for encoders that are optional/can be swapped with switches, but can be uncommented if encoders are part of the default design.

note

If building locally for split boards, you may need to add these lines to the specific half's configuration file as well as the combined configuration file.

Testing

GitHub Actions

Using GitHub Actions to build your new firmware can save you from doing any local development setup, at the expense of a longer feedback loop if there are issues. To push your changes and trigger a build:

  • Add all your pending changes with git add .
  • Commit your changes with git commit -m "Initial shield"
  • Push the changes to GitHub with git push

Once pushed, click on the "Actions" tab of the repo you created in the first step, and you should see a new build running. If the build is successful, there will be a new firmware.zip artifact shown on the summary screen you can download that will contain the new .uf2 files that can be flashed to the device.

Local Build

note

To build locally, be sure you've followed the development setup guide first.

Once you've fully created the new keyboard shield definition, you should be able to test with a build command like:

west build --pristine -b nice_nano_v2 -- -DSHIELD=<my_shield> -DZMK_EXTRA_MODULES=/full/path/to/your/module
# replace <my_shield> with e.g. <my_shield>_left for split keyboards, then repeat for <my_shield>_right

The above build command generates a build/zephyr/zmk.uf2 file that you can flash using the steps from the following section. See the dedicated building and flashing page for more details.

Flashing

If your board supports USB Flashing Format (UF2), copy that file onto the root of the USB mass storage device for your board. The controller should flash your built firmware and automatically restart once flashing is complete. If you need to flash an updated UF2 file with fixes, you can re-enter the bootloader by double tapping the reset button.

Alternatively, if your board supports flashing and you're not developing from within a Dockerized environment, enable Device Firmware Upgrade (DFU) mode on your board and run the following command to test your build:

west flash

Please have a look at documentation specific to building and flashing for additional information.

note

Further testing your keyboard shield without altering the root keymap file can be done with the use of -DZMK_CONFIG in your west build command, shown here