Skip to main content

· 5 min read
Pete Johanson

I'm happy to announce that we have completed the work to upgrade ZMK to Zephyr 3.5!

petejohanson did the upgrade work to adjust ZMK for the Zephyr changes:

  • Add west flash support to all UF2 capable boards.
  • Adjust for LVGL DTS/Kconfig changes
  • Zephyr core API changes, including CONTAINER_OF work API changes, init priority/callback, and others

Getting The Changes

Use the following steps to update to the latest tooling in order to properly use the new ZMK changes:

User Config Repositories Using GitHub Actions

Existing user config repositories using Github Actions to build will pull down Zephyr 3.5 automatically, however if you created your user config a while ago, you may need to update it to reference our shared build configuration to leverage the correct Docker image.

  1. Replace the contents of your .github/workflows/build.yml with:

    on: [push, pull_request, workflow_dispatch]

    jobs:
    build:
    uses: zmkfirmware/zmk/.github/workflows/build-user-config.yml@main
  2. If it doesn't exist already, add a new file to your repository named build.yaml:

    # 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
    #
    ---

and then update it as appropriate to build the right shields/boards for your configuration.

VS Code & Docker (Dev Container)

If you build locally using VS Code & Docker then:

  • Pull the latest ZMK main with git pull for your ZMK checkout
  • Reload the project
  • If you are prompted to rebuild the remote container, click Rebuild
  • Otherwise, press F1 and run Remote Containers: Rebuild Container
  • Once the container has rebuilt and reloaded, run west update to pull the updated Zephyr version and its dependencies.

Once the container has rebuilt, VS Code will be running the 3.5 Docker image.

Local Host Development

The following steps will get you building ZMK locally against Zephyr 3.5:

  • Run the updated toolchain installation steps, and once completed, remove the previously installed SDK version (optional, existing SDK should still work)
  • Install the latest version of west by running pip3 install --user --update west.
  • Pull the latest ZMK main with git pull for your ZMK checkout
  • Run west update to pull the updated Zephyr version and its dependencies

From there, you should be ready to build as normal!

Board/Shield Changes

The following changes have already been completed for all boards/shields in ZMK main branch. For existing or new PRs, or out of tree boards, the following changes are necessary to properly work with the latest changes.

West Flash Support

If you have a custom board for a target that has a UF2 supporting bootloader, you can easily add support for flashing via west flash. Note that using west flash isn't mandatory, it is merely a convenient way to automate copying to the mass storage device, which you can continue to do manually. To add support, add a line to your board's board.cmake file like so:

include(${ZEPHYR_BASE}/boards/common/uf2.board.cmake)

LVGL DTS/Kconfig Changes

Two items were changed for LVGL use for displays that may need adjusting for custom shields:

DPI Kconfig Rename

The old LV_Z_DPI Kconfig symbol was promoted to a Kconfig in upstream LVGL, and is now named LV_DPI_DEF. You will need to replace this symbol in your board/shield's Kconfig.defconfig file.

SSD1306 OLED Inverse Refactor

Inverting black/white pixels has moved out of the Kconfig system and into a new DTS property. If you have a custom shield that uses an SSD1306, you should:

  • Remove any override for the SSD1306_REVERSE_MODE from your Kconfig files.
  • Add the new inversion-on; boolean property to the SSD1306 node in your devicetree file.

Maxim max17048 Sensor Driver

Upstream Zephyr has added a driver for the max17048 fuel gauge, but using the new fuel gauge API that ZMK does not yet consume. To avoid a conflict with the new upstream and keep our existing sensor driver, our driver has been renamed to be namespaced with a ZMK prefix. The following changes are needed for any boards using the driver:

  • Change the compatible value for the node to be zmk,maxim-17048, e.g. compatible = "zmk,maxim-max17048";.
  • If enabling the driver explicitly via Kconfig, rename MAX17048 to the new ZMK_MAX17048 in your Kconfig.defconfig or <board>_defconfig files.

Upcoming Changes

Moving to Zephyr 3.5 will unblock several exciting efforts that were dependent on that Zephyr release.

BLE Stability Improvements

Many users have reported various BLE issues with some hardware combinations, including challenges with updated Intel drivers, and macOS general stability problems. The Zephyr 3.5 release includes many fixes for the BT host and controller portions that, combined with some small upcoming ZMK changes, have been reported to completely resolve previous issues. Further focused testing will immediately commence to fully verify the ZMK changes before making them the default.

If you'd like to test those changes, enable CONFIG_ZMK_BLE_EXPERIMENTAL_CONN=y for your builds.

Pointer Integration

The Zephyr 3.5 release includes a new input subsystem that we will be leveraging for our upcoming pointer support. The open PR for that work is now unblocked and further testing and code review will begin to work on getting that feature integrated into ZMK as well.

Power Domains

Several power domain related changes are now available as well, which were a necessity for continued work on the improved peripheral power handling that's planned to supersede the existing "VCC cutoff" code that currently exists but causes problems for builds that include multiple powered peripherals like Displays + RGB.

Thanks!

Thanks to all the testers who have helped verify ZMK functionality on the newer Zephyr version.

· 7 min read
Cem Aksoylar

This blog continues our series of posts where we highlight projects within the ZMK ecosystem that we think are interesting and that the users might benefit from knowing about them.

In this installment, we are highlighting two projects (and a bonus one!) from Joel Spadin, a member of the core ZMK team. The first one is ZMK Tools, a handy Visual Studio Code extension to ease working with ZMK configurations, and the second is ZMK Locale Generator, a tool to help users that use non-US English keyboard locales in their operating systems.

In the rest of the post we leave it to Joel to introduce and explain the motivations of his ZMK-related projects. Stay tuned for future installments in the series!

ZMK Tools

ZMK Tools is an extension for Visual Studio Code that helps with editing a ZMK user config repo or a fork of ZMK. I originally created it to add some code completion in .keymap files, but then I realized that with the web version of VS Code, I could also let you set up a user config repo and build firmware, much like the user setup script, except without downloading a single thing.

User Config Setup in Browser

Here is how you can use ZMK Tools to get started with writing a ZMK keymap entirely within your browser. More detailed instructions can be found on the ZMK Tools README.

  1. Open the ZMK config template repo on GitHub.
  2. Click the Use this template button and follow the instructions to create your own repo.
    • If you don't see this button, make sure you're signed in to GitHub first.
    • You can name the repo anything you want, but "zmk-config" is the conventional name.
  3. From the GitHub page for your new repo, press . (period) and it will re-open the repo in github.dev.
  4. Press Ctrl + P and enter the following to install the ZMK Tools extension:
    ext install spadin.zmk-tools
  5. Press Ctrl + Shift + P and run the ZMK: Add Keyboard command.
  6. Follow the prompts to select a keyboard. ZMK Tools will copy the default keymap for that keyboard if you don't already have one, and it will automatically add it to your build.yaml file so GitHub will build it for you.

You can then edit your .keymap and .conf files. Once you're done:

  1. Click the Source Control tab on the side bar.
  2. Hover over the header for the Changes list and click the + (Stage All Changes) button.
  3. Write a commit message and click Commit & Push to push your changes to GitHub.

GitHub will start building the new firmware. To check the results:

  1. Use your browser's back button to go back to your repo's GitHub page.
  2. Click the Actions tab at the top of the page.
  3. Click the latest build (it should show the commit message you entered earlier). If it's still in progress, wait for it to finish.
  4. If the build was successful, go to the Artifacts section and click firmware to download the firmware. If it failed, check the error and go back to github.dev to fix it.

Keymap Code Completion

ZMK Tools also provides some basic code completion in .keymap files. It will suggest any of ZMK's built-in behaviors inside bindings and sensor-bindings properties, and it will automatically add the necessary headers.

For example, with the cursor at the end of line 6 in the following keymap...

/ {
keymap {
compatible = "zmk,keymap";
default_layer {
bindings = <
&
>;
};
};
};

...it will suggest things such as &kp, &mo, etc., and upon entering one, it will recognize that #include <behaviors.dtsi> is missing and add it to the top of the keymap:

#include <behaviors.dtsi>
/ {
keymap {
compatible = "zmk,keymap";
default_layer {
bindings = <
&kp
>;
};
};
};

Press space after &kp, and it will suggest all of ZMK's key codes. Upon entering one, it will again recognize that #include <dt-bindings/zmk/keys.h> is missing and add it too:

