QMK 键盘固件 USB 输入链路解析:从按键到扫描码、键码再到屏幕字符的完整原理
2026/9/14 23:04:37 网站建设 项目流程

QMK 键盘固件 USB 输入链路解析:从按键到扫描码、键码再到屏幕字符的完整原理

【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware

当你按下 QMK 机械键盘上的一个键位时,屏幕上出现的字符并不是按键"直接"产生的:物理电接触、矩阵扫描、HID 扫描码上报、内核键码映射、操作系统布局翻译,是一条层层解耦的单向流水线。理解这条链路,是修改 QMK 固件前必须建立的认知基础——它会清楚地告诉你:固件只能改"哪个扫描码被送出",而无法改变"操作系统把键码翻译成哪个字符"。读完全文,你将掌握从keyboard_task()主循环到UC()Unicode 键码的完整实现路径,以及为什么 QWERTY US 布局下某个键永远打不出、又该如何绕过布局限制输入 Unicode 字符。

一、宏观链路:一次击键背后发生了什么

文档 docs/how_keyboards_work.md 首先给出一个最简化的动作链路图:

+------+ +-----+ +----------+ +----------+ +----+ || User |-------->| Key |------>| Firmware |----->| USB wire |---->| OS | +------+ +-----+ +----------+ +----------+ +----+

即:用户 → 按键(物理触发)→ 键盘固件 → USB 线缆(HID 协议)→ 操作系统。任何一次击键,事件都沿这条链单向传递;每个环节的职责边界不同,决定了你在固件层能做什么、不能做什么。下面按文档的四个阶段逐一展开,并在每阶段补充 QMK 仓库中的源码证据。

二、阶段一:你按下按键(You Press a Key)

按下按键的瞬间,键盘固件有能力注册这一事件——包括"按下(pressed)""按住(held)""抬起(released)"三种状态。

文档指出,这一过程通常通过周期性扫描按键状态实现,而扫描速度受三个因素制约:

  1. 机械轴体本身的响应时间(物理下限);
  2. 传输协议(此处为 USB HID);
  3. 所用软件的主循环实现

在 QMK 源码中,这条"周期性扫描"链路的核心是 quantum/keyboard.c 中的主任务函数:

// quantum/keyboard.c (约 L704-L713) /** \brief Main task that is repeatedly called as fast as possible. */ void keyboard_task(void) { __attribute__((unused)) bool activity_has_occurred = false; if (matrix_task()) { last_matrix_activity_trigger(); activity_has_occurred = true; } quantum_task(); ... }

keyboard_task()被尽可能快地反复调用,每一步先执行matrix_task()扫描键位矩阵(只有矩阵状态发生变化时才返回真),再进入quantum_task()。后者在末尾调用host_task()(同一文件约 L701 行),把处理后的按键结果上报给宿主——这正是链路图中 "Firmware → USB wire" 一跳的落地实现。

从源码结构看,"按下 / 按住 / 抬起"三态的识别依赖矩阵前后两帧的差值比较:matrix_task()内部保存matrix_previous,与当前行状态逐行比对得出matrix_changed,从而区分"新按下"与"持续按住",抬起则对应上一帧为真、当前帧为假。这正是文档所说"固件可以注册 pressed / held / released"的底层机制。

三、阶段二:固件到底发了什么(What the Firmware Sends)

这是整条链路中最容易被误解的一环。文档的核心论断是:

固件不发送实际的字母或字符,只发送扫描码(scancode)。

USB HID 规范(HID Usage Tables,HUT 1.12)规定了键盘可以合法上报的扫描码范围:一个预定义的简单数字列表,从0x000xE7。固件负责为键盘的每个物理按键分配一个扫描码。

由此得出 QMK 使用者必须记住的一条边界:

通过修改固件,你只能改变某个键通过 USB 送出哪个扫描码,不能改变这个扫描码最终变成什么字符。

在 QMK 仓库中,"分配扫描码"这件事发生在 keymap 层。完整的可用键码清单见 docs/keycodes.md,其中基础键码一节(Basic Keycodes)完整覆盖了 HID 使用表定义的字符键、功能键与修饰键,例如:

键码别名说明
KC_AaA
KC_11!
KC_ENTERKC_ENTReturn (Enter)
KC_LEFT_SHIFTKC_LSFT左 Shift
KC_LEFT_GUIKC_LGUIKC_LCMDKC_LWIN左 GUI(Windows/Command/Super 键)
KC_AUDIO_VOL_UPKC_VOLU音量增大

