New Behavior
Before reading this section, it is vital that you read through our clean room policy.
Overview
Behaviors refer to the actions that are invoked when a key is pressed or released. This guide outlines how to create a ZMK module that contains a new behavior.
If an out-of-tree behavior's use-case is deemed widespread enough to be merged into upstream ZMK, a new pull request may be issued instead. While this page describes practices that maximize compatibility between module-focused development and pull-request-based workflows, we also note specific modifications required for the latter to work properly.
In the context of the Zephyr RTOS, behaviors are implemented as "devices", which consist of:
- A devicetree binding file, which declares the behavior's properties
- A device driver written in C code
- Optionally, devicetree-source-include files (
.dtsi), which contain predefined instances of the behavior that may be included directly in keymaps
The general process for developing behaviors is:
- Create a new behavior repository
- Develop the behavior functionality
- Test changes locally
- Document behavior functionality
- Provide licensing information
Before developing new behaviors, developers should have a working knowledge of the Embedded Linux Devicetree. The following resources are provided for those seeking further understanding:
Creating a new Behavior Repository
Initializing a new Behavior Module
- Navigate to the ZMK module template repository
- Select "Use this template" in the upper right corner, followed by "Create a new repository"
- Choose an appropriate name for your new repository.
ZMK behavior modules should follow the naming convention,
zmk-behavior-<name-of-behavior>, using all lowercase letters and dashes to separate words - Complete the module's creation by selecting the repository's visibility, before clicking "Create repository"
- Clone a copy of your module to your development environment. The cloned repository should be easily accessible when building and testing firmware using a local toolchain.
Files unrelated to behavior development should be removed from your copy of the ZMK module template. The minimum viable filesystem for a behavior follows the following structure:
zmk-behavior-<name-of-behavior>/
├── CMakeLists.txt
├── Kconfig
├── LICENSE
├── README.md
├── dts
│ ├── behaviors
│ │ └── <name_of_behavior>.dtsi (optional)
│ └── bindings
│ └── behaviors
│ └── zmk,behavior-<name-of-behavior>.yaml
├── include
│ └── dt-bindings
│ └── zmk
│ └── <name_of_behavior>.h (optional)
├── src
│ └── behaviors
│ └── behavior_<name_of_behavior>.c
├── tests
│ └── <name_of_behavior>
│ ├── behavior_keymap.dtsi
│ └── normal
│ ├── events.patterns
│ ├── keycode_events.snapshot
│ ├── native_posix.keymap
│ └── native_posix_64.keymap
├── west.yml (optional)
└── zephyr
└── module.yml
For more information on module preparation, such as details on the contents of west.yml and zephyr/module.yml, refer to the page on module creation.
Once the module's tree has been organized properly, the relevant files are now ready to be populated. We will explain the purpose of the files listed in the tree above in order of increasing complexity.
Devicetree Bindings (.yaml)
Devicetree bindings use .yaml files to declare their properties.
They are stored in dts/bindings/behaviors/ and follow the same naming convention as the repository itself.
The mod-morph's devicetree binding is presented below as a simple example.
# Copyright (c) 2020 The ZMK Contributors
# SPDX-License-Identifier: MIT
description: Mod Morph Behavior
compatible: "zmk,behavior-mod-morph"
include: zero_param.yaml # Additional parameters
properties:
bindings:
type: phandle-array
required: true
mods:
type: int
required: true
keep-mods:
type: int
required: false
It can be seen that the .yaml files used for new behaviors' devicetree bindings consist of the following fields:
description
A brief statement of what the behavior is.
The description should be kept less than a sentence long because it is not a property seen by end-users.
Instead, detailed explanations of how the behavior works should be shared in the behavior's documentation.
compatible
Allows Zephyr to assign the correct devicetree node to the behavior extracted from the keymap or .dtsi, which is then connected to the proper driver.
The value of the compatible property is same as the name of the devicetree binding file.
In the example above, zmk,behavior-mod-morph.yaml lists compatible: "zmk,behavior-mod-morph".
include additional parameters
Choose between zero_param.yaml, one_param.yaml, or two_param.yaml depending on how many additional parameters are required to complete the behavior's binding in a keymap.
include | Example keymap binding |
|---|---|
zero_param.yaml | &sys_reset |
one_param.yaml | &kp A |
two_param.yaml | &mt LSHFT Z |
Some behaviors, like the Bluetooth behavior, use two_param.yaml despite their keymap usage appearing to only use one extra parameter, e.g., &bt BT_NXT or &bt BT_PRV.
Expanding their C preprocessor definitions reveals the following definitions: #define BT_NXT BT_NXT_CMD 0 and #define BT_PRV BT_PRV_CMD 0, respecting the use of two_param.yaml.
This is useful for creating behaviors that may have a primary "command", followed by a secondary parameter.
See Behavior Metadata for more information.
properties (Optional)
These are additional variables required to configure a particular instance of a behavior. More information can be found in ZMK's Devicetree primer or Zephyr's own documentation on Devicetree bindings.
Behavior Source Files (.c)
Behavior source files are stored in in src/behaviors/.
They are labelled in lowercase, beginning with the prefix behavior_, and ending with the behavior's name, using underscores to separate multiple words.
The developer may decide that there is a single global instance of a behavior, or multiple instances that act independently of one another. Some examples of the former are layer behaviors, backlight control, or endpoint selection. The latter includes keypresses, hold-taps, or tap-dances.
The code templates below show the differences between these categories, along with the essential components of a behavior source file.
- Behavior template with single instantiation
- Behavior template with multiple instantiation
/*
* Copyright (c) XXXX The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_<name_of_behavior>
// Dependencies
#include <zephyr/device.h>
#include <zephyr/logging/log.h>
#include <drivers/behavior.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/behavior.h>
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
// Instance-specific Data struct (Optional)
struct behavior_<name_of_behavior>_data {
bool data_param1;
bool data_param2;
bool data_param3;
};
// Instance-specific Config struct (Optional)
struct behavior_<name_of_behavior>_config {
bool config_param1;
bool config_param2;
bool config_param3;
};
// Initialization Function (Optional)
static int <name_of_behavior>_init(const struct device *dev) {
return 0;
};
static int on_<name_of_behavior>_binding_pressed(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) {
return ZMK_BEHAVIOR_OPAQUE;
}
static int on_<name_of_behavior>_binding_released(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) {
return ZMK_BEHAVIOR_OPAQUE;
}
// API struct
static const struct behavior_driver_api <name_of_behavior>_driver_api = {
.binding_pressed = on_<name_of_behavior>_binding_pressed,
.binding_released = on_<name_of_behavior>_binding_pressed,
};
BEHAVIOR_DT_INST_DEFINE(0, // Instance Number (0)
<name_of_behavior>_init, // Initialization Function
NULL, // Power Management Device Pointer
&<name_of_behavior>_data, // Behavior Data Pointer
&<name_of_behavior>_config, // Behavior Configuration Pointer
POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT // Initialization Level, Device Priority
&<name_of_behavior>_driver_api); // API struct
#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */
/*
* Copyright (c) XXXX The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_<name_of_behavior>
// Dependencies
#include <zephyr/device.h>
#include <zephyr/logging/log.h>
#include <drivers/behavior.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/behavior.h>
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
// Instance-specific Data struct (Optional)
struct behavior_<name_of_behavior>_data {
bool data_param1;
bool data_param2;
bool data_param3;
};
// Instance-specific Config struct (Optional)
struct behavior_<name_of_behavior>_config {
bool config_param1;
bool config_param2;
bool config_param3;
};
// Initialization Function (Optional)
static int <name_of_behavior>_init(const struct device *dev) {
return 0;
};
static int on_<name_of_behavior>_binding_pressed(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) {
return ZMK_BEHAVIOR_OPAQUE;
}
static int on_<name_of_behavior>_binding_released(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) {
return ZMK_BEHAVIOR_OPAQUE;
}
// API struct
static const struct behavior_driver_api <name_of_behavior>_driver_api = {
.binding_pressed = on_<name_of_behavior>_binding_pressed,
.binding_released = on_<name_of_behavior>_binding_pressed,
};
#define <NAME_OF_BEHAVIOR>_INST(n) \
static struct behavior_<name_of_behavior>_data_##n { \
.data_param1 = foo1; \
.data_param2 = foo2; \
.data_param3 = foo3; \
}; \
\
static struct behavior_<name_of_behavior>_config_##n { \
.config_param1 = bar1; \
.config_param2 = bar2; \
.config_param3 = bar3; \
}; \
\
BEHAVIOR_DT_INST_DEFINE(n, \ // Instance Number (Automatically populated by macro)
<name_of_behavior>_init, \ // Initialization Function
NULL, \ // Power Management Device Pointer
&<name_of_behavior>_data_##n, \ // Behavior Data Pointer
&<name_of_behavior>_config_##n, \ // Behavior Configuration Pointer
POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT \ // Initialization Level, Device Priority
&<name_of_behavior>_driver_api); // API struct
DT_INST_FOREACH_STATUS_OKAY(<NAME_OF_BEHAVIOR>_INST)
#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */
A more thorough explanation of the contents of a behavior source file can be found below.
Other source files may be created as well, as is often the case when creating new features or events from scratch.
For parity with upstream ZMK, these files will generally be placed in the root src/ directory, or src/events/.
Updating Kconfig
Kconfig files are used to configure the system firmware at compile time.
Behaviors specifically will generally use the DT_HAS_ZMK_BEHAVIOR_<NAME_OF_BEHAVIOR>_ENABLED macro, which checks if the behavior is defined in the devicetree.
This ensures that behavior-specific properties may be accessed without explicitly enabling the behavior in a keyboard's .conf or defconfig files.
config ZMK_BEHAVIOR_<NAME_OF_BEHAVIOR>
bool
default y
depends on DT_HAS_ZMK_BEHAVIOR_<NAME_OF_BEHAVIOR>_ENABLED
if ZMK_BEHAVIOR_<NAME_OF_BEHAVIOR>
config ZMK_BEHAVIOR_<NAME_OF_BEHAVIOR>_PROPERTY1
<bool | string | hex | int>
help <Explanation of property 1>
config ZMK_BEHAVIOR_<NAME_OF_BEHAVIOR>_PROPERTY2
<bool | string | hex | int>
help <Explanation of property 2>
config ZMK_BEHAVIOR_<NAME_OF_BEHAVIOR>_PROPERTY3
<bool | string | hex | int>
help <Explanation of property 3>
endif #ZMK_BEHAVIOR_<NAME_OF_BEHAVIOR>
For an overview on Kconfig files, see Configuration.
For more examples of behavior-specific Kconfig settings, see Behavior Configuration.
Updating CMakeLists.txt
CMakeLists.txt files are used in Zephyr's configuration stage when building firmware.
These specify which source files are included in the build, and may depend on the Kconfig settings shown previously.
At this point the developer should consider the behavior's locality.
Most behaviors are processed on a unibody keyboard, or the central half of a split board. An example is shown below.
# Copyright (c) XXXX The ZMK Contributors
# SPDX-License-Identifier: MIT
target_include_directories(app PRIVATE include)
if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
target_sources(app PRIVATE src/behaviors/behavior_<name_of_behavior>.c)
endif() # ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
Other common ways of enabling/blocking the inclusion of sources via CMakeLists.txt include:
| Condition | CMakeLists.txt entry |
|---|---|
| Locality is unibody, or the central part of a split keyboard | if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) |
| Locality is only on the central part of a split keyboard | if (CONFIG_ZMK_SPLIT AND CONFIG_ZMK_SPLIT_ROLE_CENTRAL) |
| Locality is only on the peripheral part of a split keyboard | if (CONFIG_ZMK_SPLIT AND (NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL)) |
| Kconfig Requirement must be met | Use target_sources_ifdef(CONFIG_<Configuration Requirement> app PRIVATE <name_of_behavior>.c) instead of target_sources(<name_of_behavior>.c) |
If submitting a pull request to upstream ZMK, the target_sources invocation would go inside zmk/app/CMakeLists.txt instead.
Optional: Defining Common Use-Cases for the Behavior (.dtsi)
.dtsi files, stored in the directory dts/behaviors/, are encouraged for behaviors with more common use-cases.
One such example is the mod-tap (&mt), which is a predefined type of hold-tap that takes a modifier key as the hold parameter and another key as the tap parameter.
For the purpose of this section, we will discuss the structure of zmk/app/dts/behaviors/gresc.dtsi below.
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <dt-bindings/zmk/behaviors.h>
#include <dt-bindings/zmk/keys.h>
/ {
behaviors {
#if ZMK_BEHAVIOR_OMIT(GRESC)
/omit-if-no-ref/
#endif
gresc: grave_escape {
compatible = "zmk,behavior-mod-morph";
#binding-cells = <0>;
bindings = <&kp ESC>, <&kp GRAVE>;
mods = <(MOD_LGUI|MOD_LSFT|MOD_RGUI|MOD_RSFT)>;
display-name = "Grave/Escape";
};
};
};
The format of a behavior's .dtsi file is identical to declaring an instance of the behavior in a user's keymap.
However, a major difference is that the value /omit-if-no-ref/ should be placed adjacent to the label and name of the behavior, as highlighted in the example.
This enables the behavior to only be compiled if it is used in the keymap.
If your behavior has its locality property set to anything other than BEHAVIOR_LOCALITY_CENTRAL, then the name of the node must be at most 8 characters long.
Otherwise, it will fail to be invoked on the peripheral half of a split keyboard.
In the above example, grave_escape is too long, so it would need to be shortened, e.g.
// Behavior can be invoked on peripherals, so name must be <= 8 characters.
/omit-if-no-ref/ gresc: gresc { ... };
After creating the .dtsi from above, you may #include <behaviors/new_behavior_instance.dtsi> at the top of your keymap to access the new behavior definition.
If submitting a pull request to upstream ZMK, this #include statement would go inside zmk/app/dts/behaviors.dtsi, instead of the keymap.
Developing the Behavior Functionality
Overview
This section elaborates on the contents of behavior sources that interact with ZMK directly. We will review the components from the behavior source templates in the order they appear and introduce new concepts that are commonly used in the development process.
Developing drivers for behaviors in ZMK makes extensive use of the Zephyr Devicetree API and Device Driver Model. Links to the Zephyr Project Documentation for both of these concepts can be found below:
If submitting a pull request, any .c files should be formatted according to clang-format to ensure that automated checks run smoothly.
Compatible: #define DT_DRV_COMPAT
This field should match the compatible field of your devicetree binding, using all lowercase letters and underscores to separate words, instead of hyphens and commas.
For example, the Caps Word behavior's devicetree binding lists compatible: "zmk,behavior-caps-word", so the DT_DRV_COMPAT is zmk_behavior_caps_word.
Dependencies
The dependencies required for any ZMK behavior are:
zephyr/device.h: Zephyr Device APIsdrivers/behavior.h: ZMK Behavior Functions (e.g. locality,behavior_keymap_binding_pressed,behavior_keymap_binding_released,behavior_sensor_keymap_binding_triggered)zephyr/logging/log.h: Zephyr Logging APIs (for more information on USB Logging in ZMK, see USB Logging).zmk/behavior.h: ZMK Behavior Information (e.g. parameters, position and timestamp of events)returnvalues:ZMK_BEHAVIOR_OPAQUE: Used to terminateon_<name_of_behavior>_binding_pressedandon_<name_of_behavior>_binding_releasedfunctions that accept(struct zmk_behavior_binding *binding, struct zmk_behavior_binding_event event)as parametersZMK_BEHAVIOR_TRANSPARENT: Used in thebinding_pressedandbinding_releasedfunctions for the transparent (&trans) behavior
structs:zmk_behavior_binding: Stores the name of the behavior device (char *behavior_dev) as astringand up to two additional parameters (uint32_t param1,uint32_t param2)zmk_behavior_binding_event: Contains layer, position, and timestamp data for an activezmk_behavior_binding
Other common dependencies include zmk/keymap.h, which allows behaviors to access layer information and extract behavior bindings from keymaps, and zmk/event_manager.h which is detailed below.
Behavior metadata
Behavior metadata documents the possible combinations of parameters that can be used with the behavior when added to your keymap. The metadata structure allows flexibility to specify different kinds of well known parameter types, such as a HID usage, different second parameters passed on the selected first parameter, etc.
You can see a few examples of how the metadata is implemented in practice for the following behaviors:
- Key press
- RGB underglow
- Hold-tap, which is dynamic based on what behaviors are set up in the hold-tap bindings
Behavior metadata consists of one or more metadata sets, where each metadata set has a set of values for the parameter(s) used with the behavior.
For example, a common approach for behaviors is to have a set of possible first parameters that identify the "command" to invoke for the behavior, and the second parameter is a detail/sub-parameter to the action.
You can see an example of this with the &bt behavior.
In that scenario, all &bt "commands" that take a BT profile as a second parameter are grouped into one set, and all commands that take no arguments are grouped into another.
This allows the ZMK Studio UI to properly show a input for a profile only when the appropriate first "command" selection is made in the UI. Here is a snippet of that setup from the behavior_bt.c code:
#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
// Set up the values for commands that take no additional parameter.
static const struct behavior_parameter_value_metadata no_arg_values[] = {
{
.display_name = "Next Profile",
.type = BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE,
.value = BT_NXT_CMD,
},
{
.display_name = "Previous Profile",
.type = BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE,
.value = BT_PRV_CMD,
},
{
.display_name = "Clear All Profiles",
.type = BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE,
.value = BT_CLR_ALL_CMD,
},
{
.display_name = "Clear Selected Profile",
.type = BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE,
.value = BT_CLR_CMD,
},
};
// Set up the "no arg" metadata set.
static const struct behavior_parameter_metadata_set no_args_set = {
.param1_values = no_arg_values,
.param1_values_len = ARRAY_SIZE(no_arg_values),
};
// Set up the possible param1 values for commands that take a profile index for param2
static const struct behavior_parameter_value_metadata prof_index_param1_values[] = {
{
.display_name = "Select Profile",
.type = BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE,
.value = BT_SEL_CMD,
},
{
.display_name = "Disconnect Profile",
.type = BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE,
.value = BT_DISC_CMD,
},
};
// Set up the param2 value metadata for the valid range of possible profiles to pick from.
static const struct behavior_parameter_value_metadata prof_index_param2_values[] = {
{
.display_name = "Profile",
.type = BEHAVIOR_PARAMETER_VALUE_TYPE_RANGE,
.range = {.min = 0, .max = ZMK_BLE_PROFILE_COUNT},
},
};
// Set up the metadata set for the commands that take a profile for the second parameter.
static const struct behavior_parameter_metadata_set profile_index_metadata_set = {
.param1_values = prof_index_param1_values,
.param1_values_len = ARRAY_SIZE(prof_index_param1_values),
.param2_values = prof_index_param2_values,
.param2_values_len = ARRAY_SIZE(prof_index_param2_values),
};
// Finally, expose all the sets in the top level aggregate structure.
static const struct behavior_parameter_metadata_set metadata_sets[] = {no_args_set,
profile_index_metadata_set};
static const struct behavior_parameter_metadata metadata = {
.sets_len = ARRAY_SIZE(metadata_sets),
.sets = metadata_sets,
};
#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
... Rest of the behavior implementation
// Add the metadata to the driver API conditionally:
static const struct behavior_driver_api behavior_bt_driver_api = {
.binding_pressed = on_keymap_binding_pressed,
.binding_released = on_keymap_binding_released,
#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
.parameter_metadata = &metadata,
#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
};
ZMK API struct
Comes in the form static const struct behavior_driver_api <name_of_behavior>_driver_api.
The most relevant fields of struct behavior_driver_api, defined in drivers/behavior.h, are shown below.
| Field | Description |
|---|---|
| .binding_pressed | The function called when the key is pressed. Typically reserved for a function named on_<name_of_behavior>_binding_pressed |
| .binding_released | The function called when the key is pressed. Typically reserved for a function named on_<name_of_behavior>_binding_released |
| .locality | Describes how the behavior affects parts of a split keyboard. |
Locality has been discussed previously at compile-time.
Locality in the context of the API struct refers to runtime locality.
The .locality field may take the following values.
BEHAVIOR_LOCALITY_CENTRAL: Behavior only affects the central half, which is the case for most keymap-related behavior.BEHAVIOR_LOCALITY_EVENT_SOURCE: Behavior affects only the central or peripheral half depending on which side invoked the behavior binding, such as reset behaviors.BEHAVIOR_LOCALITY_GLOBAL: Behavior affects the entire keyboard, such as external power and lighting-related behaviors that need to be synchronized across halves.
For unibody keyboards, all locality values perform the same as BEHAVIOR_LOCALITY_GLOBAL.
The API struct's metadata-specific fields are shown below.
| Field | Description |
|---|---|
| .get_parameter_metadata | Callback function that can dynamically provide/populate the metadata describing the parameters to use with the behavior so the behavior may be used with ZMK Studio. |
| .parameter_metadata | Pointer to metadata describing the parameters to use with the behavior so the behavior may be used with ZMK Studio. |
Invoking BEHAVIOR_DT_INST_DEFINE
BEHAVIOR_DT_INST_DEFINE is a special ZMK macro which uses Zephyr's DEVICE_DT_INST_DEFINE macro to define the driver instance, before adding it to a list of ZMK behaviors so that can be found by the function zmk_behavior_get_binding().
For more information on this function, refer to Zephyr's documentation on the Device Driver Model.
The example BEHAVIOR_DT_INST_DEFINE call can be left as is with the first parameter, the instance number, equal to 0 for behaviors that only require a single instance (e.g. external power, backlighting, accessing layers).
For behaviors that can have multiple instances (e.g. hold-taps, tap-dances, sticky-keys), BEHAVIOR_DT_INST_DEFINE can be placed inside a #define statement, usually formatted as #define <ABBREVIATED BEHAVIOR NAME>_INST(n), that sets up any data pointers and/or configuration pointers that are unique to each instance.
An example of this can be seen below, taking the #define KP_INST(n) from the hold-tap driver.
#define KP_INST(n) \
static const struct behavior_hold_tap_config behavior_hold_tap_config_##n = { \
.tapping_term_ms = DT_INST_PROP(n, tapping_term_ms), \
.hold_behavior_dev = DT_PROP(DT_INST_PHANDLE_BY_IDX(n, bindings, 0), label), \
.tap_behavior_dev = DT_PROP(DT_INST_PHANDLE_BY_IDX(n, bindings, 1), label), \
.quick_tap_ms = DT_INST_PROP(n, quick_tap_ms), \
.flavor = DT_ENUM_IDX(DT_DRV_INST(n), flavor), \
.retro_tap = DT_INST_PROP(n, retro_tap), \
.hold_trigger_key_positions = DT_INST_PROP(n, hold_trigger_key_positions), \
.hold_trigger_key_positions_len = DT_INST_PROP_LEN(n, hold_trigger_key_positions), \
}; \
BEHAVIOR_DT_INST_DEFINE(n, behavior_hold_tap_init, NULL, NULL, &behavior_hold_tap_config_##n, \
APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \
&behavior_hold_tap_driver_api);
DT_INST_FOREACH_STATUS_OKAY(KP_INST)
Note that in the hold-tap example, the instance number, 0, has been replaced by n, signifying the unique node_id of each instance of a behavior.
Furthermore, the DT_INST_FOREACH_STATUS_OKAY(KP_INST) macro iterates through each compatible, non-disabled devicetree node, creating and applying the proper values to any instance-specific configurations or data by invoking the KP_INST macro for each instance of the new behavior.
Behaviors also require the following parameters of BEHAVIOR_DT_INST_DEFINE to be changed:
Optional: Initialization function
Comes in the form static int <name_of_behavior>_init(const struct device *dev).
Initialization functions preconfigure any data, like resetting timers and position for hold-taps and tap-dances.
All initialization functions return 0; once complete.
The second argument of BEHAVIOR_DT_INST_DEFINE can be set to NULL instead if an initialization function is not required.
Optional: Data pointers
The data struct stores additional data required for each new instance of the behavior.
Regardless of the instance number, n, behavior_<name_of_behavior>_data_##n is typically initialized as an empty struct.
The data respective to each instance of the behavior can be accessed in functions like on_<name_of_behavior>_binding_pressed(struct zmk_behavior_binding *binding, struct zmk_behavior_binding_event event) by extracting the behavior device from the keybind like so:
const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev);
struct behavior_<name_of_behavior>_data *data = dev->data;
The variables stored inside the data struct, data, can be then modified as necessary.
The fourth argument of BEHAVIOR_DT_INST_DEFINE can be set to NULL instead if instance-specific data is not required.
Optional: Configuration pointers
The configuration struct stores the properties declared from the behavior's .yaml for each new instance of the behavior.
As seen in the #define KP_INST(n) of the hold-tap example, the configuration struct, behavior_<name_of_behavior>_config_##n, for each instance number, n, can be initialized using the Zephyr Devicetree Instance-based APIs,
which extract the values from the properties of each instance of the devicetree binding from a user's keymap or predefined use-case .dtsi files stored in app/dts/behaviors/.
We illustrate this further by comparing the #define KP_INST(n) from the hold-tap driver and the properties of the hold-tap devicetree binding.
The config structure instances should always be declared const
so they are placed into flash, not RAM, by the linker.
The fifth argument of BEHAVIOR_DT_INST_DEFINE can be set to NULL instead if instance-specific configurations are not required.
Keycodes
Let us examine one of the simplest behavior actions: sending and releasing keycodes.
The core of the key press behavior is raise_zmk_keycode_state_changed_from_encoded(), found in keycode_state_changed.h.
This function takes in three arguments: an HID usage, a boolean value to determine if the keycode is pressed or released, and a timestamp.
We present a snippet from the key press behavior source, where it is seen that the HID usage of each keycode is extracted from the keymap, before it is determined to be pressed or released.
static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) {
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1);
return raise_zmk_keycode_state_changed_from_encoded(binding->param1, true, event.timestamp);
}
static int on_keymap_binding_released(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) {
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1);
return raise_zmk_keycode_state_changed_from_encoded(binding->param1, false, event.timestamp);
}
static const struct behavior_driver_api behavior_key_press_driver_api = {
.binding_pressed = on_keymap_binding_pressed,
.binding_released = on_keymap_binding_released,
#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
.parameter_metadata = &metadata,
#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
};
Layers
All functions that interact with layers can be found in keymap.h.
Layers can be identified in two ways: an "order-independent" zmk_keymap_layer_id_t, and an "order-dependent" zmk_keymap_layer_index_t.
| Function | Description |
|---|---|
zmk_keymap_layer_default(void) | Returns a zmk_keymap_layer_id_t of the default layer. |
zmk_keymap_layer_state(void) | Returns the current keyboard's layer state: a bitmask where each bit represents the state of the corresponding order-independent ID. |
zmk_keymap_layer_active(zmk_keymap_layer_id_t layer) | Returns a bool representing if the layer with the chosen zmk_keymap_layer_id_t is active. |
zmk_keymap_highest_layer_active(void) | Returns a zmk_keymap_layer_index_t the ordered layer index of the highest active layer. |
zmk_keymap_layer_activate(zmk_keymap_layer_id_t layer) | Activates the chosen layer. Returns 0 if successful. Returns values < 0 if an error occurs. |
zmk_keymap_layer_deactivate(zmk_keymap_layer_id_t layer) | Deactivates the chosen layer. Returns 0 if successful. Returns values < 0 if an error occurs. |
zmk_keymap_layer_toggle(zmk_keymap_layer_id_t layer) | Toggles the chosen layer. Returns 0 if successful. Returns values < 0 if an error occurs. |
zmk_keymap_layer_to(zmk_keymap_layer_id_t layer) | Deactivates every layer, before activating the chosen layer. Returns 0 if successful. Returns values < 0 if an error occurs. |
zmk_keymap_layer_name(zmk_keymap_layer_id_t layer) | Returns a C-string containing the layer's name. |
zmk_keymap_layer_index_to_id(zmk_keymap_layer_index_t layer_index) | Returns the order-independent ID for a given order-dependent layer index. |
ZMK Events
The event manager is a queue-based system that can be leveraged by behaviors to check for significant changes in the system's state.
Some common examples of this are determining if one or more key positions have been pressed or released, to check if a specific keycode has been sent, or registering changes between keymap layers.
All events can be found in their headers, stored in zmk/app/include/zmk/events/.
To use the event manager, we #include <zmk/event_manager.h> at the top of our behavior source file.
Some examples of events that are the most relevant to behavior development can be seen below.
| Event | Description |
|---|---|
hid_indicators_changed.h | The current HID indicators (Num Lock, Caps Lock, Scroll Lock, Compose, Kana) as a bitmask |
keycode_state_changed.h | Keycode events' state (on/off), usage page, keycode value, modifiers, and timestamps |
layer_state_changed.h | Layer events' state (bitmask), layer index, and timestamps |
position_state_changed.h | Position events' state (on/off), source, position, and timestamps |
See the events directory for other examples of events.
For more information on how to interact with events and the event manager, see Events.
Interacting with other behaviors and the ZMK Behavior Queue
This section will refer to features found in behavior.h and behavior_queue.h.
#include <zmk/behavior.h>
These functions work with behaviors at a device level. They are used to retrieve the device associated with a keymap binding, or invoke other behaviors, such as ones provided as a parameter to the current behavior.
| Function | Description |
|---|---|
struct zmk_behavior_binding | A struct containing the behavior binding's name stored as a C-string, and its parameters. |
struct zmk_behavior_binding_event | A struct describing where and when a behavior binding is invoked based on its the layer, key position, and timestamp. For split keyboards, this also includes which part of the keyboard invoked the binding. |
zmk_behavior_get_binding(const char *name) | Get a const struct device* for a behavior from its name field. |
zmk_behavior_invoke_binding(const struct zmk_behavior_binding *src_binding, struct zmk_behavior_binding_event event, bool pressed) | Invoke a behavior given its binding and invoking event details. |
#include <zmk/behavior_queue.h>
The behavior queue is leveraged by macros and sensor rotation behaviors. This queue ensures that behaviors may be invoked sequentially using specific time-based triggers without blocking the rest of the keyboard functionality.
| Function | Description |
|---|---|
zmk_behavior_queue_add | Adds the behavior to the behavior queue. |
Testing Changes Locally
Create a new folder in tests/ (or app/tests/ if submitting a pull request) to develop virtual test sets for all common use cases of the behavior.
For pull requests, behaviors should be tested thoroughly on both virtual testing environments using west test and real hardware.
Zephyr currently does not support logging over Bluetooth, so any use of the serial monitor for hardware testing must be done over hardware UART or USB virtual UART.
- See Tests for more information on how to create virtual test sets.
- For hardware-based testing, see USB Logging.
Documenting Behavior Functionality
Consider the following prompts when writing documentation for new behaviors:
- What does it do? Describe some general use-cases for the behavior.
- Which properties included in the devicetree binding should be configured manually by the user? What do they do, and if applicable, what are their default values?
- What does an example implementation in a keymap look like? Include a code-snippet of the example implementation in the keymap file's
behaviorsnode. - How does the behavior perform in edge cases? For example, tap-dances invoke the last binding in its list of
bindingsonce the maximum number of keypresses has been reached.
Including visual aids alongside written documentation for additional clarity may be helpful.
If submitting a pull request, see Documentation for more information on writing, testing, and formatting ZMK documentation.
Licensing Information
The ZMK Project and its contributors do not claim to be legal representatives, and any material below should not considered official legal advice. When distributing your work, please review the terms and conditions associated with any relevant licenses thoroughly.
Developers may wish to share their work with the public, which is often done by sharing a link to a GitHub repository. However, making a repository public does not automatically qualify the repository as open source, or permit others to use the works as they see fit. To qualify a codebase as open source, authors must provide a license that in addition to their source code, satisfy criteria that includes but is not limited to:
- The source code and license must be freely accessible and redistributable
- The source code may be freely modified, which may result in the creation of derivative works under the conditions of the included license
- The license must not discriminate against any person, group of persons, or specific fields of endeavor
For more information, consider looking at the following resources:
Contributing to ZMK (MIT License)
The MIT License is used for developers submitting a pull request or those wish to make their work usable, modifiable, and distributable in its entirety to the ZMK community. If the author's intent is to contribute their work to ZMK in these manners, especially when submitting pull requests, they should be aware of the constraints specified in our clean room policy.
SPDX copyright headers for each of the files outlined in this document can be copied and pasted from the tabs below.
- Devicetree Bindings (.yaml) and CMakeLists.txt
- Drivers (.c) and predefined use-cases (.dtsi)
# Copyright (c) XXXX The ZMK Contributors
# SPDX-License-Identifier: MIT
/*
* Copyright (c) XXXX The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
Remember to change the copyright year (XXXX) to the current year when adding the copyright headers to your newly created files.
This also applies to the LICENSE file at the repository's root.
Other licenses
Developers may also use other licenses with their work. Some common example are Apache 2.0 or GNU Public Licenses (GPL). However, software licenses are generally treated as "one-way" compatible. This means that code registered under a more permissive license may be used in a project with a more restrictive license, but not the other way around.
For example, as noted in ZMK's clean room policy, projects like QMK and TMK use GPL licenses, which are more restrictive than ZMK's MIT license. Code from ZMK may be used as a reference when developing work for QMK/TMK, but code from QMK/TMK may not be used as source material when working on ZMK.