#include <behaviors.dtsi>
#include <dt-bindings/zmk/keys.h>
/ {
keymap {
compatible = "zmk,keymap";
default_layer {
bindings = <
&kp A
>;
};
};
};

This can be very helpful for making sure you spelled key codes correctly and included all the correct headers.

Future Work

Unfortunately, all the code completion info currently comes from a config file baked into the extension, so it won't pick up any custom behaviors or key code aliases you've defined. I'd like to make that work eventually, but it's a much more difficult problem to solve.

ZMK Tools will discover all the boards/shields from both ZMK and your user config repo. With some recent changes in ZMK to allow pulling in features from other Zephyr modules, it's now possible to use board/shields defined in other repos, but ZMK Tools doesn't know about this yet. I'd like to support this too, but making it work in the web version of the extension will be challenging.

ZMK Locale Generator

ZMK's key codes follow the HID specification, and many key codes indicate the position of a key on US keyboard layout, not the key's function. If your operating system is set to a different keyboard locale, then the character each key types won't necessarily line up with the key code name. For example, on a German "QWERTZ" layout, &kp Y will type Z and &kp Z will type Y, so you have to write your layout as if it were QWERTY instead. Other layouts can be even more confusing!

ZMK Locale Generator is another tool I made to help with this. It reads CLDR keyboard layouts and generates #defines to alias key codes to names that make sense in other locales. To use it, first go to the latest release and download the header that matches the locale you use. Next, copy it into the same folder as your keymap and #include it:

#include "keys_de.h"

/ {
...
};

If you open the header file in a text editor, you'll see that it contains many of the standard ZMK key codes, except they are prefixed by the locale code. Depending on the locale, it may also define key codes for special characters specific to that locale, e.g. DE_A_UMLAUT for "ä" and DE_SZ for "ß". If you use these in your keymap, then ZMK will send the correct key codes to type those characters.

#include "keys_de.h"

/ {
keymap {
compatible = "zmk,keymap";
default_layer {
bindings = <
&kp DE_Q &kp DE_W &kp DE_E &kp DE_R &kp DE_T &kp DE_Z ...
>;
};
}
};

I should note that, as a native English speaker and typer, I don't use any of this myself! I just saw that many people were asking for help with this, and I realized I could automate a solution. If you find something that isn't generated correctly, please file an issue or PR a fix on GitHub.

Keyboard Latency Testing

The last project I want to mention is a tool for testing keyboard latency. It requires only a Rasbperry Pi, an optocoupler IC, a resistor, and some wire. If you've ever wondered how ZMK's latency compares to other keyboards, you can check the results here!

I don't have a very large collection of keyboards though, so the data is pretty limited so far. If you want to try it on your own keyboard, see the instructions on the keyboard latency tester README, and please send me a PR with your results!

About Me

I got a degree in electrical engineering but promptly became a software engineer instead. I still like tinkering with electronics though, so I discovered ZMK when I was making wireless macropad with a nice!nano, and I became a regular contributor after that. I use mostly larger keyboards with standard layouts and rarely use anything more complicated than momentary layers, so I've mostly focused on improving core features and tooling.

The keyboards I regularly use are a Ducky One 2 TKL that I leave at work, a Freebird TKL[^1], a custom wireless numpad, and a Yamaha CP4.

[^1] Running QMK, but I have designs to make a wireless PCB for it someday...

· 7 min read
Cem Aksoylar

This blog continues our series of posts where we highlight projects within the ZMK ecosystem that we think are interesting and that the users might benefit from knowing about them. You might be aware that ZMK configurations in the Devicetree format use the C preprocessor so that directives like #define RAISE 2 or #include <behaviors.dtsi> can be used in them. In this installment we are highlighting the zmk-nodefree-config project by urob that contains helper methods that utilizes this fact for users who prefer editing and maintaining their ZMK config directly using the Devicetree syntax format.

In the rest of the post we leave it to urob to introduce and explain the motivations of the project, and various ways it can be used to help maintain ZMK keymaps. Stay tuned for future installments in the series!

Overview

Loosely speaking the nodefree repo -- more on the name later -- is a collection of helper functions that simplify configuring keymap files. Unlike the graphical keymap editor covered in the previous spotlight post, it is aimed at users who edit and maintain directly the source code of their keymap files.

The provided helpers fall into roughly one of three categories:

  1. Helpers that eliminate boilerplate, reduce the complexity of keymaps, and improve readability.
  2. Helpers that improve portability of "position-based" properties such as combos.
  3. Helpers that define international and other unicode characters.

The reminder of this post details each of these three categories.

Eliminating Boilerplate

In ZMK, keymaps are configured using so-called Devicetree files. Devicetree files define a collection of nested nodes, whereas each node in turn specifies a variety of properties through which one can customize the keymap.

For example, the following snippet sets up a mod-morph behavior that sends . ("dot") when pressed by itself and sends : ("colon") when shifted:

/ {
behaviors {
dot_colon: dot_colon_behavior {
compatible = "zmk,behavior-mod-morph";
#binding-cells = <0>;
bindings = <&kp DOT>, <&kp COLON>;
mods = <(MOD_LSFT|MOD_RSFT)>;
};
};
};