文档特别强调了扫描码与键码(keycode)的区分:扫描码是 USB 线上的裸编号(0x000xE7),而 QMK 的KC_*宏是构建期符号。发送路径在 quantum/action.c、quantum/action_util.c 等文件中实现——它们将动作解析后的结果填入 HID 键盘报告并调用 USB 协议栈发送。QMK 针对不同硬件平台集成了相应的 USB 库,见仓库lib/目录下的 LUFA(Atmel AVR 的 USB 外设框架)、VUSB(虚拟 USB 外设,用于无硬件 USB 控制器的 MCU)等,这些就是链路图中 "USB wire" 一段在固件侧的实际载体。

四、阶段三:事件输入子系统 / 内核做了什么(Scancode → Keycode)

扫描码经过 USB 到达主机后,并不是直接可用。文档指出:

扫描码会被映射为键码(keycode),该映射依赖于键盘(以 systemd 的hwdb.d/60-keyboard.hwdb文件为例)。如果没有这个映射,操作系统将收不到有效键码,也就无法对这次按键做任何有用处理。

这意味着:

  • 键码与扫描码的对应关系由操作系统侧的映射表决定(Linux 下即内核 HID 驱动 + systemd hwdb 一类机制);
  • 固件层无法干预这一步。同一扫描码在不同操作系统、不同 hwdb 版本下理论上可能映射到不同键码;
  • 这也是"为什么同一把键盘接 Windows 和 Linux 表现略有差异"的根源之一。

对 QMK 开发者的实际含义是:调试"某个键没反应"时,先确认固件是否真的上报了预期扫描码(可用qmk console观察keyboard report),再怀疑主机侧映射。

五、阶段四:操作系统做了什么(Keycode → Character)

键码到达操作系统后,还需要一块软件把它匹配为实际字符——依据就是当前生效的键盘布局(keyboard layout)。文档给出了 QWERTY 布局下映射表的样本:

keycodecharacter
0x04a/A
0x05b/B
0x06c/C
......
0x1Cy/Y
0x1Dz/Z
......

注意两个细节:

  1. 同一个键码在 Shift 按下/未按下时分别对应小写/大写aA),大小写切换仍发生在主机侧,固件只需正确上报 Shift 修饰键状态;
  2. 映射表是布局的函数——换布局,同一键码翻译成不同字符。

六、回到固件:QMK 如何用布局名来定义键

既然布局是固定的(除非你自创布局),QMK 的 keymap 语法就允许直接用布局中的键名调用键码,而不是手写十六进制编号。文档举例:KC_A实际代表 QWERTY 下的0x04。完整清单即 docs/keycodes.md 与 docs/keycodes_basic.md。

在 keymap 中的实际写法(QMK 标准 keymap 结构)形如:

#include QMK_KEYBOARD_H const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { [0] = LAYOUT( KC_A, KC_B, KC_C, KC_ENTER, KC_SPC, KC_A ), [1] = LAYOUT( UC(0x20AC), UM(0), UP(1, 2), ... ) };

第 0 层用KC_A(即 QWERTY 下0x04)这样的布局名定义;第 1 层则引出 QMK 的 Unicode 键码,下一节详述。

七、你能发送的字符是有界的(List of Characters You Can Send)

文档在此抛出一个关键结论:

撇开快捷键不谈,有限数量的键码被映射到有限布局上,意味着你能分配给某个键的字符,只能是该布局中已存在的字符

文档给出的经典反例:如果你的布局是QWERTY US,想把某个键直接配成输出(欧元符号)——做不到,因为 QWERTY US 布局里没有这条映射。可行的出路是改用QWERTY UKQWERTY US International这类包含该符号的布局(这属于主机侧布局设置,而非固件改动)。

文档同时回答了"为什么不设计一个覆盖全部 Unicode 的键盘布局":

通过 USB 可用的键码数量有限,根本不允许这么做。

HID 使用表预留的字符键码(0x040x24区段的字母数字符号加上0xE00xE7等)总共只有几十个,而 Unicode 码点有上百万——数量级差距决定了"布局内直发"这条路必然止步于有限字符集。这也是前文"固件只能改扫描码"论断的边界来源。

八、(也许)输入 Unicode 字符的办法(How to (Maybe) Enter Unicode Characters)

