☰
ZMK Devicetree 全面指南:从声明式树结构到键位映射的底层原理
2026/10/5 2:24:45 网站建设 项目流程
  • 固件
  • 嵌入式
  • 智能硬件
  • 蓝牙

【免费下载链接】zmk

ZMK Firmware Repository

项目地址:https://gitcode.com/gh_mirrors/zm/zmk
点击查看免费下载

本文是 ZMK 固件中 Devicetree 机制的入门与进阶指南,面向 ZMK 用户与键盘设计师。文章以一段真实的键位映射(keymap)代码为主线,系统讲解 Devicetree 的节点结构、属性类型、compatible与status属性、标签(label)与 phandle 引用机制,以及 C 预处理器与 Devicetree 合并/覆盖/删除等预处理流程,并结合 ZMK 源码 与 binding 文件 揭示底层实现。读完本文,你将能读懂并编写自己的 keymap 与 overlay 文件,理解行为(behavior)节点如何被实例化与调用,并能通过检查最终生成的 devicetree 来排查配置问题。

什么是 Devicetree,为什么 ZMK 重度依赖它

ZMK 对一种名为devicetree的树形数据结构进行了重度使用。Devicetree 是一种声明式(declarative)地描述 Zephyr 设备几乎所有信息的方式——从键位映射(keymap)的定义、行为(behavior)的配置,一直到内部存储分区以及板载 MCU 的架构。

对 ZMK 用户和设计者而言,devicetree 是日常与固件打交道的最主要入口:你写的keymap、.overlay文件本质上都是 devicetree 源文件的一部分。本文是面向 ZMK 用户与设计者的 devicetree 入门介绍;如需进一步深入,可参考 devicetree 规范 与 Zephyr 官方文档。

贯穿全文的运行示例

本文将以一段取自 keymap 的代码作为贯穿全文的运行示例:

#include <dt-bindings/zmk/keys.h> #include <behaviors.dtsi> / { behaviors { spc_ul: space_underscore { compatible = "zmk,behavior-mod-morph"; #binding-cells = <0>; bindings = <&kp SPACE>, <&kp UNDERSCORE>; mods = <(MOD_LSFT|MOD_RSFT)>; }; }; keymap { compatible = "zmk,keymap"; default_layer { bindings = <&spc_ul &kp Z &kp M &kp K>; }; }; };

建议你在阅读下文时将此示例放在手边对照。另外需要注意:Devicetree 使用 C 风格的注释,即// ...表示行注释,/* ... */表示块注释。

Devicetree 的树形结构

一个 devicetree 节点(node)的通用结构如下([]内的部分为可选):

[label:] name { [properties] [child nodes] };

devicetree 的根节点(root node)名称恒为/,写作:

/ { [child nodes] };

根节点也是唯一一个名称中带有/字符的节点。节点名称允许的字符请查阅 devicetree 规范。

经过各种预处理步骤之后,devicetree 的全部内容都会位于根节点之下或其内部。若一个节点位于另一个节点之内,则称前者是后者的子节点(child node),并可以依此类推地谈论孙节点(grandchild node)等。

在运行示例中,behaviors与keymap是根节点的子节点;space_underscore是behaviors的子节点、default_layer是keymap的子节点,因此二者都是根节点的孙节点。

属性(Properties)与常用属性类型

一个节点可以拥有哪些属性,差异极大。在标准属性中,有两个对用户与设计者特别重要:compatible和status。其他标准属性可查阅 devicetree 规范。

下面是使用 ZMK 时最常遇到的属性类型。Zephyr 的 Devicetree bindings 文档 提供了更详细的信息与完整类型列表。

bool

取值为真或假。若要将属性设为真,直接列出它且不写值;设为假则直接不列出它。

示例:property;

如果某个属性已被设为真、而你希望覆盖为假,可用如下指令删除既有属性:

/delete-property/ the-property-name;
int

单个整数,用尖括号包裹,支持数学表达式。

示例:property = <42>;

string

用双引号包裹的文本。

示例:property = "foo";

array

由空格分隔的整数列表,用尖括号包裹。数学表达式可用,但必须用圆括号包裹。

示例:property = <1 2 3 4>;

值还可以拆分成多个块,例如property = <1 2>, <3 4>;。

phandle

单个节点引用,用尖括号包裹。phandle 将在后文的 标签与 Phandle 一节详细解释。

示例:property = <&label>

phandles

节点引用列表,用尖括号包裹。phandle 的详细解释见 标签与 Phandle。

示例:property = <&label1 &label2 &label3>

phandle array

节点引用以及可能附加的数字(参数)列表,用尖括号包裹。数学表达式可用但需用圆括号包裹。phandle 的详细解释见 标签与 Phandle。