Adding this snippet to the keymap will create a new node dot_colon_behavior (nested underneath the behaviors and root / nodes), and assigns it four properties (compatible, #binding-cells, etc). Here, the crucial properties are bindings and mods, which spell out the actual functionality of the new behavior. The rest of the snippet (including the nested node-structure) is boilerplate.

The idea of the nodefree repo is to use C preprocessor macros to improve readability by eliminating as much boilerplate as possible. Besides hiding redundant behavior properties from the user, it also automatically creates and nests all required behavior nodes, making for a "node-free" and less error-prone user experience (hence the name of the repo).

For example, using ZMK_BEHAVIOR, one of the repo's helper functions, the above snippet simplifies to:

ZMK_BEHAVIOR(dot_colon, mod_morph,
bindings = <&kp DOT>, <&kp COLON>;
mods = <(MOD_LSFT|MOD_RSFT)>;
)

For complex keymap files, the gains from eliminating boilerplate can be enormous. To provide a benchmark, consider my personal config, which uses the nodefree repo to create various behaviors, set up combos, and add layers to the keymap. Without the nodefree helpers, the total size of my keymap would have been 41 kB. Using the helper macros, the actual size is instead reduced to a more sane 12 kB.1

Simplifying "Position-based" Behaviors

In ZMK, there are several features that are position-based. As of today, these are combos and positional hold-taps, with behaviors like the "Swapper" and Leader key currently developed by Nick Conway in pull requests also utilizing them.

Configuring these behaviors involves lots of key counting, which can be cumbersome and error-prone, especially on larger keyboards. It also reduces the portability of configuration files across keyboards with different layouts.

To facilitate configuring position-based behaviors, the nodefree repo comes with a community-maintained library of "key-position labels" for a variety of popular layouts. The idea is to provide a standardized naming convention that is consistent across different keyboards. For instance, the labels for a 36-key layout are as follows:

    ╭─────────────────────┬─────────────────────╮
│ LT4 LT3 LT2 LT1 LT0 │ RT0 RT1 RT2 RT3 RT4 │
│ LM4 LM3 LM2 LM1 LM0 │ RM0 RM1 RM2 RM3 RM4 │
│ LB4 LB3 LB2 LB1 LB0 │ RB0 RB1 RB2 RB3 RB4 │
╰───────╮ LH2 LH1 LH0 │ RH0 RH1 RH2 ╭───────╯
╰─────────────┴─────────────╯

The labels are all of the following form:

  • L/R for Left/Right side
  • T/M/B/H for Top/Middle/Bottom and tHumb row.
  • 0/1/2/3/4 for the finger position, counting from the inside to the outside

The library currently contains definitions for 17 physical layouts, ranging from the tiny Osprette to the large-ish Glove80. While some of these layouts contain more keys than others, the idea behind the library is that keys that for all practical purposes are in the "same" location share the same label. That is, the 3 rows containing the alpha keys are always labeled T/M/B with LM1 and RM1 defining the home position of the index fingers. For larger boards, the numbers row is always labeled N. For even larger boards, the function key row and the row below B are labeled C and F (mnemonics for Ceiling and Floor), etc.

Besides sparing the user from counting keys, the library also makes it easy to port an entire, say, combo configuration from one keyboard to the next by simply switching layout headers.

Unicode and International Keycodes

The final category of helpers is targeted at people who wish to type international characters without switching the input language of their operation system. To do so, the repo comes with helper functions that can be used to define Unicode behaviors.

In addition, the repo also ships with a community-maintained library of language-files that define Unicode behaviors for all relevant characters in a given language. For instance, after loading the German language file, one can add &de_ae to the keymap, which will send ä/Ä when pressed or shifted.

About Me

My path to ZMK and programmable keyboards started in the early pandemic, when I built a Katana60 and learned how to touch-type Colemak. Soon after I purchased a Planck, which turned out to be the real gateway drug for me.

Committed to making the best out of the Planck's 48 keys, I have since discovered my love for tinkering with tiny layouts and finding new ways of squeezing out a bit more ergonomics. Along the way, I also made the switch from QMK to ZMK, whose "object-oriented" approach to behaviors I found more appealing for complex keymaps.2

These days I mostly type on a Corne-ish Zen and are waiting for the day when I will finally put together the Hypergolic that's been sitting on my desk for months. My current keymap is designed for 34 keys, making liberal use of combos and timerless homerow mods to make up for a lack of keys.

Footnotes

  1. To compute the impact on file size, I ran pcpp --passthru-unfound-includes on the base.keymap file, comparing two variants. First, I ran the pre-processor on the actual file. Second, I ran it on a version where I commented out all the nodefree headers, preventing any of the helper functions from getting expanded. The difference isolates precisely the size gains from eliminating boilerplate, which in my ZMK config are especially large due to a vast number of behaviors used to add various Unicode characters to my keymap.

  2. I am using the term object-oriented somewhat loosely here. What I mean by that is the differentiation between abstract behavior classes (such as hold-taps) and specific behavior instances that are added to the keymap. Allowing to set up multiple, reusable instances of each behavior has been a huge time-saver compared to QMK's more limited behavior settings that are either global or key-specific.

· 6 min read
Cem Aksoylar
Shows a screenshot of the Keymap Editor application featuring a graphical layout of the Corne Keyboard with a keymap loaded from the nickcoutsos/keymap-editor-demo-crkbd GitHub repository.Shows a screenshot of the Keymap Editor application featuring a graphical layout of the Corne Keyboard with a keymap loaded from the nickcoutsos/keymap-editor-demo-crkbd GitHub repository.

This blog post is the first in a series of posts where we highlight projects within the ZMK ecosystem that we think are cool and that the users might benefit from knowing about them. We are starting the series with a big one, Keymap Editor by Nick Coutsos!

In the rest of the post we leave it to Nick himself to introduce the project, detail his goals and motivation in developing such a tool, and talk about the future of the project. Stay tuned for future installments in the series!

What is Keymap Editor?

Keymap Editor is a web based graphical editor for ZMK keymaps. It provides a visual way to manage the contents of your keymap and if nothing else offers two critical features:

  1. Automatic formatting of the keymap file, so that bindings arrays remain readable
  2. Searchable behaviors, keycodes, commands, etc, so you won't have to remember if it's LCTL or LCTRL (I just had to double check myself and I guessed wrong, apparently)

What can Keymap Editor do?

  • Render devicetree keymaps using pre-defined, auto-generated, or side-loadable keyboard layouts
  • Integrate with a GitHub repo to streamline firmware builds, or FileSystem/Clipboard if you'd still rather build locally
  • Edit combos, behaviors, macros, conditional layers and rotary encoder bindings
  • Manage references: moving a layer or renaming a behavior will look for references throughout your keymap and update them.

But check back regularly, because I update pretty often. A recent significant achievement was enabling parameterized macros and tying it in with my existing parameter type resolution so, yeah, you can finally create that reusable macro combining bluetooth profile selection with RGB backlight colour. Or use it for an actual useful thing, even. (See also: Using Parameterized Macros in Keymap Editor)

My goals are, broadly:

  • Treat code as a first-class entity: as long as ZMK keymaps are described in devicetree code then an editor needs to produce readable devicetree code.
  • Flexibly support ZMK features: use of any ZMK keymap feature should theoretically be achievable within the app. In some cases this can mean more initial setup (See also: my thoughts on implementing "autoshift") but having that foundation makes its easier to add shortcuts and niceties — something I do quite often now.
  • Don't get in the way of not-yet-supported features: If a new ZMK feature is released and the app isn't able to add it natively, you can always edit your keymap file directly. While the app may not recognize the new features, further changes through the app should not break your keymap.

History of Keymap Editor

When I started writing Keymap Editor I had a handwired Dactyl variant running QMK. Manually editing keymap code was fine, but keeping things readable was important to me, and automating that was the best way to ensure consistency. Programmatically modifying source code was beyond me at the time so the first version persisted keymap data in JSON and spat out formatted versions of both the JSON and C keymaps.

After switching to ZMK I added a few more features, I guess as a pandemic project, and then gradually migrated from generating a templated keymap file to manipulating devicetree syntax directly, and that has made a big difference in adding new ZMK features.

Why am I doing this?

It started out as a useful tool for me. I shared it with the ZMK community and gained a little traction, and then apparently quite a bit of traction — turns out it's useful for a lot of people.

I'm a software developer because I enjoy building things. Much of my day-to-day work isn't user facing, so seeing how helpful the keymap editor has been for people in the ZMK community is a big motivator to keep improving it.

Future plans

Runtime updates

Streamlining the keymap update process is probably top of mind for most users, but that involves a really big firmware feature, and I'm the wrong person to tackle it.

That said, once there's a protocol I would absolutely be down to integrate it as an additional keymap source. Being able to pull data directly from the keyboard should unlock a lot of possibilities and ease some of the constraints imposed by using devicetree code as a medium.

Simplifying behavior use

I think a lot of people would like to see the concept of behaviors abstracted away for new users and to prompt them with

  • "When the key is tapped...",
  • "When the key is held...",
  • "When the key is double-tapped..." and so on.

Users who are less familiar with ZMK's behaviors and how they are composed may find these prompts to be more intuitive, and their answers could be mapped to an appropriate combination of behaviors managed internally by an editor.

Uh, what else?

This has been long enough already, if you're looking for a feature I haven't mentioned don't assume I won't add it. Feel free to make feature requests on the GitHub repo, and I'd be happy to discuss it!

About Me And My Keebs

I like computers and write software. Many in this field enjoy using mechanical keyboards for their feel or aesthetics, but what piqued my interest was the Dactyl keyboard. I think, ergonomics aside, I'm more interested in the DIY/maker aspect than the collecting of keyboards and switches.

So I made a Dactyl, and then I made another Dactyl and I made a third Dactyl that isn't interesting enough to photograph, but now I'm using ZMK so I left room for 18650 cells.

That last Dactyl (with MX browns and a cheap blank XDA keycap set) serves me well the eight or so hours a day I'll spend at my desk, but I also spend a good deal of time computing on my couch where I'll use... my Macbook's built-in keyboard.

In case that's not surprising enough I'll leave you with this: despite all of the work and testing I've put into the keymap editor project, I've only updated an actual keymap once in the last year.

Thank you and good night.

More information

· 13 min read
Cem Aksoylar

Welcome to the sixth ZMK "State Of The Firmware" (SOTF)!

This update will cover all the major activity since SOTF #5. That was over a year ago (again!), so there are many new exciting features and plenty of improvements to cover!

Recent Activity

Here's a summary of the various major changes since last time, broken down by theme:

Keymaps/Behaviors

Hold-tap improvements

andrewjrae added the require-prior-idle-ms property to the hold-tap behavior in #1187 and #1387, which prevents the hold behavior from triggering if it hasn't been a certain duration since the last key press. This is a useful feature to prevent accidental hold activations during quick typing and made its way into many keymaps! The same property was added to combos as well to help prevent false combo activations.

Note that an earlier iteration of this feature was supported with the global-quick-tap property, which did not allow customizing the timeout and used the value of quick-tap-ms for it. This property is now deprecated and users are encouraged to use require-prior-idle-ms instead.

urob added the hold-trigger-on-release property in #1423. This significantly increases the usefulness of positional constraints on hold-taps, since it allows combining multiple holds such as different modifiers for home row mods usage.

Masking mods in mod-morphs

aumuell, vrinek and urob contributed to improving the behavior of mod-morphs by masking the triggering modifiers and added keep-mods property in #1412. This unlocks more use cases for mod-morphs, since you are no longer constrained to emitting keycodes that work well with the triggering modifier keycodes.

As an example, you can now define a mod-morph that swaps ; and : so that the former is the shifted version of the latter, which wasn't previously possible:

        col_semi: colon_semicolon {
compatible = "zmk,behavior-mod-morph";
#binding-cells = <0>;
bindings = <&kp COLON>, <&kp SEMI>;
mods = <(MOD_LSFT|MOD_RSFT)>;
};

Parameterized macros

petejohanson added macros that can be parameterized with one or two parameters in #1232. This allows users to define macros in a more modular way and is a nice quality-of-life improvement.

As a simple example, you could define a macro that puts any keycode provided between double quotes as below, then use it like &ql A in your keymap:

        ql: quoted_letter {
#binding-cells = <1>;
compatible = "zmk,behavior-macro-one-param";
bindings =
<&kp DQT>,
<&macro_param_1to1 &kp MACRO_PLACEHOLDER>,
<&kp DQT>;
};

Please see the documentation page linked above for usage and more examples.

Arbitrary behaviors on encoder rotation

nickconway and petejohanson added sensor rotation behaviors to allow invoking arbitrary behaviors from encoders #1758. Previously encoder rotations could only invoke the key-press behavior &kp through the &inc_dec_kp binding, whereas now you can define new sensor rotation behaviors to invoke others.

(Note that currently behaviors that have "locality" such as &rgb_ug do not work as expected via encoder rotation bindings in split keyboards, due to issue #1494.)

Pre-releasing already pressed keys

andrewjrae contributed a tweak to emitting keycodes in #1828, where rolling multiple keys that involve the same keycode now releases the keycode before sending a press event again. While this might sound like a technical distinction, it leads to more correct behavior when quickly typing sequences like += and makes the key repeat behavior work properly when it is pressed before the previous key is released.

Key toggle behavior

cgoates added the key toggle behavior in #1278, which can be used via its &kt binding to toggle the state of a keycode between pressed and released.

Apple Globe key

ReFil added support for the C_AC_NEXT_KEYBOARD_LAYOUT_SELECT keycode with alias GLOBE which acts as the Globe key in macOS and iOS in #1938. Note that this keycode doesn't exactly behave like a Globe key that is present on an Apple keyboard and its limitations are documented in this comment thanks to testing by SethMilliken. These limitations will be noted in the official keycodes documentation shortly.

Bug fixes and other improvements

petejohanson, andrewjrae and okke-formsma tracked down and fixed an issue causing stuck keys when there are combos on key positions involving hold-tap behaviors in #1411. This was an elusive bug that took a lot of effort from the community to nail down and fix!

nguyendown and joelspadin tracked down and fixed a couple issues causing stuck keys with sticky keys in #1586, #1745.

okke-formsma fixed an issue allowing tap dances to be invoked by combos in #1518.

petejohanson tweaked the caps word behavior to ignore modifiers in #1330.

HelloThisIsFlo documented a bug with combos involving overlapping keys and different timeouts, produced a reproducing unit test, then proceeded to fix it in #1945.

Bluetooth and Split Improvements

Multiple peripherals

xudongzheng contributed to add support for more than one peripheral per keyboard in #836. This allows setups such as split keyboards with more than two halves, or enable a BLE-based "dongle mode" via a third device running ZMK that can stay connected to a computer via USB.

Note that documentation is still lacking for utilizing more than one peripheral and there will potentially be future changes in the build system to allow for more seamless configuration.

Pairing passkey requirement

petejohanson added the option to require passkey input while pairing to new devices in #1822. Enabling this will require you to enter a six digit passcode via the number keys on your keymap and press enter when pairing to a new device, enhancing security during the pairing procedure.

Split keyboard improvements

petejohanson contributed a fix to release held keys on peripheral disconnect #1340, which makes scenarios where a split disconnects unexpectedly less painful.

petejohanson also improved the settings_reset shield by making it clear bonds more reliably, and allow it to build for all boards in #1879.

petejohanson and xudongzheng contributed additional split connectivity improvements, via using directed advertising in #1913 and improving the robustness of central scanning in #1912.

Hardware Support

Encoders

petejohanson contributed a major refactor of encoder (and more generally sensor) functionality in #1039. While the documentation for these changes are still in progress, check out the dedicated blog post for more details.

This refactor paved way to implementing a long-awaited feature, encoder support in peripheral halves of split keyboards! Building upon the work by stephen in #728, petejohanson implemented support in #1841.

Direct GPIO driver

joelspadin extended the comprehensive debouncing framework used for matrix scan driver to the direct GPIO driver in #1288.

kurtis-lew added toggle mode support for direct GPIO driver in #1305. This allows for adding toggle switches to a keyboard, by properly reading their initial state on boot and making sure the power use is efficient.

IO peripheral drivers

petejohanson added support for the 595 shift register commonly used with smaller controllers like Seeeduino Xiaos, in #1325.

zhiayang added the driver for the MAX7318 GPIO expander in #1295.

Underglow auto-off options

ReFil added two new RGB auto off options, one using an idle timeout and the other USB status in #1010.

nice!view support

nicell added support for nice!view, a memory display optimized for low power use in #1462. He also contributed a custom vertically-oriented status screen that is automatically enabled when the nice_view shield is used in #1768, since the default status screen has a horizontal orientation. Please see the instructions in the nice!view README if you would like to restore the stock status screen.

E-paper display initialization

petejohanson contributed EPD initialization improvements in #1098, which makes the keyboards using slow refresh displays such as the Corne-ish Zen much more responsive during initial boot.

Xiao BLE improvements

Various improvements were made for the Seeeduino Xiao BLE board in #1293, d0176f36, #1545 and #1927 by petejohanson and caksoylar, enabling features necessary for ZMK and improving its power use.

Zephyr 3.2 Upgrade

petejohanson once again contributed the massive work necessary for upgrading ZMK to Zephyr 3.2 in #1499, with review help from joelspadin and testing by the community. This Zephyr release brings with it upgrades to the display library LVGL, adds official support for the RP2040 controllers and many internal refactors to help future development. Check out the dedicated blog post for more details!

Documentation

Configuration docs

joelspadin, through a massive amount of work in #722, contributed a whole new section to the documentation: configuration! It enumerates the configuration options for each ZMK feature that might be relevant to users in dedicated pages, making it a very handy reference.

In addition, the overview page presents an overview of how configuration works in Zephyr in the context of ZMK, in terms of devicetree files (like the keymap files or shield overlays), and Kconfig ones (like the .conf files). It is very helpful in de-mystifying what the various files do and what syntax is expected in them.

New behavior guide

For users or future contributors that might want to dive into writing their own ZMK behaviors, kurtis-lew wrote a useful guide on how to create new behaviors in #1268.

Tap dance and hold-tap documentation improvements

kurtis-lew also improved the documentation for these two behaviors in #1298, by updating the diagrams to better clarify how their timings work and adding examples for scenarios that are frequently asked by users.

Battery sensor documentation

joelspadin also added documentation for setting up battery sensors, typically required for new boards, in #868.

Shield interconnects

petejohanson updated the new shield guide for non-Pro Micro interconnects including Xiao, Arduino Uno and Blackpill in #1607.

Bluetooth feature page

petejohanson and caksoylar added a new Bluetooth feature page as part of #1499 and in #1818, detailing ZMK's Bluetooth implementation and troubleshooting for common problems.

In addition to the specific contributions listed above, various improvements and fixes to documentation are made by many users from the community, including but not limited to kurtis-lew, joelspadin, filterpaper, byran.tech, dxmh and caksoylar. These contributions are are all very appreciated!

Miscellaneous

Reusable GitHub build workflow

elagil helped switch the build workflow used by the user config repos to a reusable one in #1183 and it was further tweaked by filterpaper in #1258. This allows any changes in the workflow to be propagated automatically to users, rather than requiring them to make the updates. The build workflow can be customized by the users using input parameters if desired.

Pre-commit hooks

joelspadin added various pre-commit hooks and added checks to the repo to run them for each commit in #1651. These hooks and resulting updates standardize formatting across devicetree and other source files, reducing busywork on both contributors and reviewers.

Zephyr usage and other refactors

joelspadin also contributed a few refactor PRs such as #1269, #1255 and #1803, generally improving code quality and bringing the codebase in line with the latest Zephyr conventions.

petejohanson refactored the drivers structure to bring it in line with the current Zephyr conventions for out-of-tree drivers in #1919.

Updated USB polling interval default

USB HID polling interval now defaults to 1 ms, i.e. a 1000Hz polling rate, thanks to joelspadin's tweak in #1271.

Additional display config options

caksoylar added a couple configuration options for displays, including a setting to invert display colors in #1754 and an option to display the battery percentage for the stock status screen in #1563.

New Shields

New Boards

Coming Soon!

Some items listed in the last coming soon section are still under active development and other new exciting items are in progress:

  • Automatic/simple BLE profile management
  • Soft off support for turning the keyboard "off" through firmware
  • Improved automatic power management for devices with multiple peripherals, e.g. OLED displays and RGB LEDs
  • Caps/Scroll/Num Lock LED support
  • Mouse keys
  • Wired split support
  • More modular approach to external boards/shields, custom code, user keymaps, etc.
  • More shields and boards

Statistics

Some statistics of interest for ZMK:

  • GitHub (lifetime stats)
    • 166 Contributors
    • 1256 Closed PRs
    • 1883 Stars
    • 1949 Forks
  • Discord Chat
    • 8055 total registered (130% up from last SOTF!)
  • Website (last 30 days)
    • 52K page views
    • 4.7K new users

Sponsorship

While ZMK is an open source project that uses the permissive MIT license, below are opportunities for anyone who would like to show their support to the project financially.

Open Collective

The ZMK project has an Open Collective sponsorship that has been going for two and a half years. This fund helps pay for project costs like domain registration or development of hardware such as the ZMK Uno shield. Note that donations to this fund do not pay for the work of any individual contributor directly.

Contributor sponsorships

Project creator and lead Pete Johanson has a GitHub sponsorship set up that you can contribute to, in order to directly support his time and efforts in developing and maintaining ZMK. He has also been traveling full time while focusing on ZMK and keyboard hardware design for more than a year now! If you are curious, you can check out his blog post on deciding to embark on this adventure, in addition to his thoughts on contributor vs. project sponsorship, and sustainability of open source projects in general.

Thanks!

As the first person to author a State Of The Firmware post besides Pete, I'd like to take the opportunity to thank him for his efforts on leading and developing ZMK, along with fostering a great community of contributors and users around it.

Also a big thank you to contributors that submit patches and perform reviews, testers that help validate changes, and users that take time out of their day to help out folks with ZMK usage on Discord channels, GitHub issues and other communities.

Article Updates

  • 12/2023: Removed the deprecated label property from code snippets.

· 4 min read
Pete Johanson

Today, we merged a significant change to the low level sensor code that is used to support encoders. In particular, this paves the way for completing the work on supporting split peripheral sensors/encoders, and other future sensors like pointing devices.

As part of the work, backwards compatibility for existing shields has been retained, but only for a grace period to allow out-of-tree shields to move to the new approach for encoders.

Special thanks to joelspadin for the thorough code review and testing throughout the development of the refactor.

Summary of Changes

The following items have been merged:

  1. Split configuration of hardware details, and behavior configuration to allow more flexible functionality of sensors/encoders, in particular linear encoders that lack detents/"clicks" as they rotate.
  2. Support for upstream Zephyr sensor drivers, including the NRFX QDEC driver that can be used on nRF52 based keyboards.
  3. Sensor data handling changes that pave the way for split sensor handling easily.

Configuration Changes

The major changes to configuration in the devicetree files relates to how the number of steps/triggers for a given encoder are set. In particular, the number of pulses/steps for a given encoder is configured first, allowing ZMK to determine the exact angular degrees of change that is represented by a single pulse on the data lines to that encoder.

Once that angular degrees mapping is completed, now independently there is a configuration setting to control how many triggers of the behavior in the keymap should occur for each full rotation of the sensor. Another way to think of this is "how many degrees of rotation results in a triggering of the sensor behavior in your keymap layer".

Splitting these two parts of the encoder configuration allows greater flexibility, and fine grained control of encoder behavior for linear encoders that don't have fixed detents.

Old Configuration

Previously, an encoder configuration looked like:

    left_encoder: encoder_left {
compatible = "alps,ec11";
a-gpios = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
b-gpios = <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
resolution = <4>;
};

Here, the resolution property was used to indicate how many encoder pulses should trigger the sensor behavior one time. Next, the encoder is selected in the sensors node:

    sensors {
compatible = "zmk,keymap-sensors";
sensors = <&left_encoder &right_encoder>;
};

That was the entirety of the configuration for encoders.

New Configuration

    left_encoder: encoder_left {
compatible = "alps,ec11";
a-gpios = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
b-gpios = <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
steps = <80>;
};

Here, the steps property is now used to indicate how many encoder pulses there are in a single complete rotation of the encoder. Next, the encoder is selected in the sensors node as before, but an additional configuration is used to indicate how many times the encoder should trigger the behavior in your keymap per rotation:

    sensors {
compatible = "zmk,keymap-sensors";
sensors = <&left_encoder &right_encoder>;
triggers-per-rotation = <20>;
};

For tactile encoders that have detents, the triggers-per-rotation would match the number of detents on the encoder. For linear encoders, the value can be chosen to suit your needs.

Zephyr Sensor Drivers

The configuration changes bring ZMK's code in line with how upstream Zephyr sensor drivers handle rotations. This has the added advantage of allowing us to leverage other sensor drivers. On Nordic MCUs, like nRF52840, the NRFX QDEC driver can be used, for example:

&pinctrl {
qdec_default: qdec_default {
group1 {
psels = <NRF_PSEL(QDEC_A, 1, 11)>,
<NRF_PSEL(QDEC_B, 1, 10)>;
bias-pull-up;
};
};
};

// Set up the QDEC hardware based driver and give it the same label as the deleted node.
encoder: &qdec0 {
status = "okay";
led-pre = <0>;
steps = <80>;
pinctrl-0 = <&qdec_default>;
pinctrl-names = "default";
};

The NRFX QDEC driver has the advantage of supporting optical encoders as well, and although it polls, it does so in hardware without waking the MCU core; initial basic power profiling is promising.

Split Sensor/Encoder Support

In addition to the refactors for splitting the configuration, the changes merged included refactors designed to simplify and move forward with the long outstanding feature of supporting encoders on the peripheral side of split keyboards. That work is planned as a follow up.

Deprecation

The old configuration will be supported for a period of one month, and then removed, giving users a grace period to complete the migration to the new separated configuration.

Article Updates

  • 12/2023: Removed the deprecated label property from code snippets.

· 9 min read
Pete Johanson

I'm happy to announce that we have completed the work to upgrade ZMK to Zephyr 3.2!

petejohanson did the upgrade work to adjust ZMK for the Zephyr changes, with help from Nicell on the LVGL pieces.

  • Upgrade to LVGL 8.x API, and move to the new Kconfig settings.
  • Tons of RP2040 work.
  • Zephyr core API changes, including DTS label use changes.
  • Move to pinctrl Zephyr subsystem.

Getting The Changes

Use the following steps to update to the latest tooling in order to properly use the new ZMK changes:

User Config Repositories Using GitHub Actions

Existing user config repositories using Github Actions to build will pull down Zephyr 3.2 automatically, however if you created your user config a while ago, you may need to update it to reference our shared build configuration to leverage the correct Docker image.

  1. Replace the contents of your .github/workflows/build.yml with:

    on: [push, pull_request, workflow_dispatch]

    jobs:
    build:
    uses: zmkfirmware/zmk/.github/workflows/build-user-config.yml@main
  2. If it doesn't exist already, add a new file to your repository named build.yaml:

    # 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
    #
    ---

and then update it as appropriate to build the right shields/boards for your configuration.

Upgrade a manual script

If you have a custom GitHub Actions workflow you need to maintain for some reason, you can update the workflow to to use the stable Docker image tag for the build:

  • Open .github/workflows/build.yml in your editor/IDE

  • Change zmkfirmware/zmk-build-arm:2.5 to zmkfirmware/zmk-build-arm:stable wherever it is found

  • Locate and delete the lines for the DTS output step, which is no longer needed:

    - name: ${{ steps.variables.outputs.display-name }} DTS File
    if: ${{ always() }}
    run: |
    if [ -f "build/zephyr/${{ matrix.board }}.pre.tmp" ]; then cat -n build/zephyr/${{ matrix.board }}.pre.tmp; fi
    if [ -f "build/zephyr/zephyr.dts" ]; then cat -n build/zephyr/zephyr.dts; fi

VS Code & Docker (Dev Container)

If you build locally using VS Code & Docker then:

  • pull the latest ZMK main with git pull for your ZMK checkout
  • reload the project
  • if you are prompted to rebuild the remote container, click Rebuild
  • otherwise, press F1 and run Remote Containers: Rebuild Container
  • Once the container has rebuilt and reloaded, run west update to pull the updated Zephyr version and its dependencies.

Once the container has rebuilt, VS Code will be running the 3.2 Docker image.

Local Host Development

The following steps will get you building ZMK locally against Zephyr 3.2:

  • Run the updated toolchain installation steps, and once completed, remove the previously installed SDK version (optional, existing SDK should still work)
  • Install the latest version of west by running pip3 install --user --update west.
  • pull the latest ZMK main with git pull for your ZMK checkout
  • run west update to pull the updated Zephyr version and its dependencies

From there, you should be ready to build as normal!

Known Issues

A few testers have reported inconsistent issues with bluetooth connections on Windows after upgrading, which can be resolved by re-pairing your keyboard by:

  1. Remove the device from Windows.
  2. Clear the profile on your keyboard that is associated with the Windows device by triggering &bt BT_CLR on your keymap while that profile is active.
  3. Restart Windows.
  4. Re-connect Windows to your keyboard.

Windows Battery Reporting Fix

Zephyr 3.2 introduced a new Kconfig setting that can be used to work around a bug in Windows related to battery reporting. Check out our bluetooth config for the full details. The key new configuration that can be set if using Windows is:

CONFIG_BT_GATT_ENFORCE_SUBSCRIPTION=n

Keymap Changes

Due to conflicts with new devicetree node labels added for Zephyr's reset system, the &reset behavior has been renamed to &sys_reset.

All of the in-tree keymaps have been fixed, but you may encounter build failures about duplicate names, requiring you rename the behavior reference in your keymap. Use the Keymap Upgrader and this will get fixed for you automatically.

Board/Shield Changes

The following changes have already been completed for all boards/shields in ZMK main branch. For existing or new PRs, or out of tree boards, the following changes are necessary to properly work with the latest changes.

Move to pinctrl driver

Before this change, setting up the details of pins to use them for peripherals like SPI, I2C, etc. was a mix of platform specific driver code. Zephyr has moved to the newer pinctrl system to unify the handling of pin configuration, with additional flexibility for things like low power modes for those pins, etc.

Board specific shield overlays

The main area this affects existing shields is those with board specific overrides, e.g. <shield>/boards/seeeduino_xiao_ble.overlay, that sets up additional components on custom buses, e.g. addressable RGB LEDs leveraging the SPI MOSI pin.

nRF52 Pin Assignments

Previously in ZMK, we relied on per-driver devicetree source properties to set the alternate pin functions for things like SPI or I2C. For example, here is the I2C bus setup as it was previously on the nice_nano board:

&i2c0 {
compatible = "nordic,nrf-twi";
sda-pin = <17>;
scl-pin = <20>;
};

With the move to the pinctrl system, this setup now look like:

 &i2c0 {
compatible = "nordic,nrf-twi";
pinctrl-0 = <&i2c0_default>;
pinctrl-1 = <&i2c0_sleep>;
pinctrl-names = "default", "sleep";
};

which references the pinctrl configuration:

&pinctrl {
i2c0_default: i2c0_default {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 0, 17)>,
<NRF_PSEL(TWIM_SCL, 0, 20)>;
};
};

i2c0_sleep: i2c0_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 0, 17)>,
<NRF_PSEL(TWIM_SCL, 0, 20)>;
low-power-enable;
};
};
};

