QMK 固件 Tri Layer 功能详解:TL_LOWR/TL_UPPR 键码实现与三层配置实践
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
本文以 QMK 固件的 Tri Layers 功能文档 为主体,系统讲解 OLKB 风格的 "Tri Layer"(三层)键码机制:如何启用该功能、TL_LOWR/TL_UPPR键码的行为规则、三个层号的默认配置与修改方式,并结合 quantum/tri_layer.c、quantum/process_keycode/process_tri_layer.c 与 quantum/action_layer.c 的源码,剖析 "按下两个键激活第三层" 这一行为在固件内部是如何被判定和执行的。读完后,你不仅能正确配置和使用该功能,还能理解其位运算判定逻辑以及为何层设置不具备掉电保持性。
功能概述:Tri Layer 是什么
QMK 的 Tri Layer 功能支持 OLKB(OLeary 键盘风格)的 "Tri Layer" 键码。其工作方式类似MO(momentary,瞬时层切换)功能键:按住某个键时打开某一层,松开后关闭。不同的是,Tri Layer 提供了两个功能键——"Lower"(下层)键和 "Upper"(上层)键,当两个键同时被按住时,会额外激活第三个 "Adjust"(调整)层。
典型应用场景:在调整层中放置灯光、音效、EEPROM 写入等低频但重要的管理功能,用户无需专门记忆一个独立的 Adjust 层切换键,而是通过 "Lower + Upper 同时按下" 这一直觉化的组合触达。
启用方式
在键盘的rules.mk中添加以下配置即可启用:
TRI_LAYER_ENABLE = yes该选项是 QMK 通用特性开关之一,由构建系统 builddefs/common_features.mk 统一处理,并在 builddefs/show_options.mk 中登记为可展示的特性选项。启用后,相关键码与处理逻辑才会在编译时进入固件(源码中大量使用#ifdef TRI_LAYER_ENABLE进行条件编译,例如 quantum/action_tapping.c 中对 tap-hold 计时逻辑的特判)。
层号取值规则
需要明确两点(来自原文档):
- "upper"、"lower"、"adjust" 这三个名称没有任何特殊技术含义,仅仅是用于标识和澄清行为的命名;
- 层的处理顺序是从数值最大到最小,但这些层号不要求连续。例如 lower 为 1、upper 为 5、adjust 为 8 是完全合法的配置。
关于层栈(layer stack)本身的完整工作机制(瞬时层、叠加关系、层优先级等),可进一步参考 Keymap Overview 的 Keymap and Layers 章节。
键码定义:TL_LOWR 与 TL_UPPR
Tri Layer 功能提供两个键码:
| 键码 | 别名 | 行为说明 |
|---|---|---|
QK_TRI_LAYER_LOWER | TL_LOWR | 瞬时打开 "lower" 层;若此时 "upper" 层也已打开,则同时打开 "adjust" 层 |
QK_TRI_LAYER_UPPER | TL_UPPR | 瞬时打开 "upper" 层;若此时 "lower" 层也已打开,则同时打开 "adjust" 层 |
两个键码互为镜像:无论先按哪个、后按哪个,只要两个键同时处于按下状态,adjust 层就会被激活。
从源码看,键码数值在 quantum/keycodes.h 中定义:
QK_TRI_LAYER_LOWER = 0x7C77, QK_TRI_LAYER_UPPER = 0x7C78,别名TL_LOWR、TL_UPPR在同一文件的别名段(quantum/keycodes.h)中直接映射到上述常量,因此在 keymap 中直接使用TL_LOWR即可。同时,键码字符串模块 quantum/keycode_string.c 已注册了这两个键码的显示名,用于 OLED/LCD 等界面的键码回显。
源码行为剖析:process_tri_layer
这两个键码的实际处理逻辑位于 quantum/process_keycode/process_tri_layer.c,其核心流程非常清晰:
case QK_TRI_LAYER_LOWER: if (record->event.pressed) { layer_on(get_tri_layer_lower_layer()); // 按下:打开 lower 层 update_tri_layer(get_tri_layer_lower_layer(), get_tri_layer_upper_layer(), get_tri_layer_adjust_layer()); // 重新判定 adjust 层 } else { layer_off(get_tri_layer_lower_layer()); // 松开:关闭 lower 层 update_tri_layer(...); // 重新判定 adjust 层 } return false;QK_TRI_LAYER_UPPER的分支与之对称。这里有两点值得注意:
- 按下和松开都会调用
update_tri_layer()。也就是说,松开任意一个键时,固件会重新检查 lower/upper 是否仍同时处于激活状态,若不再满足条件,adjust 层会随之关闭。这保证了组合状态变化的两个方向都能正确联动; - 层号来自运行时可查询的变量(
get_tri_layer_*_layer()),而非编译期常量,这正是后面 "运行时修改层号" 功能的基础。
adjust 层的判定逻辑:一次位运算
真正决定 "adjust 层是否开启" 的是 quantum/action_layer.c 中的update_tri_layer_state():
layer_state_t update_tri_layer_state(layer_state_t state, uint8_t layer1, uint8_t layer2, uint8_t layer3) { layer_state_t mask12 = ((layer_state_t)1 << layer1) | ((layer_state_t)1 << layer2); layer_state_t mask3 = (layer_state_t)1 << layer3; return (state & mask12) == mask12 ? (state | mask3) : (state & ~mask3); }其语义可以直白地翻译为:把整个层状态state(一个位图,每一位代表一个层)与 "lower 位 | upper 位" 的掩码做与运算,若结果恰好等于掩码本身(即两个位全部为 1,说明 lower 和 upper 层都激活),就把 adjust 层的位置 1;否则把 adjust 层的位置 0。随后update_tri_layer()将新状态写回全局层状态并触发层状态更新。
从源码结构看,这种实现有两个工程上的好处:
- 无状态判定:判定完全基于当前层状态位图,不需要额外的标志位记录 "谁按下了谁",因此天然支持两个键的任意先后顺序、甚至与 OS 层、TO 层等其他层机制叠加使用;
- 非破坏性:函数只对 adjust 层对应的位做置位/清零,不改动位图中的其他层,用户其他手动开启的层不受影响。
配置项:自定义三个层号
三个层号均有编译期默认值,可在键盘的config.h中通过宏覆盖:
| 配置名 | 默认值 | 说明 |
|---|---|---|
TRI_LAYER_LOWER_LAYER | 1 | 设置 "lower" 层的默认层号 |
TRI_LAYER_UPPER_LAYER | 2 | 设置 "upper" 层的默认层号 |
TRI_LAYER_ADJUST_LAYER | 3 | 设置 "adjust" 层的默认层号 |
例如,若希望把 "Adjust" 层设置为第 5 层,在config.h中加入:
#define TRI_LAYER_ADJUST_LAYER 5这些默认值在源码中的落点是 quantum/tri_layer.h:
#ifndef TRI_LAYER_LOWER_LAYER # define TRI_LAYER_LOWER_LAYER 1 #endif #ifndef TRI_LAYER_UPPER_LAYER # define TRI_LAYER_UPPER_LAYER 2 #endif #ifndef TRI_LAYER_ADJUST_LAYER # define TRI_LAYER_ADJUST_LAYER 3 #endif即:config.h中定义了对应宏就使用用户值,否则回退到 1/2/3。这三个宏随后被 quantum/tri_layer.c 用作三个静态运行时变量的初值:
static uint8_t tri_layer_lower_layer = TRI_LAYER_LOWER_LAYER; static uint8_t tri_layer_upper_layer = TRI_LAYER_UPPER_LAYER; static uint8_t tri_layer_adjust_layer = TRI_LAYER_ADJUST_LAYER;这解释了配置模型的分层设计:config.h宏只决定"出厂默认值",运行时实际生效的值由下方这组 setter/getter 管理。
运行时函数接口:动态切换层号
Tri Layer 模块对外暴露一组 set/get 函数(声明见 quantum/tri_layer.h,实现在 quantum/tri_layer.c):
| 函数名 | 功能 |
|---|---|
set_tri_layer_lower_layer(layer) | 运行时修改 "lower" 层号* |
set_tri_layer_upper_layer(layer) | 运行时修改 "upper" 层号* |
set_tri_layer_adjust_layer(layer) | 运行时修改 "adjust" 层号* |
set_tri_layer_layers(lower, upper, adjust) | 一次性设置三个层号* |
get_tri_layer_lower_layer() | 读取当前 "lower" 层号 |
get_tri_layer_upper_layer() | 读取当前 "upper" 层号 |
get_tri_layer_adjust_layer() | 读取当前 "adjust" 层号 |
注意:这些运行时设置不持久化——断电或控制器重启后会恢复为
config.h中定义的默认值。这一点与源码实现严格对应:三个层号保存在static uint8_t变量中(quantum/tri_layer.c),并未写入 EEPROM,因此复位后自然回到宏初值。
一个典型的实战用法是:在 keymap 中预留几组自定义键,配合set_tri_layer_layers()让不同工作模式(如游戏模式、编程模式)使用不同的 lower/upper/adjust 层组合,而TL_LOWR/TL_UPPR两个物理键的位置和行为保持不变。由于process_tri_layer()每次都实时读取 getter 的返回值,切换层号后下一次按键即刻生效,无需重新编译。
与 Tap-Hold 等机制的交互细节
Tri Layer 键虽然是"按住生效"的 hold 型行为,但它并不占用 QMK tap-hold 状态机。从源码结构看,在 flow tap(流式敲击判定)路径中,固件显式忽略了 Tri Layer 键的释放事件(quantum/action_tapping.c):
#ifdef TRI_LAYER_ENABLE // Ignore Tri Layer keys. case QK_TRI_LAYER_LOWER: case QK_TRI_LAYER_UPPER: #endif // TRI_LAYER_ENABLE return;这意味着在开启 flow tap / 推测式 hold 判定的键盘上,Tri Layer 键的按下/释放不会被当作 "上一个敲击" 来影响相邻 tap-hold 键的判定窗口。同理,quantum/process_keycode/process_repeat_key.c 将两个 Tri Layer 键码标记为 hold 型键码,使其与 Caps Word、repeat 键等依赖键码分类的模块正确协同。如果你在编写或审计 keymap 时遇到 "Tri Layer 键与 MT 键相邻出现行为异常" 一类的问题,这些分类宏(MODIFIER_KEYCODE_RANGE、QK_MOMENTARY ...等 switch 分支)是排查的第一站。
此外,quantum/process_keycode/process_caps_word.c 也把QK_TRI_LAYER_LOWER ... QK_TRI_LAYER_UPPER区间纳入了其键码类型判断,说明该键码区间被整体视为 "非字符类" 功能键——这与0x7C77/0x7C78这两个键码值紧邻分配、区间连续的设计意图一致。
测试验证
仓库为 Tri Layer 功能提供了专门的单元测试目录 tests/tri_layer/test_tri_layer.cpp,通过模拟按键序列验证层状态转换。在 QMK 中,这类功能测试通常以 quantum/keycodes.h 中定义的键码驱动虚拟矩阵事件,断言layer_state的变化,是验证 "按下 Lower 后 Adjust 未激活、再按下 Upper 后 Adjust 激活、松开任一后 Adjust 关闭" 这类时序逻辑的可靠手段。如果你修改了与层状态相关的代码(如update_tri_layer_state),建议运行该测试回归验证,确保位运算判定未被破坏。
小结
- 启用:
rules.mk中加TRI_LAYER_ENABLE = yes; - 使用:keymap 中放置
TL_LOWR(=QK_TRI_LAYER_LOWER,0x7C77)与TL_UPPR(=QK_TRI_LAYER_UPPER,0x7C78),双键同按即激活 adjust 层; - 配置:
config.h中用TRI_LAYER_LOWER_LAYER(默认 1)、TRI_LAYER_UPPER_LAYER(默认 2)、TRI_LAYER_ADJUST_LAYER(默认 3)设定默认层号,层号无需连续; - 扩展:
set_tri_layer_*系列函数支持运行时改层号,但不掉电保持; - 原理:
process_tri_layer()在按下/松开时开关对应层并调用update_tri_layer(),后者用一次位图掩码运算判定 lower、upper 是否同时激活,从而置位或清除 adjust 层——整个机制无额外状态、可与其他层机制自由叠加。
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考