既然布局内字符集有界,QMK 提供了一条绕行路径:

让固件发送按键序列,去触发目标操作系统的软件 Unicode 输入方式(例如通过十六进制输入方式),从而实际上不依赖操作系统里定义的布局输入任意字符。

QMK 中对应的实现是 Unicode 键码族(见 docs/keycodes.md 的 "Unicode Support" 一节):

键码说明
UC(c)发送 Unicode 码点c(支持到0x7FFF
UM(i)发送unicode_map表中索引i的码点
UP(i, j)发送索引i的码点;若 Shift/Caps 处于开启状态则发送j
QK_UNICODE_MODE_NEXT/QK_UNICODE_MODE_PREVIOUS在已选输入模式间切换
QK_UNICODE_MODE_MACOS切换到 macOS 输入方式
QK_UNICODE_MODE_LINUX切换到 Linux 输入方式
QK_UNICODE_MODE_WINDOWS切换到 Windows 输入方式
QK_UNICODE_MODE_BSD切换到 BSD 输入方式(尚未实现)
QK_UNICODE_MODE_WINCOMPOSE切换到 Windows 下基于 WinCompose 的输入方式
QK_UNICODE_MODE_EMACS切换到 Emacs(C-x 8 RET)输入方式

这些键码的底层实现位于 quantum/unicode/unicode.h 与 quantum/unicode/unicode.c,配套的处理逻辑在 quantum/process_keycode/process_unicode_common.c。源码中定义的输入模式枚举与键码一一对应:

// quantum/unicode/unicode.h (约 L42-L51) enum unicode_input_modes { UNICODE_MODE_MACOS, // macOS using Unicode Hex Input UNICODE_MODE_LINUX, // Linux using IBus UNICODE_MODE_WINDOWS, // Windows using EnableHexNumpad UNICODE_MODE_BSD, // BSD (not implemented) UNICODE_MODE_WINCOMPOSE, // Windows using WinCompose UNICODE_MODE_EMACS, // Emacs UNICODE_MODE_COUNT };

注意:这里的UC()并非通过单字节扫描码直达字符,而是在固件内部展开成一段普通按键序列(例如 macOS 模式走 Unicode Hex Input,即Control + Option + 十六进制码 + Enter一类组合),逐键通过正常 HID 报告送出。也就是说,"打一个"在总线上体现为若干次普通的修饰键 + 字符键上报。

这条路径的三大代价

文档明确列出了这种"按键序列模拟输入法"的局限,使用 Unicode 键码前必须清楚:

  1. 绑定单一操作系统:切换 OS 需要重新编译固件(因为模拟的按键序列是 OS 特定的);
  2. 同一 OS 内并非所有软件都支持:目标程序必须实现了相应的 Unicode 输入入口(例如 Windows 的 EnableHexNumpad、macOS 的 Unicode Hex Input 开关);
  3. 部分系统上仅能输入 Unicode 的子集,而非全码位区。

这就是文档标题中 "(Maybe)" 一词的含义:该方案可用,但不是无条件可用。

九、全链路小结:每一层你能改什么

环节载体可修改性(固件视角)
按下 → 扫描矩阵 + quantum/keyboard.c 主循环固件内:防抖、扫描率、矩阵映射均可改
扫描码上报HID 报告(0x000xE7固件内:唯一可以直接决定"发什么扫描码"的地方
扫描码 → 键码内核 / hwdb 映射固件不可干预(主机侧)
键码 → 字符操作系统键盘布局固件不可干预(换布局只能改 OS 设置)
任意 Unicode 字符Unicode 键码UC/UM/UP+ 键序列模拟固件内可用,但受三大代价限制

回到文档开篇的链路图,可以把它重读为一句话:User 产生事件,Key 传递状态,Firmware 决定扫描码,USB wire 承载 HID 协议,OS 完成键码与字符的两级翻译。你在 QMK 中修改 keymap,作用面严格限定在第三个节点;任何期望"固件直接打出某个字符"的想法,都会被第 3、4 环节的主机侧映射规则重新解释。掌握这条边界后,docs/keymap.md、docs/feature_layers.md 等后续主题中的按键行为设计,才能建立在正确的心智模型之上。

【免费下载链接】qmk_firmwareOpen-source keyboard firmware for Atmel AVR and Arm USB families项目地址: https://gitcode.com/GitHub_Trending/qm/qmk_firmware

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

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

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

立即咨询