- 固件
- 嵌入式
- 智能硬件
- 蓝牙
【免费下载链接】zmk
ZMK Firmware Repository
本文是 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 和大部分自定义内容会出现在该文件接近末尾的位置。
预处理有两个来源:
- C 预处理器 可用于 Devicetree Source(
dts)文件内。 - 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
相关推荐
Sway 智能合约 StorageMap 存储映射完全指南:从声明、读写到嵌套与底层槽位原理
Sway 智能合约 StorageMap 存储映射完全指南:从声明、读写到嵌套与底层槽位原理 导读 StorageMap<K, V 是 Sway 标准库提供的持
编程语言编译器区块链npx skills 交互式安装:答对三个问题,AI 技能就位
npx skills 交互式安装:答对三个问题,AI 技能就位 你打开终端,想给编码代理装个 AI 技能,光标停在命令行上。npx skills 的交互式安装模
固件嵌入式智能硬件蓝牙Mac Mouse Fix 侧键映射上手:3 步配置让普通鼠标拥有触控板手感
Mac Mouse Fix 侧键映射上手:3 步配置让普通鼠标拥有触控板手感 滚轮一格一格跳、侧键常年吃灰?Mac Mouse Fix 就是干这个的:把几十块钱
桌面应用系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考