QMK 固件 US ANSI Shifted 符号键码:KC_TILDE 等 Shift 符号快捷键的实现原理与使用限制
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
QMK 固件为 US ANSI 布局提供了一组 "Shifted 符号" 键码(如KC_TILDE、KC_EXCLAIM、KC_AT),它们本身不是独立的 HID 按键,而是LSFT(kc)的语法糖——按下时自动同时发送左 Shift 与对应的基础键码,让键帽上的符号可以直接写入键位定义。本文基于仓库中的 US ANSI Shifted 符号键码文档 与 keymap_us.h 源码,完整梳理这 22 个键码及其别名、宏展开机制、底层注册流程,以及 Mod-Tap/Layer-Tap 中不可用、Windows 远程桌面丢字符这两个重要限制的原因与解决办法。
一、定位:这些键码"不是真正的键码"
文档首先给出的关键结论是:这些键码对应的是标准 US ANSI 键盘上需要 "shift" 才能打出的字符。它们没有自己的 HID 键码(US ANSI 布局中~!@#$%^&*()_+{}|:"<>?在底层就是基础键加 Shift),QMK 只是为它们提供了更直观的宏名,作为LSFT(kc)的快捷方式。
因此使用KC_EXCLAIM时,固件实际发送的是"左 Shift +1"这一组合,而不是某个独立的 "!" 键码。这个语义差异正是后文所有限制(Mod-Tap 中失效、远程桌面丢字)的根源。
在源码层面,这组键码统一定义在 keymap_us.h 中,全部展开为S(...)宏:
// quantum/keymap_extras/keymap_us.h(节选) #define KC_TILD S(KC_GRAVE) // ~ #define KC_EXLM S(KC_1) // ! #define KC_AT S(KC_2) // @ #define KC_HASH S(KC_3) // # #define KC_DLR S(KC_4) // $ #define KC_PERC S(KC_5) // % #define KC_CIRC S(KC_6) // ^ #define KC_AMPR S(KC_7) // & #define KC_ASTR S(KC_8) // * #define KC_LPRN S(KC_9) // ( #define KC_RPRN S(KC_0) // ) #define KC_UNDS S(KC_MINUS) // _ #define KC_PLUS S(KC_EQUAL) // + #define KC_LCBR S(KC_LEFT_BRACKET) // { #define KC_RCBR S(KC_RIGHT_BRACKET) // } #define KC_PIPE S(KC_BACKSLASH) // | #define KC_COLN S(KC_SEMICOLON) // : #define KC_DQUO S(KC_QUOTE) // " #define KC_LABK S(KC_COMMA) // < #define KC_RABK S(KC_DOT) // > #define KC_QUES S(KC_SLASH) // ?二、完整键码与别名对照表
以下是 原文档 提供的完整键码表。QMK 同时提供两套命名风格:短别名(历史遗留,如KC_TILD、KC_EXLM)和全拼名(如KC_TILDE、KC_EXCLAIM)。两者在 keymap_us.h 中通过#define相互绑定,例如#define KC_TILDE KC_TILD,因此在 keymap 中混用完全等价。
| 键码(全拼名) | 别名 | 对应符号 | 等价于 |
|---|---|---|---|
KC_TILDE | KC_TILD | ~ | S(KC_GRAVE) |
KC_EXCLAIM | KC_EXLM | ! | S(KC_1) |
KC_AT | — | @ | S(KC_2) |
KC_HASH | — | # | S(KC_3) |
KC_DOLLAR | KC_DLR | $ | S(KC_4) |
KC_PERCENT | KC_PERC | % | S(KC_5) |
KC_CIRCUMFLEX | KC_CIRC | ^ | S(KC_6) |
KC_AMPERSAND | KC_AMPR | & | S(KC_7) |
KC_ASTERISK | KC_ASTR | * | S(KC_8) |
KC_LEFT_PAREN | KC_LPRN | ( | S(KC_9) |
KC_RIGHT_PAREN | KC_RPRN | ) | S(KC_0) |
KC_UNDERSCORE | KC_UNDS | _ | S(KC_MINUS) |
KC_PLUS | — | + | S(KC_EQUAL) |
KC_LEFT_CURLY_BRACE | KC_LCBR | { | S(KC_LEFT_BRACKET) |
KC_RIGHT_CURLY_BRACE | KC_RCBR | } | S(KC_RIGHT_BRACKET) |
KC_PIPE | — | \| | S(KC_BACKSLASH) |
KC_COLON | KC_COLN | : | S(KC_SEMICOLON) |
KC_DOUBLE_QUOTE | KC_DQUO、KC_DQT | " | S(KC_QUOTE) |
KC_LEFT_ANGLE_BRACKET | KC_LABK、KC_LT | < | S(KC_COMMA) |
KC_RIGHT_ANGLE_BRACKET | KC_RABK、KC_GT | > | S(KC_DOT) |
KC_QUESTION | KC_QUES | ? | S(KC_SLASH) |
其中KC_DOUBLE_QUOTE、KC_LEFT_ANGLE_BRACKET、KC_RIGHT_ANGLE_BRACKET各有两个别名,例如KC_DQT与KC_DQUO都指向同一个宏(见 keymap_us.h 中的#define KC_DQT KC_DQUO、#define KC_LT KC_LABK、#define KC_GT KC_RABK),写 keymap 时选哪种拼法不影响行为。
三、宏展开链路:从 KC_AT 到发送 Left Shift + 2
理解这些键码的关键在于S()宏的定义链。在 quantum_keycodes.h 中:
#define S(kc) LSFT(kc)而LSFT在同文件 第 46 行 定义:
#define LSFT(kc) (QK_LSFT | (kc))因此KC_AT在编译期就静态展开为QK_LSFT | KC_2这样的 16 位值:高 8 位携带 "左 Shift" 标志,低 8 位保留基础键码KC_2(注意QK_LSFT的值落在低 8 位键码区的高位上,宏直接按位或即可编码出组合语义)。这与KC_LSPO/KC_RSPC这类真正独立的 HID 键码完全不同——后者不需要 Shift 参与。
运行时的注册流程
按键被判定为按下后,会进入 quantum.c 中的 16 位键码注册入口:
__attribute__((weak)) void register_code16(uint16_t code) { if (IS_MODIFIER_KEYCODE(code) || code == KC_NO) { do_code16(code, register_mods); } else { do_code16(code, register_weak_mods); } register_code(code); }其中do_code16调用extract_mod_bits(quantum.c 第 102 行)解析出要发送的修饰键:当code & QK_LSFT成立且不属于右修饰键区间时,执行
if (code & QK_LSFT) mods_to_send |= MOD_BIT(KC_LEFT_SHIFT);随后才调用register_code(code)发送剥掉标志位后的基础键码。也就是说,KC_HASH按下时固件实际产生两个动作:先(弱)注册左 Shift 修饰键,再注册KC_3,松键时逆序释放——这就是"发送 Left Shift 与未 Shift 的键码,而非符号本身"的底层机制。
四、限制一:不能用于 Mod-Tap / Layer-Tap
原文档 的 Caveats 部分指出:这些键码不能用在 Mod-Tap 或 Layer-Tap 中,因为键码里携带的修饰键会被忽略。
从源码结构看,这一限制可以直接验证。MT()与LT()宏在 quantum_keycodes.h 中定义:
#define LT(layer, kc) (QK_LAYER_TAP | (((layer) & 0xF) << 8) | ((kc) & 0xFF)) #define MT(mod, kc) (QK_MOD_TAP | (((mod) & 0x1F) << 8) | ((kc) & 0xFF))两个宏都只对kc做了& 0xFF掩码。而KC_EXCLAIM展开后的QK_LSFT | KC_1中,QK_LSFT的标志位正好位于低 8 位内被掩码掉的位置,组合语义随之丢失——tap 分支最终只会触发KC_1本身,而不是!。
正确写法是在 Mod-Tap/Layer-Tap 中显式传入基础键,或者把整个组合交给外层宏,例如:
// 错误:KC_EXCLAIM 的 Shift 标志被 MT 掩码丢弃 MT(MOD_LSFT, KC_EXCLAIM) // 正确一:tap 部分直接用基础键码 MT(MOD_LSFT, KC_1) // 正确二:需要符号输出时,用 LT/MT 之外的方式组合,或在 tap 分支 // 里显式使用 S(kc) 展开后的完整键值(具体取决于想要的行为)同理,任何"在 keymap 宏内嵌套组合宏"的场景都应先确认内层键值是否会被外层宏的位运算破坏,这是 QMK 键码位编码模型的通用心智负担。
五、限制二:Windows 远程桌面丢字符及修复方法
原文档 还记录了第二个坑:在 Windows 上使用 Remote Desktop Connection 时可能打不出这些符号。原因是这些键码触发的 Shift 按下与释放极快(弱修饰键在发送基础键码前后几乎瞬时完成),远程桌面的输入转发链路可能漏掉这对事件。
文档给出的官方修复步骤是:
- 打开 Remote Desktop Connection;
- 点击Show Options;
- 进入Local Resources(本地资源)选项卡;
- 在 Keyboard 区域,把下拉框从默认值改为On this Computer。
这样键盘事件在本地处理后再转发,字符即可正常输出。
需要说明的适用前提:该问题只出现在"本地 QMK 键盘 → 远程桌面 → 远程会话"这一特定链路,且与 Shifted 符号键码"发送速度极快"的特性直接相关;换成LCTL/LSFT长按组合或KC_LBRC等真实键码则不受影响。
六、使用建议小结
- 静态按键位(普通 keymap 层、
KC_TILDE直接填进数组)是最常用且完全安全的场景,等价于手写S(KC_GRAVE),但可读性更好; - 动态场景(Mod-Tap、Layer-Tap、自定义
on_press回调中)避免直接传入这些 Shifted 键码,防止标志位被外层宏掩码丢弃; - 跨平台注意:它们只编码 US ANSI 的符号位置(
KC_LABK=S(KC_COMMA)等),在 US ISO 或其他布局下对应位置的实际字符会不同——这是布局特性,不是键码缺陷; - 远程桌面用户按第五节步骤调整 RDP 键盘选项即可消除丢字。
七、相关文件索引
| 文件 | 作用 |
|---|---|
| docs/keycodes_us_ansi_shifted.md | 本文档:键码表、Caveats、RDP 修复步骤 |
| quantum/keymap_extras/keymap_us.h | 全部 22 个 Shifted 符号键码及别名的#define定义 |
| quantum/quantum_keycodes.h | LSFT/S宏定义,LT/MT宏的 0xFF 掩码逻辑 |
| quantum/quantum.c | extract_mod_bits与register_code16:QK_LSFT标志到左 Shift 修饰键的解析与发送 |
【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考