Although slightly more verbose this allows pin configuration infrastructure to be re-used, specify other modes, like sleep, etc. in a standard way across architectures.

Out of Tree Boards/Shields

All of the in-tree boards and shields have been upgraded, but if you maintain/use an out-of-tree board or shield that uses the converted boards and overrides pins for various buses, you may need to switch to pinctrl to match ZMK's new approach.

The approach is the following when updating a board:

  1. Add an entry CONFIG_PINCTRL=y to the <board>_defconfig file in the board directory.

  2. Add a new file with the naming convention <board>-pinctrl.dtsi to your board directory.

  3. In the new file, add your pinctrl entries that set up different pin control configurations for whatever peripherals/buses are needed. Here's the nice!nano file as an example:

    /*
    * Copyright (c) 2022 The ZMK Contributors
    * SPDX-License-Identifier: MIT
    */

    &pinctrl {
    uart0_default: uart0_default {
    group1 {
    psels = <NRF_PSEL(UART_RX, 0, 8)>;
    bias-pull-up;
    };
    group2 {
    psels = <NRF_PSEL(UART_TX, 0, 6)>;
    };
    };

    uart0_sleep: uart0_sleep {
    group1 {
    psels = <NRF_PSEL(UART_RX, 0, 8)>,
    <NRF_PSEL(UART_TX, 0, 6)>;
    low-power-enable;
    };
    };

    i2c0_default: i2c0_default {
    group1 {
    psels = <NRF_PSEL(TWIM_SDA, 0, 17)>,
    <NRF_PSEL(TWIM_SCL, 0, 20)>;
    };
    };

    i2c0_sleep: i2c0_sleep {
    group1 {
    psels = <NRF_PSEL(TWIM_SDA, 0, 17)>,
    <NRF_PSEL(TWIM_SCL, 0, 20)>;
    low-power-enable;
    };
    };
    };
  4. From the main <board>.dts file, add an #include "<board>-pinctrl.dtsi" to have the C-preprocessor combine the files.

  5. Update the various peripheral nodes to use the new pinctrl configurations. For example, the following old configuration:

    &i2c0 {
    compatible = "nordic,nrf-twi";
    sda-pin = <15>;
    scl-pin = <17>;
    };

    would be changed to:

    &i2c0 {
    compatible = "nordic,nrf-twi";
    pinctrl-0 = <&i2c0_default>;
    pinctrl-1 = <&i2c0_sleep>;
    pinctrl-names = "default", "sleep";
    };