示例:property = <&none &mo 1>;

值也可以拆分为多个块,例如property = <&none>, <&mo 1>;。关于参数如何与节点关联的更多细节,可参考 Zephyr Devicetree bindings 文档中对 "phandle-array" 的说明。

GPIO array

这本质上就是一个 phandle array。文档将其单列为一种类型,是为了明确哪些属性期望接收一组 GPIO。

数组中的每一项都应是一个 GPIO 节点的标签(各硬件平台的 GPIO 节点名称不同),后跟索引与配置标志。完整标志列表见 Zephyr 的 GPIO 文档。phandle 与标签将在 标签与 Phandle 中详述。

示例:

some-gpios = <&gpio0 0 GPIO_ACTIVE_HIGH>, <&gpio0 1 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)> ;
path

指向某节点的路径,可以是节点引用,也可以是字符串。详见 标签与 Phandle。

示例:

property = &label; property = "/path/to/some/node";

compatible:节点与代码的桥梁

一个节点最重要的属性通常是compatible属性。该属性用于把代码映射到节点。存在一些特例,例如名为chosen的节点——它通过节点名而非compatible属性来识别。

在运行示例中,space_underscore节点的属性为compatible = "zmk,behavior-mod-morph";。ZMK 的 mod-morph 行为代码 会作用于所有compatible为该值的节点;ZMK 的 keymap 代码 对compatible = "zmk,keymap";的处理方式同理。在 keymap.c 中,如果编译时找不到任何zmk,keymap节点,构建会直接以#error报错,提醒开发者检查 keymap 是否可用、compatible是否设置正确。

compatible属性也用来标识一个节点可能具有哪些额外属性。任何不属于标准属性的属性,都必须列在 "devicetree bindings" 文件中。这些文件有时还会包含节点用法的补充信息。

ZMK 将所有 devicetree bindings 存放在app/dts/bindings目录下。例如compatible = "zmk,behavior-mod-morph";的 bindings 文件是app/dts/bindings/behaviors/zmk,behavior-mod-morph.yaml,内容如下:

# Copyright (c) 2020 The ZMK Contributors # SPDX-License-Identifier: MIT description: Mod Morph Behavior compatible: "zmk,behavior-mod-morph" include: zero_param.yaml properties: bindings: type: phandle-array required: true mods: type: int required: true keep-mods: type: int required: false

节点可拥有的属性列在properties之下;部分附加属性从zero_param.yaml导入。bindings 文件是节点属性的权威来源——ZMK 在 行为配置文档 中有时会省略类似#binding-cells这样的属性(它从前述文件中导入,描述了该行为接受的参数个数)。bindings 文件语法的完整说明见 Zephyr 官方文档。

从源码实现看,bindings 文件中的每个属性都会在 C 侧被 DT 宏逐一读取:在 behavior_mod_morph.c 中,mods属性通过DT_INST_PROP(n, mods)读入配置结构,keep-mods则通过DT_INST_NODE_HAS_PROP判断是否存在,并据此计算“被掩码的修饰键”masked_mods;bindings属性中的两个行为引用则通过DT_INST_PHANDLE_BY_IDX依次解析为normal_binding(索引 0)与morph_binding(索引 1)。这一对应关系正是“bindings 文件是属性权威”的底层证据。

bindings 文件还可以为子节点指定属性,例如zmk,keymap.yaml就为 keymap 中的每个 layer 定义了属性:

description: | Allows defining a keymap composed of multiple layers compatible: "zmk,keymap" child-binding: description: "A layer to be used in a keymap" properties: display-name: type: string required: false description: The name of this layer to show on displays bindings: type: phandle-array required: true sensor-bindings: type: phandle-array required: false label: type: string required: false deprecated: true description: Deprecated. Use "name" instead.

可以看到,每个 layer 节点的bindings是phandle-array类型且必须提供;sensor-bindings用于编码器(encoder)等传感器行为;display-name用于在屏幕上显示层名,旧的label属性已被标记为 deprecated。这解释了运行示例中default_layer节点为什么必须包含bindings属性。

status:节点的开关

status属性描述节点的状态。对 ZMK 用户与设计者而言,只有两个相关的取值:

  • status = "disabled";:节点被禁用。代码不应生效或使用该节点,但树的其余部分仍可引用它。
  • status = "okay";:未显式声明时的默认状态。节点被视为 "active"(激活)。该属性一般只在覆盖某个status = "disabled";时才会显式写出。

这一属性在实践中的具体用法,在阅读完下文 Devicetree 预处理 之后会更加清晰。

标签与 Phandle

