
固件嵌入式智能硬件蓝牙【免费下载链接】zmkZMK 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 labelphandles节点引用列表用尖括号包裹。phandle 的详细解释见 标签与 Phandle。示例property label1 label2 label3phandle 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_modsbindings属性中的两个行为引用则通过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。注 1phandle 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 Sourcedts文件内。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快速定位问题。赞分享固件嵌入式智能硬件蓝牙【免费下载链接】zmkZMK Firmware Repository项目地址https://gitcode.com/gh_mirrors/zm/zmk点击查看免费下载相关推荐Sway 智能合约 StorageMap 存储映射完全指南从声明、读写到嵌套与底层槽位原理Sway 智能合约 StorageMap 存储映射完全指南从声明、读写到嵌套与底层槽位原理 导读 StorageMapK, V 是 Sway 标准库提供的持编程语言编译器区块链npx skills 交互式安装答对三个问题AI 技能就位npx skills 交互式安装答对三个问题AI 技能就位 你打开终端想给编码代理装个 AI 技能光标停在命令行上。npx skills 的交互式安装模固件嵌入式智能硬件蓝牙Mac Mouse Fix 侧键映射上手3 步配置让普通鼠标拥有触控板手感Mac Mouse Fix 侧键映射上手3 步配置让普通鼠标拥有触控板手感 滚轮一格一格跳、侧键常年吃灰Mac Mouse Fix 就是干这个的把几十块钱桌面应用系统编程上一篇Nuitka 标准插件Standard Plugins完全指南插件机制、命令行参数与独立打包实战下一篇在 Remotion 中播放 GIF / APNG / AVIF / WebP 动画图片AnimatedImage 与 Gif 组件完全指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考