Because pinctrl configuration is very dependent on the specific target SoC, you will rarely need to consider it for a shield overlay that leverages a pro micro or XIAO abstraction. As noted, you're more likely to need to fix up pinctrl settings is using a board specific shield overlay, e.g. <shield>/boards/<board>.overlay to set things up.

LVGL Kconfig changes.

With the update to LVGL 8.x, Zephyr now leverages an upstream Kconfig file for most LVGL settings. Due to this, the naming for many existing configs has been adjusted. For any configs moved upstream, the naming mostly involves a prefix change from LVGL_ to the shorter LV_. For any that are still Zephyr specific configs, they are now prefixed with LV_Z_ prefix.

If you maintain or use an out of tree board/shield with a display, the following will need to be changed in your Kconfig files:

  • LVGL_VDB_SIZE -> LV_Z_VDB_SIZE
  • LVGL_DPI -> LV_DPI_DEF
  • LVGL_BITS_PER_PIXEL -> LV_Z_BITS_PER_PIXEL

Other than those specific examples, most other Kconfig values can simply change the LVGL_ prefix to LV_.

Raspberry Pi Pico/RP2040 Support

This Zephyr update allows ZMK to support the new(-ish) RP2040 SoC found in the Raspberry Pi Pico.

note

ZMK does not support wired split communication yet, so RP2040 is only usable for non-split keyboards. To follow progress on wired splits, see #1117.

