Devicetree Overview
ZMK makes heavy usage of a type of tree data structure known as devicetree. Devicetree is a declarative way of describing almost everything about a Zephyr device, from the definition of keymaps and configuration of behaviors all the way to the internal storage partitions and architecture of the board's MCU.
This page is an introduction to devicetree for ZMK users and designers. For further reading, refer to the devicetree spec and Zephyr's documentation.
Running Example
The following segment taken from a keymap will be used as a running example:
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
/ {
behaviors {
spc_ul: space_underscore {
compatible = "zmk,behavior-mod-morph";
#binding-cells = <0>;
bindings = <&kp SPACE>, <&kp UNDERSCORE>;
mods = <(MOD_LSFT|MOD_RSFT)>;
};
};
keymap {
compatible = "zmk,keymap";
default_layer {
bindings = <&spc_ul &kp Z &kp M &kp K>;
};
};
};
It may be helpful to open this page twice and leave one copy open at this example.
Note also that Devicetree uses C-style comments, i.e. // ...
for line comments and /* ... */
for block comments.
Structure
A devicetree node has the general structure (parts within []
being optional)
[label:] name {
[properties]
[child nodes]
};
The root node of the devicetree always has the name /
, i.e. is written as
/ {
[child nodes]
};
It is also the only node which has the /
character as a name. See the devicetree spec for permitted characters for node names.
After various preprocessing steps, all contents of the devicetree will be found within/under the root node. If one node is found within another node, we say that the first node is a child node of the second one. Similarly, one can also refer to a grandchild node, etc.
In the running example, behaviors
and keymap
are child nodes of the root node. space_underscore
and default_layer
are child nodes of behaviors
and keymap
respectively, making them both grandchild nodes of the root node.
Properties
What properties a node may have varies drastically. Of the standard properties, there are two which are of particularly relevant to users and designers: compatible
and status
. Additional standard properties may be found in the devicetree spec.
Property types
These are some of the property types you will see most often when working with ZMK. Zephyr's Devicetree bindings documentation provides more detailed information and a full list of types.
bool
True or false. To set the property to true, list it with no value. To set it to false, do not list it.
Example: property;
If a property has already been set to true and you need to override it to false, use the following command to delete the existing property:
/delete-property/ the-property-name;
int
A single integer surrounded by angle brackets. Also supports mathematical expressions.
Example: property = <42>;
string
Text surrounded by double quotes.
Example: property = "foo";
array
A list of integers surrounded by angle brackets and separated with spaces. Mathematical expressions can be used but must be surrounded by parenthesis.
Example: property = <1 2 3 4>;
Values can also be split into multiple blocks, e.g. property = <1 2>, <3 4>;
phandle
A single node reference surrounded by angle brackets. Phandles will be explained in more detail in a later section.
Example: property = <&label>
phandles
A list of node references surrounded by angle brackets. Phandles will be explained in more detail in a later section.
Example: property = <&label1 &label2 &label3>
phandle array
A list of node references and possibly numbers to associate with the node. Mathematical expressions can be used but must be surrounded by parenthesis. Phandles will be explained in more detail in a later section.
Example: property = <&none &mo 1>;
Values can also be split into multiple blocks, e.g. property = <&none>, <&mo 1>;
See the documentation for "phandle-array" in Zephyr's Devicetree bindings documentation for more details on how parameters are associated with nodes.
GPIO array
This is just a phandle array. The documentation lists this as a different type to make it clear which properties expect an array of GPIOs.
Each item in the array should be a label for a GPIO node (the names of which differ between hardware platforms) followed by an index and configuration flags. See Zephyr's GPIO documentation for a full list of flags. Phandles and labels will be explained in more detail in a later section.
Example:
some-gpios =
<&gpio0 0 GPIO_ACTIVE_HIGH>,
<&gpio0 1 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>
;
path
A path to a node, either as a node reference or as a string. This will be explained in more detail in a later section.
Examples:
property = &label;
property = "/path/to/some/node";
Compatible
The most important property that a node has is generally the compatible
property. This property is used to map code to nodes. There are some special cases, such as the node named chosen
, where the node name is used rather than a compatible
property.
In the running example, space_underscore
has the property compatible = "zmk,behavior-mod-morph";
. The ZMK's mod-morph behavior code acts on all nodes with compatible
set to this value. The ZMK keymap code acts similarly for compatible = "zmk,keymap";
.
The compatible
property is also used to identify what additional properties a node may have. Any properties which are not one of the standard properties must be listed in a "devicetree bindings" file. These files will sometimes also include some additional information on the usage of the node.
ZMK keeps all of its devicetree bindings under the app/dts/bindings
directory.
The bindings file for compatible = "zmk,behavior-mod-morph";
is app/dts/bindings/behaviors/zmk,behavior-mod-morph.yaml
.
# Copyright (c) 2020 The ZMK Contributors
# SPDX-License-Identifier: MIT
description: Mod Morph Behavior
compatible: "zmk,behavior-mod-morph"
include: zero_param.yaml
properties:
bindings:
type: phandle-array
required: true
mods:
type: int
required: true
keep-mods:
type: int
required: false
The properties the node can have are listed under properties
. Some additional properties are imported from zero_param.yaml. Bindings files are the authority on node properties, with our documentation of said properties sometimes omitting things like the #binding-cells
property (imported from the previously mentioned file, describing the number of parameters that the behavior accepts). A full description of the bindings file syntax can be found in Zephyr's documentation.
Note that binding files can also specify properties for children, like the zmk,keymap.yaml
bindings file specifying properties for layers in the keymap.
Status
The status
property simply describes the status of a node. For ZMK users and designers, there are only two relevant values that this could be set to:
status = "disabled";
The node is disabled. Code should not take effect or make use of the node, but it can still be referenced by other parts of the devicetree.status = "okay";
The default setting when not explicitly stated. The node is treated as "active". This property is generally only explicitly stated when overwriting astatus = "disabled";
.
How this property is used in practice will become more clear after the devicetree preprocessing section later on.
Labels and Phandles
In addition to names, nodes can also have labels. For the ZMK user/designer, labels are arguably more important than node names. Whereas node names are used within code to access individual nodes, labels are used to reference other nodes from within devicetree itself. Such a reference is called a phandle, and can be thought of as similar to a pointer in C.
In the running example, spc_ul
is the label given to the node space_underscore
. The bindings
property of the default_layer
node is a "phandle-array" - an array of references to other nodes1. Its first element is &spc_ul
- a phandle to the node with label spc_ul
, i.e. space_underscore
. &kp
is another example of a phandle. It points to a node defined as below:
/ {
behaviors {
kp: key_press {
compatible = "zmk,behavior-key-press";
#binding-cells = <1>;
display-name = "Key Press";
};
};
};
This node is imported from a different file -- imports will be discussed later on. The &kp
phandles found in the running example also show the concept of parameters being passed to phandles. In this case, Z
, M
, and K
are passed as parameters.
When ZMK needs to trigger a behavior found at a location in the keymap's binding
property, it uses the phandle to identify the behavior node which needs to be called. It then executes the code determined by the compatible
property of said node, passing in parameters while doing so2. Depending on the behavior, another behavior phandle may need to be triggered, in which case the same process is used to identify the node and thus the parts of code which need to be executed.
Essentially, each layer in a keymap consists of an array of phandles pointing to various behaviors (alongside parameters) that were defined elsewhere. If you do not need to define the behavior node yourself, that just means ZMK has already defined it for you.
Devicetree Preprocessing
Much of the complexity in dts
files comes from preprocessing. The resulting devicetree after all preprocessing has finished can be inspected for both GitHub Actions and local builds. For reasons that will make more sense later, your keymap and most of your customisations will be found near the bottom of the file.
Preprocessing comes from two sources:
- The C preprocessor can be used within Devicetree Source (
dts
) files. - Devicetree has its own system for merging together, overwriting, and even deleting nodes and properties.
C Preprocessor
An introduction to the C preprocessor is beyond the scope of this page. There are plenty of resources online for the unfamiliar reader to refer to.
However, some specific methods of how the C preprocessor is used in ZMK's devicetree files can be useful, to better understand how everything fits together.
The C preprocessor is used to import some nodes and other preprocessor definitions from other files. The lines
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
which are found at the top of the running example import the default behavior node definitions for ZMK, along with a list of preprocessor definitions. The parameters Z
, M
, and K
(passed to the &kp
phandle in the running example) are actually C preprocessor defines. For example, during preprocessing references to Z
get turned into the number 0x07001D
, which is the number that gets passed to the ZMK host device (e.g. your computer) for it to then re-interpret as the letter "z".
The C preprocessor often gets leveraged by ZMK power users to reduce repetition in their keymap files. An example of this is the macro-behavior convenience macro. ZMK designers will also come across the RC
macro used for matrix transformations, and make use of convenience defines such as GPIO_ACTIVE_HIGH
.
Devicetree Processing
A devicetree is almost always constructed from multiple files. These files are generally speaking:
.dtsi
files, which exist exclusively to be included via the C preprocessor (their contents get "pasted" at the location of the#include
command) and are not used by the build sytem otherwise.- A
.dts
file, which forms the "base" of the devicetree. A single one of these is always present when a devicetree is constructed. For ZMK, the.dts
file contains the sections of the devicetree describing the board. This includes importing a number of.dtsi
files describing the specific SoC that the board uses. - Any number of
.overlay
files. These files can come from various sources, such as shields or snippets. An overlay is applied to a.dts
file by appending its contents to the end of the.dts
file, i.e. it is placed at the bottom of the file. Multiple overlays are applied by doing so repeatedly in a particular order. Without going into the details of the exact order in which overlays are applied, it is enough to know that if you specify e.g.shield: corne_left nice_view_adapter nice_view
in yourbuild.yaml
, then the overlays are applied left to right. - A single
.keymap
file. This file being included is ZMK-specific, and is treated as the "final".overlay
file, appended after all other overlays.
Merging and overwriting nodes
When a node appears multiple times in the devicetree (after the files are imported and merged together), it gets merged into a single node as a preprocessing step. For example:
/ {
mn_ex: my_example_node {
property1 = <0>;
property2 = <2>;
};
};
/ {
my_example_node {
property2 = <1>;
property3 = <4>;
};
example2 {
property;
};
};
The second appearance of my_example_node
has priority, thus its property2
value will overwrite the first appearance. The two root nodes also get merged in the process. The resulting tree after processing would be
/ {
mn_ex: my_example_node {
property1 = <0>;
property2 = <1>;
property3 = <4>;
};
example2 {
property;
};
};
Labels do not get overwritten; a node can have multiple labels. Phandles can also be used to overwrite or add properties:
&mn_ex {
property4 = <2>;
};
The phandle approach is the recommended one, as one does not need to know the exact names of all the parent nodes with this approach. Crucially, when using phandles to overwrite or add properties, the phandle must not be located within the root node. It is instead placed outside of the tree entirely.
Special devicetree directives
Devicetree has some special directives that affect the tree. Relevant ones are:
- Nodes can be deleted with the /delete-node/ directive:
/delete-node/ &node_label;
outside of the root node. - Properties can be deleted with the /delete-property/ directive:
/delete-property/ node-property;
inside the relevant node. /omit-if-no-ref/
causes a node to be omitted from the resulting devicetree if there are no references/phandles to the node:/omit-if-no-ref/ &node_label;
Footnotes
-
A phandle array by definition also includes metadata, i.e. parameters. Strictly speaking, a list of phandles without metadata has type
phandles
rather thanphandle-array
. A property with a single phandle has typephandle
. ↩ -
The number of parameters passed to the behavior code (and skipped over to find the next behavior phandle) is determined by the
#binding-cells
property mentioned above. ↩