除了名称(name)之外,节点还可以拥有标签(label)。对 ZMK 用户/设计者而言,标签往往比节点名更重要:节点名用于在代码中访问单个节点,而标签用于在 devicetree 内部引用其他节点。这种引用被称为phandle,可以把它类比为 C 语言中的指针。

在运行示例中,spc_ul是节点space_underscore的标签。default_layer节点的bindings属性是一个 "phandle-array"——一组对其他节点的引用。其第一个元素是&spc_ul,即指向标签为spc_ul(也就是space_underscore)的节点的 phandle。&kp是另一个 phandle 的例子,它指向一个如下定义的节点:

/ { behaviors { kp: key_press { compatible = "zmk,behavior-key-press"; #binding-cells = <1>; display-name = "Key Press"; }; }; };

该节点从别的文件导入——导入机制将在后文讨论。运行示例中的&kpphandle 还展示了参数(parameters)随 phandle 一起传递的概念:本例中Z、M、K被作为参数传入。

当 ZMK 需要触发 keymap 的binding属性中某个位置对应的行为时,它会使用 phandle 识别出需要调用的行为节点,然后执行由该节点compatible属性决定的代码,并在执行时传入参数。根据行为的不同,可能还需要继续触发另一个行为 phandle,此时会以相同流程再次定位节点并执行相应代码。

本质上,keymap 中的每个 layer 都是一组指向各行为(附带参数)的 phandle 数组,而这些行为节点是在别处定义的。如果你不需要自己定义某个行为节点,那通常意味着 ZMK 已经为你定义好了——例如kp、mt、mo等内置行为均来自 app/dts/behaviors 下的一系列.dtsi文件,并通过 app/dts/behaviors.dtsi 集中#include进任何 keymap。

注 1:phandle array 按定义还包含元数据,即参数。严格来说,不带元数据的 phandle 列表其类型是phandles而非phandle-array;只含单个 phandle 的属性类型为phandle。

注 2:传递给行为代码的参数个数(以及跳过它们以找到下一个行为 phandle 的步长)由上文提到的#binding-cells属性决定。例如key_press的#binding-cells = <1>,意味着&kp Z中Z是一个参数;而运行示例中自定义的space_underscore是#binding-cells = <0>,因此&spc_ul不接受任何参数。

Devicetree 预处理

dts文件中很大一部分复杂性来自预处理。所有预处理完成后的最终 devicetree 可以在本地构建或 GitHub Actions 构建时检查。由于下文将要解释的原因,你的 keymap 和大部分自定义内容会出现在该文件接近末尾的位置。

预处理有两个来源:

  1. C 预处理器 可用于 Devicetree Source(dts)文件内。
  2. Devicetree 自有一套合并、覆盖乃至删除节点和属性的机制。

C 预处理器

C 预处理器的完整介绍超出了本文范围,不熟悉它的读者可以参考网上的大量资料。

不过,C 预处理器在 ZMK devicetree 文件中的一些特定用法很有价值,能帮助你理解各部分如何拼接到一起。

C 预处理器用于从其他文件导入一些节点及其他预处理器定义。运行示例顶部的这两行:

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

导入了 ZMK 的默认行为节点定义,以及一组预处理器定义。示例中传给&kpphandle 的参数Z、M、K实际上都是 C 预处理器宏。例如在预处理阶段,Z会被替换成数字0x07001D——这个数字被传给 ZMK 宿主设备(如你的电脑),由它重新解释为字母 "z"。在 app/include/dt-bindings/zmk/keys.h 中可以找到#define Z (ZMK_HID_USAGE(HID_USAGE_KEY, HID_USAGE_KEY_KEYBOARD_Z))这样的定义——ZMK_HID_USAGE将 HID 用途页与用途 ID 组合成单个数值,这正是0x07001D(键盘页0x07、字母 z 的用途 ID)的由来。

C 预处理器还常被 ZMK 高级用户用来减少 keymap 文件中的重复。一个例子是宏行为(macro behavior)的便捷 C 宏。ZMK 设计者还会遇到用于矩阵变换的RC宏,并会使用GPIO_ACTIVE_HIGH之类的便捷定义。

Devicetree 处理(多文件合并)

一棵 devicetree 几乎总是由多个文件构建而成。这些文件通常包括:

  • .dtsi文件:专为通过 C 预处理器包含而存在(其内容会在#include位置被“粘贴”),构建系统不会以其他方式使用它们。
  • 一个.dts文件:构成 devicetree 的“基座”。构建一棵 devicetree 时始终有且仅有一个这样的文件。对 ZMK 而言,.dts文件包含描述板子(board)的 devicetree 段落,其中会导入多个描述该板所用 SoC 的.dtsi文件。仓库中的实际例子可参见 app/boards 目录下各板子的.dts文件。
  • 任意数量的.overlay文件:这些文件可以来自各种来源,例如护盾(shield)或 Zephyr 的 snippets。overlay 作用于.dts文件的方式是将其内容追加到.dts文件末尾,即放在文件底部。多个 overlay 会按特定顺序重复追加。在不深入讨论 overlay 应用精确顺序的前提下,只需知道:如果你在build.yaml中写了例如shield: corne_left nice_view_adapter nice_view,那么 overlay 会从左到右依次应用。
  • 一个.keymap文件:包含该文件是 ZMK 特有的机制,它被视为“最终”的.overlay文件,追加在所有其他 overlay 之后。
节点的合并与覆盖

当一个节点在 devicetree 中出现多次(在各文件被导入并合并之后),它会作为预处理步骤被合并为单个节点。例如:

/ { mn_ex: my_example_node { property1 = <0>; property2 = <2>; }; }; / { my_example_node { property2 = <1>; property3 = <4>; }; example2 { property; }; };

第二次出现的my_example_node拥有更高优先级,因此其property2的值会覆盖第一次出现的值。两个根节点也会在这个过程中被合并。处理后的树为:

/ { mn_ex: my_example_node { property1 = <0>; property2 = <1>; property3 = <4>; }; example2 { property; }; };

标签不会被覆盖;一个节点可以有多个标签。phandle 也可以用来覆盖或添加属性:

&mn_ex { property4 = <2>; };

phandle 方式是推荐的做法,因为这种方式不需要知道所有父节点的确切名称。关键点在于:使用 phandle 覆盖或添加属性时,该 phandle 不能位于根节点内部,而应完全放在树之外。

Devicetree 特殊指令

Devicetree 有一些影响整棵树的特殊指令,与本文相关的有:

  • 节点可通过/delete-node/指令删除:在根节点之外写/delete-node/ &node_label;。
  • 属性可通过/delete-property/指令删除:在相关节点内部写/delete-property/ node-property;。
  • /omit-if-no-ref/使某个节点在没有任何引用/phandle 指向它时,从最终 devicetree 中省略:写/omit-if-no-ref/ &node_label;。

/omit-if-no-ref/在 ZMK 中有实际应用:内置行为节点定义大多带上了这一指令。例如 app/dts/behaviors/key_press.dtsi 中的kp节点,在启用ZMK_BEHAVIOR_OMIT(KP)时会被标记为/omit-if-no-ref/——如果你的 keymap 从未引用&kp,该节点就不会进入最终固件,从而节省资源。

如何验证你的 devicetree 配置

预处理机制意味着最终生效的 devicetree 可能与你写下的多个文件都不完全相同。ZMK 的构建问题排查文档介绍了两种验证方式:

  • 构建日志中会列出构建过程发现并使用的 devicetree 文件(包括各个.overlay与最终的.keymap);
  • 本地构建时,合并了板子、shield 与用户所有 devicetree 文件的最终结果位于构建目录下的<build_folder>/zephyr/zephyr.dts,可直接查看。

检查这个文件时,你会发现自己的 keymap 与自定义内容出现在文件接近底部的位置——这正是因为.keymap作为“最终的 overlay”被追加在所有内容之后。当 devicetree 报错时,构建问题排查 中常见错误的定位方法也很有帮助:例如parse error往往对应 keymap 文件具体行号的语法问题(如缺少分号),而lacks #binding-cells则提示某处对行为的参数使用不当。

小结

Devicetree 是 ZMK 配置体系的基石:keymap 中的每个 layer 都是指向行为节点的 phandle 数组,行为节点通过compatible与 bindings 文件建立“属性 → 代码”的映射,C 预处理器负责宏展开与文件拼接,而 Devicetree 自身的合并/覆盖/删除规则决定了多文件最终如何融合。掌握节点、属性类型、label/phandle 与预处理这四块知识,你就能读懂并自信地编写自己的 keymap、overlay 与行为配置,也能在遇到构建错误时通过检查<build_folder>/zephyr/zephyr.dts快速定位问题。

  • 固件
  • 嵌入式
  • 智能硬件
  • 蓝牙

【免费下载链接】zmk

ZMK Firmware Repository

项目地址:https://gitcode.com/gh_mirrors/zm/zmk
点击查看免费下载

相关推荐

上一篇:Nuitka 标准插件(Standard Plugins)完全指南:插件机制、命令行参数与独立打包实战
下一篇:在 Remotion 中播放 GIF / APNG / AVIF / WebP 动画图片:`<AnimatedImage>` 与 `<Gif>` 组件完全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询