Supported Controllers

The following RP2040 powered controllers have board definitions for folks to test:

  • Raspberry Pi Pico (rpi_pico)
  • SparkFun Pro Micro RP2040 (sparkfun_pro_micro_rp2040)
  • Adafruit Keyboar/KB2040 (adafruit_kb2040)
  • Seeeduino XIAO RP2040 (seeeduino_xiao_rp2040)
  • Adafruit Qt PY RP2040 (adafruit_qt_py_rp2040)
  • BoardSource blok (boardsource_blok)
  • Elite-Pi (compatible with the sparkfun_pro_micro_rp2040 board)

Upcoming Changes

Display re-init

Zephyr's improved power domain support is a foundation upon which we can provide a proper fix for the longstanding display re-init bug which has prevented ZMK from formally supporting our display code.

There is work still remaining to fully leverage the power domain system within ZMK to fix the bug, but upgrading Zephyr is the first necessary step.

Thanks!

Thanks to all the testers who have helped verify ZMK functionality on the newer Zephyr version.

· 3 min read
Pete Johanson

Two years ago, today, I minted the first ever commit for ZMK:

commit 85c8be89dea8f7a00e8efb06d38e2b32f3459935
Author: Pete Johanson <peter@peterjohanson.com>
Date: Tue Apr 21 16:20:34 2020 -0400

