Skip to main content

New Keyboard Shield

danger

Before reading this section, it is vital that you read through our clean room policy.

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:

  • Create a new Zephyr module to contain your shield.
  • Create a new shield directory.
  • Add the base Kconfig files.
  • Add the shield overlay file defining:
    • The keyboard scan driver for detecting key press/release.
    • The matrix transform for mapping keyboard scan row/column values to key positions in the keymap.
    • The physical layout definition to select the matrix transform and keyboard scan instance.
  • 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.

Many of the above files will differ depending on whether your keyboard is a unibody or is split into multiple parts.

After adding ZMK support for a basic shield using this guide, check the sidebar for guides on adding any additional features (such as encoders) that your keyboard has. It may be helpful to review the upstream shields documentation to get a proper understanding of the underlying system before continuing.

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

The repository is a combination of the directories and files required of a ZMK config, and those required of a shield module. To create a shield module, the following components are needed:

  • The boards/shields directory, where the keyboard's files will go
  • The zephyr/module.yml file, which identifies and describes the module. See the Zephyr documentation for details on customising this file. For the purposes of creating a shield module, the default found in the template can be left untouched.

Neither of these should be moved out of their parent directory. The other files and directories such as config are not necessary for the purposes of a shield module, but rather intended to be used for user configuration and testing.

New Shield Directory

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 the shield name used to build your keyboard.

Kconfig.shield
# No whitespace after the comma or in your keyboard name!
config SHIELD_MY_KEYBOARD
def_bool $(shields_list_contains,my_keyboard)

This will set the SHIELD_MY_KEYBOARD flag to y whenever my_keyboard is used as the shield name. The SHIELD_MY_KEYBOARD flag will be used in Kconfig.defconfig to set other properties about your shield, so make sure that they match.

Kconfig.defconfig

The Kconfig.defconfig file is used to set new defaults for configuration settings 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.

Kconfig.defconfig
if SHIELD_MY_KEYBOARD

# Name must be less than 16 characters long!
config ZMK_KEYBOARD_NAME
default "My Keyboard"

endif

User Configuration Files

In addition to the Kconfig.shield and Kconfig.defconfig files, many shields will also define a user configuration file called my_keyboard.conf. This file exists to provide "suggestions" of configuration settings for a user to select, such as enabling deep sleep. Note that the name should match the shield/part name defined in the Kconfig.shield file.

warning

This file can also be used to set configuration options. However, if a flag is set in this file, the user can no longer change it. Though sometimes necessary, this method of setting configuration options is discouraged. The case for which this is necessary is due to be eliminated in the future, making this method redundant.

Shield Overlays

Shield overlay files contain a devicetree description that is merged with the primary board devicetree description during the firmware building process. There are three main things that need to be defined in this file:

  • Your keyboard scan (kscan) driver, which determines which GPIO pins to scan for key press events
  • Your matrix transform, which acts as a "bridge" between the kscan and the keymap
  • Your physical layout, which aggregates the above and (optionally) defines physical key positions so that the keyboard can be used with ZMK Studio.

A unibody keyboard will have a single overlay file named my_keyboard.overlay, where my_keyboard is the shield name defined in the Kconfig.shield file.

Kscan

The kscan node defines the controller GPIO pins that are used to scan for key press and release events. The pins are referred to using the GPIO labels noted in the pinouts below:

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.

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 Keyboard Scan configuration documentation has the full details on configuring the kscan driver.

For a simple 3x3 macropad matrix, the kscan might look something like:

my_keyboard.overlay
/ {
kscan0: kscan0 {
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)>
;
};
};

Matrix Transform

The matrix transform is used to transform row/column events into "key position" events.

When a key is pressed, a kscan event is generated from it with a row and a column value corresponding to the zero-based indices of the row-gpios and col-gpios pins that triggered the event, respectively. Then, the "key position" triggered is the index of the RC(row, column) in the matrix transform where row and column are the indices as mentioned above. This key position will in turn have a behavior binding associated with it in the keymap.

The my_keyboard.overlay must include a matrix transform that defines this mapping from row/column values to key positions. Add #include <dt-bindings/zmk/matrix_transform.h> to the top of the file.

Here is an example of a matrix transform for the previous 3x3 macropad:

my_keyboard.overlay
#include <dt-bindings/zmk/matrix_transform.h> // Put this with the other includes at the top of your overlay

/ {
default_transform: keymap_transform0 {
compatible = "zmk,matrix-transform";
columns = <3>; // Length of the "col-gpios" array
rows = <3>; // Length of the "row-gpios" array
map = <
// Key 1 | Key 2 | Key 3
RC(0,0) RC(0,1) RC(0,2)
// Key 4 | Key 5 | Key 6
RC(1,0) RC(1,1) RC(1,2)
// Key 7 | Key 8 | Key 9
RC(2,0) RC(2,1) RC(2,2)
>;
};
};

The matrix transform is also used to "correct" pin orderings into something that more closely matches the physical order of keys. Causes of abnormal pin orderings include:

  • 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.
  • For non-rectangular keyboards with thumb clusters, non 1u locations, etc.

See the in-tree keyboards that ZMK defines for examples of more complex matrix transformations.

Also see the matrix transform section in the Keyboard Scan configuration documentation for further details and examples of matrix transforms.

Physical Layout

Your keyboard will need to have a physical layout defined. Read through our dedicated page on physical layouts for information on how to define a physical layout. Once you have finished creating your physical layout, you should import the file in which it was created:

#include "my_keyboard-layouts.dtsi"

Chosen Node

Set the chosen node to a defined "default" physical layout. This should also be placed in the same file as the physical layout, i.e. my_keyboard.overlay for unibodies and my_keyboard.dtsi for split keyboards.

/ {
chosen {
zmk,physical-layout = &physical_layout0;
// Other chosen items
};
};

If you define multiple physical layouts, users can select a different layout by overriding the zmk,physical-layout chosen node in their keymap file or by using ZMK Studio if your board is compatible with it.

note

If all of your physical layouts use the same kscan node under the hood, you can skip setting the kscan property on each layout and instead assign the zmk,kscan chosen node to your single kscan instance:

/ {
chosen {
zmk,kscan = &kscan0;
zmk,physical-layout = &physical_layout0;
// Other chosen items
};
};

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/my_keyboard/my_keyboard.keymap file. The keymap is configured as an additional devicetree overlay that includes the following:

Here is an example simple keymap for a 3x3 macropad, with only one layer:

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

default_layer { // Layer 0
// -------------------------------------
// | Z | M | K |
// | A | B | C |
// | D | E | F |
bindings = <
&kp Z &kp M &kp K
&kp A &kp B &kp C
&kp D &kp E &kp F
>;
};
};
};

The keymap should match the order of the keys in the matrix transform exactly, left to right, top to bottom (they are both 1 dimensional arrays rearranged with newline characters for better legibility). See Keymaps for information on defining keymaps in ZMK. If you wish to use ZMK Studio with your keyboard, make sure to assign the ZMK Studio unlocking behavior to a key in your keymap.

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.

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 my_keyboard.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.

Testing

Once you've defined everything as described above, you can build your firmware to make sure everything is working.

GitHub Actions

To use GitHub Actions to test, push the files defining the keyboard to GitHub. Next, update the build.yaml of your zmk-config to build your keyboard.

Local Toolchain

You can also use a local toolchain setup to test your keyboard. Follow our guide for getting set up, then follow the instructions for building and flashing locally. You will need to specify the module of your keyboard when building.