Initial work.

.gitignore | 1 +
.gitmodules | 3 +++
CMakeLists.txt | 40 +++++++++++++++++++++++++++++++++++++++
src/main.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/zmk_lib.h | 7 +++++++
zephyr-rust | 1 +
6 files changed, 112 insertions(+)

I will never forget that commit. Not because of the code it contained (please don't look, it's not worth it!), but for what it started.

Working on ZMK has given me the opportunity to reconnect with old friends (@brixmeister was my Gentoo mentor/sponsor when I became a contributor there on my first ever OSS project, and is a current active Zephyr RTOS contributor!), make new ones, and learn so much from the amazing mechanical keyboard community.

First Keyboard

But I'm getting ahead of myself! Back to early ZMK. I present you the first ZMK keyboard:

stm32wb55rg dev kit keyboard

That first "keyboard" taught me a lot. It forced me to dust off my long forgotten, rudimentary electronics knowledge, and gave me my first taste of really combining the physical/tangible with code in a way that years of doing backend API development never had.

I was hooked.

Zephyr RTOS

Early in my brainstorming, I knew I needed a foundation to build upon that would get me "a lot for free." I evaluated several different real-time operating systems (RTOSes) and happened upon Zephyr. It immediately ticked all the boxes I wanted:

  • Robust, open source Bluetooth stack, supporting multiple SoCs. At the time, I was trying out stm32wb thanks to some interest among keyboard designers, but I also so there were other compelling choices that might be a good fit.
  • An open source, non-copyleft license. I am a firm believer in F/OSS, and wanted to use a license that was as unrestricted as possible.
  • Had a lot of core APIs available, so I could focus on the keyboard functionality, not the plumbing. I love tinkering, but I wanted to focus my time on the interesting bits, not infrastructure.

I'm really happy with the choice, it has served us incredibly well the past two years.

Real Keyboard

At some point, somehow, innovaker introduced me to nicell who graciously sent me a few of the early pre-production nice!nano controllers, which I was able to get running on my Kyria. Doing so required the first split code, as well as lots of general improvements.

kyria keyboard

The day I was finally able to type on a wireless, split keyboard running ZMK was deeply momentous for me!

Onward and Upward

We've come a long way since then, with our supported hardware, features and behaviors growing regularly.

ZMK powered keyboards are now available in group buys and in stock at various vendors; compatible controllers have been used in a wide range of builds to empower our users to free themselves from their USB/TRRS cables and move about untethered.

This progress is only possible thanks to all of the contributors who've joined me in the vision for a wireless-first world. I am so grateful for everyone who has given their time to contribute code, answer questions on our Discord server, write more documentation, and especially all the users who have trusted us to make their input devices work.

I can't wait to see what we can accomplish together in the next two years.

· 8 min read
Pete Johanson

Welcome to the fifth ZMK "State Of The Firmware" (SOTF)!

This update will cover all the major activity since SOTF #4. That was over a year ago, so lots to cover!

Recent Activity

Here's a summary of the various major changes since last time, broken down by theme:

Keymaps/Behaviors

Since last time, there have been several new powerful keymap features and behaviors added, including several asked for features, such as tap-dance and macros.

Caps Word

petejohanson added the caps word behavior, i.e. &caps_word, in #823 that allows toggling a mode where all all alpha characters are sent to the host capitalized until a non-alpha, non-"continue list" keycode is sent. This can be useful for typing things like CONFIG_ENABLE_CAPS_WORD without having to hold down shift. This is similar in spirit to using the caps lock key, but with the added benefit of turning itself off automatically.

Key Repeat

petejohanson added the new key repeat behavior in #1034 to allow repeating the last sent key-press again, including any modifiers that were applied to that key press. It can be added to your keymap using the simple &key_repeat reference.

Macros

petejohanson, taking heavy inspiration on the initial work from okke-formsma, added macro support in #1168. Several common patterns are documented, but one example, changing the underglow color as you activate/deactivate a layer, looks like:

ZMK_MACRO(layer_color_macro,
wait-ms = <0>;
tap-ms = <0>;
bindings
= <&macro_press &mo 1>
, <&macro_tap &rgb_ug RGB_COLOR_HSB(128,100,100)>
, <&macro_pause_for_release>
, <&macro_release &mo 1>
, <&macro_tap &rgb_ug RGB_COLOR_HSB(300,100,50)>;
)

Tap Dance

kurtis-lew worked diligently to add the tap-dance behavior in #1139, allowing different behaviors to be invoked based on the number of times a user taps a single key in their keymap, e.g.

/ {
behaviors {
td0: tap_dance_0 {
compatible = "zmk,behavior-tap-dance";
#binding-cells = <0>;
tapping-term-ms = <200>;
bindings = <&kp N1>, <&kp N2>, <&kp N3>;
};
};

keymap {
compatible = "zmk,keymap";

default_layer {
bindings = <
&td0
>;
};
};
};

Conditional Layers

bcat added conditional layers in #830 as a generalized version of the common "adjust layer" pattern on smaller keyboards.

Example:

/ {
conditional_layers {
compatible = "zmk,conditional-layers";
tri_layer {
if-layers = <1 2>;
then-layer = <3>;
};
};
};

Combos

mcrosson added the layer specific combos in #661, so users can make certain combos only triggerable when the layers set for the combo are active.

This is used by the ZMK implementation of ARTSEY extensively.

Sticky Keys

okke-formsma updated sticky keys in #1122 to add the ignore-modifiers; property; when set, sticky keys won't release when other modifiers are pressed. This allows you to combine sticky modifiers, which is popularly used with "callum-style mods".

Hold-Tap Improvements

jmding8 added an additional positional hold-tap configuration in #835 to help certain sequences produce the expected results.

jmding8 also added an additional hold-tap flavor: tap-unless-interrupted in #1018 which works very well with the new positional hold-tap config.

okke-formsma implemented retro-tap hold-tap property in #667

okke-formsma also added quick-tap-ms hold-tap property in #655

Apple Device Compatibility Improvements

Pairing

petejohanson did some sleuthing and fixed a long standing problem with inconsistent pairing with macOS in [#946]](https://github.com/zmkfirmware/zmk/pull/946). With the changes, macOS more reliably pairs with ZMK devices.

Consumer (Media) Codes

Another persistent bug that Apple users experienced was related to crashes and problems with keyboard configurations, that was traced to an issue with ZMK's HID usage that was fixed by petejohanson in #726.

Debounce Enhancements

joelspadin applied some major enhancements to our debouncing approach to allow fine grained control of our debouncing in #888, including allowing eager debouncing which can reduce key press latency.

Split Improvements

Behavior Locality

The long awaited locality enhancement was finally merged by petejohanson in #547, allowing more fine grained control of where certain behaviors are invoked. Some key improvements thanks to the changes:

  • RGB Underglow behaviors now run globally, so enabling/disabling RGB, changing the color, animation, etc. applies to both sides of a split properly.
  • Reset/Bootloader behaviors now run wherever the key was pressed. For example, adding a &bootloader reference to the peripheral side of a split will now put that side of the split into the bootloader when pressed.

Split Connections

petejohanson also added fixes to improve split re-connection for certain scenarios in #984, helping ensure splits properly connect when one side or the other is reset.

Hardware Support

Backlight

bortoz added single color backlight support in #904 for those keyboards that have it as an alternative to RGB underglow.

E-Paper Display (EPD) Driver

petejohanson worked with LOWPROKB to add support for the E-Paper Displays (EPD) in #895 used in keyboards like the Corne-ish Zen.

nRF VDDH Battery Sensing

joelspadin added a new sensor driver to support battery charge calculation by sensing voltage on the VDDH pin on nRF52 chips in #750, which is particularly useful for designs using "high voltage mode" with that SoC.

Miscellaneous

Documentation

dxmh and caksoylar have joined the ZMK organization to help with documentation, and have been doing an amazing job adding new docs, and leading reviewing docs related PRs to free other contributors up to focus on other areas. It's been an incredible addition to ZMK!

NKRO Support

petejohanson's work on the HID foundation also included adding support for full NKRO HID in #726 that can be enabled by adding the following to your .conf file for your config:

CONFIG_ZMK_HID_REPORT_TYPE_NKRO=y

Power Profiler

It's been live for a while, but nicell added an amazing power profiler in #312 to allow users to estimate their battery life for various hardware configurations.

Min/Max Underglow Brightness

malinges added support for configuring min/max underglow brightness in #944 by setting the values in your .conf file as percentages of full:

CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN=20
CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX=80

This can be useful to be sure that lowering brightness doesn't set the brightness to zero, and raising the brightness doesn't consume too much power.

Zephyr 3.0

petejohanson helped prepare and test the upgrade of ZMK to Zephyr 3.0 in #1143. The updated Zephyr release brings with it some key BLE stability fixes, as well as various other core improvements that improve ZMK. This was a huge undertaking!

New Shields

New Boards

Board/Shield Metadata

nicell and petejohanson worked together in #883 to settle on a metadata format that is used to document every board and shield. This now drives automatic generation of our supported hardware page and our more nuanced GH Actions automation for testing changes to ZMK.

Coming Soon!

Some items listed in the last coming soon section are still under active development.

  • RP2040 support
  • Peripheral rotary encoder support
  • Caps/Scroll/Num Lock LED support
  • Mouse Keys
  • Wired split support
  • More modular approach to external boards/shields, custom code, user keymaps, etc.
  • More shields and boards

Statistics

Some statistics of interest for ZMK:

  • GitHub (lifetime stats)
    • 105 Contributors
    • 791 Closed PRs
    • 849 Stars
    • 832 Forks
  • Discord Chat
    • 3430 total registered
  • Website (last 30 days)
    • 35.9K page views
    • 3.29K new users

Thanks!

As we approach the two year birthday for ZMK, I am reminded of how far we have come in such a short time, in large part thanks to the amazing community that has grown around it. I am so grateful to have so many contributors, testers, and user believing in the project and helping make it a joy to work on.

Article Updates

  • 12/2023: Removed the deprecated label property from code snippets.

· 6 min read
Pete Johanson

I'm happy to announce that we have completed the work to upgrade ZMK to Zephyr 3.0!

petejohanson did the upgrade work to adjust ZMK for the Zephyr changes.

  • Moving to Zephyr's UF2 build integration that was submitted upstream by petejohanson
  • Additional color-mapping property needed for ws2812 LED strep devicetree nodes
  • Zephyr core API changes, including delayed work, USB/HID
  • Adjust for pinctrl changes on stm32
  • Fixes for power management and log formatter changes

Getting The Changes

Use the following steps to update to the latest tooling in order to properly use the new ZMK changes:

User Config Repositories Using GitHub Actions

Existing user config repositories using Github Actions to build will pull down Zephyr 3.0 automatically, however to build properly, the repository needs to be updated to use the stable Docker image tag for the build:

  • Open .github/workflows/build.yml in your editor/IDE

  • Change zmkfirmware/zmk-build-arm:2.5 to zmkfirmware/zmk-build-arm:stable wherever it is found

  • Locate and delete the lines for the DTS output step, which is no longer needed:

    - name: ${{ steps.variables.outputs.display-name }} DTS File
    if: ${{ always() }}
    run: |
    if [ -f "build/zephyr/${{ matrix.board }}.pre.tmp" ]; then cat -n build/zephyr/${{ matrix.board }}.pre.tmp; fi
    if [ -f "build/zephyr/zephyr.dts" ]; then cat -n build/zephyr/zephyr.dts; fi
note

If you created your user config repository a while ago, you may find that your build.yml file instead references a zephyr-west-action-arm custom GitHub Action instead. In this case, the upgrade is not as direct. We suggest that instead you re-create your config repository to get an updated setup using the new automation approach.

VS Code & Docker (Dev Container)

If you build locally using VS Code & Docker then:

  • pull the latest ZMK main with git pull for your ZMK checkout
  • reload the project
  • if you are prompted to rebuild the remote container, click Rebuild
  • otherwise, press F1 and run Remote Containers: Rebuild Container
  • Once the container has rebuilt and reloaded, run west update to pull the updated Zephyr version and its dependencies.

Once the container has rebuilt, VS Code will be running the 3.0 Docker image.

Local Host Development

The following steps will get you building ZMK locally against Zephyr 3.0:

  • Run the updated toolchain installation steps, and once completed, remove the previously installed SDK version (optional, existing SDK should still work)
  • pull the latest ZMK main with git pull for your ZMK checkout
  • run west update to pull the updated Zephyr version and its dependencies

From there, you should be ready to build as normal!

Board/Shield Changes

The following changes have already been completed for all boards/shields in ZMK main branch. For existing or new PRs, or out of tree boards, the following changes are necessary to properly work with the latest changes.

RGB Underglow

Zephyr's WS2812 led_strip driver added a new required property. When adding underglow to a board, you now must also add the additional include #include <dt-bindings/led/led.h> at the top of your devicetree file, and add a color-mapping property like:

led_strip: ws2812@0 {
compatible = "worldsemi,ws2812-spi";

/* SPI */
reg = <0>; /* ignored, but necessary for SPI bindings */
spi-max-frequency = <4000000>;

/* WS2812 */
chain-length = <10>; /* number of LEDs */
spi-one-frame = <0x70>;
spi-zero-frame = <0x40>;
color-mapping = <LED_COLOR_ID_GREEN
LED_COLOR_ID_RED
LED_COLOR_ID_BLUE>;
};
note

Standard WS2812 LEDs use a wire protocol where the bits for the colors green, red, and blue values are sent in that order. If your board/shield uses LEDs that require the data sent in a different order, the color-mapping property ordering should be changed to match.

Display Selection

Zephyr moved to using a chosen node named zephyr,display to select the display device to be used with LVGL, the underlying display library we use.

For example, for a shield with:

&pro_micro_i2c {
status = "okay";

oled: ssd1306@3c {
compatible = "solomon,ssd1306fb";
reg = <0x3c>;
width = <128>;
height = <32>;
segment-offset = <0>;
page-offset = <0>;
display-offset = <0>;
multiplex-ratio = <31>;
com-invdir;
segment-remap;
com-sequential;
prechargep = <0x22>;
};
};

You would add a chosen node like:

/ {
chosen {
zephyr,display = &oled;
};
};

USB Logging

Zephyr unified the way the console/logging device is selected, removing the hacks that special-cased the USB CDC ACM output. Now, the CDC ACM device is configured in the devicetree as well. To ensure that USB logging properly works with custom board definitions, two sections of the <board>.dts file need updating.

Underneath the USB device, add the CDC ACM node:

&usbd {
status = "okay";
cdc_acm_uart: cdc_acm_uart {
compatible = "zephyr,cdc-acm-uart";
};
};

Then, an additional chosen node (near the top of the file) will mark the CDC ACM device as the console:

/ {
chosen {
...
zephyr,console = &cdc_acm_uart;
};
...
};

UF2 Builds

Previously, to get ZMK to build a UF2 image to flash to a given board required adding a CMakeLists.txt file that added a custom post build command. Now, the only thing necessary to have Zephyr build a UF2 is to add the following to your <board>_defconfig file:

CONFIG_BUILD_OUTPUT_UF2=y

If updating an existing board, be sure to remove the previous CMakeLists.txt file to avoid generating the UF2 twice during a west build.

For more details on the implementation, see zephyr#31066.

STM32 Clock Configuration

Clock configuration moved to devicetree as well, out of the Kconfig files. Here is a sample config for a board that uses the HSI for the PLL source:

&clk_hsi {
status = "okay";
};

&pll {
prediv = <1>;
mul = <12>;
clocks = <&clk_hsi>;
status = "okay";
};

&rcc {
clocks = <&pll>;
clock-frequency = <DT_FREQ_M(72)>;
ahb-prescaler = <1>;
apb1-prescaler = <2>;
};

After adding the nodes, be sure to remove the clock/PLL related configuration from the <board>_defconfig file.

Seeeduino XIAO

The Seeed(uino) XIAO has gained in popularity for use on smaller boards, and gained more traction with the release of the new XIAO BLE board, powered by the popular nRF52840 SoC. As part of the 3.0 update, we've also more fully integrated the XIAO and XIAO BLE to make it easier to build keyboard (shields) using either controller.

Future Hardware

One of the exciting items that's one step closer as part of this work is support for Raspberry Pi Pico/RP2040. With Zephyr 3.0 merged, this start the process for getting those controllers/chips supported by ZMK. Follow the issue to keep track of progress. This will also enable us to support the XIAO compatible Adafruit Qt Py RP2040 and XIAO RP2040.

Thanks!

Thanks to all the testers who have helped verify ZMK functionality on the newer Zephyr version.

Article Updates

  • 12/2023: Removed the deprecated label property from code snippets.