按键出错时,QMK 的架构最容易显出轮廓。轻点一次却出现两次输入,按下三个键的组合却冒出第四个键,自定义按键悄然消失,主机也会打出键帽上根本没有的字符。屏幕上的症状相近,到了键盘内部,它们分属事件路径的不同阶段。
QMK 从开关矩阵采得的电压起步,字母要到后面才出现。接下来,固件为原始样本去抖,依据得到的坐标,从已编译 keymap 的当前活动层中解析 keycode,再把这条记录交给一串功能模块和用户 hook(钩子)。层命令或已被消费的自定义动作不会生成 keyboard report;宏可以生成多份。对主机可见的基础按键而言,设备侧最后一步是一份 USB report,其中的 usage(用法标识)由主机解释。[1][2][6]
这种分阶段路径是 QMK 架构的要点。可编程性呈现在表面,职责分离则让同一套固件适用于紧凑型、分体式、正交直列式和传统错列式键盘。这样的分工也把键盘制作者的问题从“QMK 为什么漏掉了我的按键?”引向更准确的一处:事件在哪一道边界开始偏离实际状态?
题图拍摄于 2014 年,一把拆去外壳的 ErgoDox 键盘,两半分列在触控板两侧,两块电路板完全裸露。空白键帽各有实体位置,却没有印刷含义。这几乎是 QMK 模型的实体写照:硬件几何先存在,行为随后赋予。[3][8]
按键始于一个坐标
机械开关闭合,只会让两条电气线路接通;“A”的含义要到后面的环节才出现。键盘设计者把开关排成行列矩阵,让控制器用较少的引脚检查许多位置,省下为每个按键单独配置输入引脚的成本。常规扫描期间,固件依次驱动矩阵的一侧并读取另一侧,得到该时刻处于断开或闭合状态的一组行列位置。[2]
这种引脚节省也带来第一道故障边界。多个开关同时按住时,电流会在普通矩阵中穿过意外路径,使未按下的位置看起来已经闭合,这就是鬼键(ghosting)。每个开关各配一只二极管,便可限制电流方向,让扫描辨认出真正按下的组合。固件只能响应它收到的状态;含混接线一旦抹去物理真相,后续处理也无从还原。[2]
QMK 在键盘配置中明确写下这份电气约定。info.json 里的 matrix_pins.rows、matrix_pins.cols 和 diode_direction 等字段,描述控制器如何连接各个开关。键盘也可以改为声明直连引脚,或启用自定义矩阵代码;这两种做法对应不同的硬件约定,与替换 keymap 无关。同一份文件还可以标明处理器、bootloader、固件输出格式、支持的功能,以及 QMK Configurator 等工具显示的几何布局。[4]
移植期间,这一分工直接决定排查顺序。某一行全无响应、两个位置彼此调换,或某组按键组合产生幽灵键时,首先应查看原始矩阵状态及其接线描述。把 KC_A 换成 KC_B,只是给同一个错误坐标重新涂上标签。
去抖决定一次变化何时成为真实事件
接线正确时,电气边沿仍会带有抖动。机械触点在接合或分离时会颤动,连续几次扫描的结果会在断开与闭合之间短暂跳变。QMK 在每次原始矩阵扫描之后执行去抖(debounce),判断采样到的变化何时稳定到足以交给固件的其余部分。[3]
这项策略允许配置,因为响应速度、抗噪能力、内存占用和矩阵形状之间各有取舍。QMK 文档给出的默认去抖间隔是 5 毫秒,算法由 DEBOUNCE_TYPE 选择。延后型(deferred)算法会等到一段安静间隔结束,再报告变化;即时型(eager)算法立即报告,随后在这段间隔内抑制其他变化。状态可以在全局、逐行或逐键三个范围内跟踪。全局状态开销较低;更细的状态划分则以更多 RAM 和处理量为代价,避免矩阵一处的活动拖延无关按键。[3][4]
“延迟设置”只概括了去抖的一小部分。即时报告按下沿会带来轻快触感,同时也会把较早出现的噪声边沿当成有效变化。延后报告能够滤掉噪声,代价是等待足够的证据。全局计时器效率较高,却会把原本无关的开关牵连在一起。QMK 文档解释逐行算法为何适合某些矩阵形态时,甚至专门提到 ErgoDox 型号采用的旋转矩阵。[3]
诊断时,先要区分不稳定的样本与错误的含义。原始位置持续闪烁时,keymap 还没有介入。去抖后的位置只变化一次,随后却触发错误动作,此时拉长去抖间隔只会遮住真正的层状态或处理错误。
实体布局与 keymap 是相互分离的映射
固件编译时,实体排列与电气坐标之间的分离已经确立。LAYOUT() 宏把按实体布局列出的键位,转换成按行列排布的 keymap 初始化器。宽键只占一个开关,却会覆盖多个视觉单位;弧形或分体式键盘的接线顺序往往看不出矩形;矩阵中的某些位置也完全没有开关,因此这一步不可省略。QMK 用 KC_NO 填入空缺位置,keymap 作者便可以按照实体布局书写行为,免于直接遵循接线顺序。[1]
运行期间,去抖后的矩阵坐标会索引这份已编译的数据。它在 C 语言中的表示按层、行、列组织;基础层上的某个位置可以解析为 KC_A,换到另一个活动层,同一位置也可以成为方向键、修饰键或层命令。这是一道清楚的接缝:键盘定义与 LAYOUT() 宏确定开关存放在哪里,活动 keymap 则说明这个位置此刻要做什么。[1][4]
当两块键盘都提供相同的 LAYOUT_<name> 约定时,QMK 的 community layout 功能允许它们原样复用同一份源码级 keymap。摆在桌面上外观相近的两块键盘,可以暴露不同布局;电气走线不同的两块键盘,也可以暴露同一种布局。键盘专用代码仍会限制可移植性,共享约定由此打开复用途径;板级代码决定其限度,外观和键数本身给不出保证。[9]
处理链可以改写事件,也可以截留事件
QMK 对比当前去抖后的矩阵与上一轮状态,把变化分派为按下和释放记录。这些记录进入 process_record_quantum(),已启用的功能会在普通按键处理完成前解释它们。编译后的处理链可以包含组合键、tap dance、Caps Word、key override、动态宏和用户定义 keycode 等行为;构建配置通常会移除未使用的功能。[1]
这条链也预留了明确的扩展点。键盘级代码可以定义 process_record_kb(),keymap 则可以定义 process_record_user()。返回 true 会让后续处理继续,返回 false 则会在此处终止。事件由此可以被截留:自定义按键执行自己的动作,同时停止发出底层 keycode;一次疏忽的返回值也会让下游所有行为看起来都已失效。[1][5]
QMK 还在其他阶段提供 hook,包括初始化、每次矩阵扫描,以及循环末尾的 housekeeping。调用时机本身就是架构的一部分。matrix_scan_user() 会以控制器所能达到的扫描频率运行;housekeeping 则在上一轮扫描产生的层更新、USB report、LED 和显示处理全部结束后运行。QMK 建议在这些高频路径中只做少量工作,非紧急任务应当限流。一次缓慢的显示更新,或误写的 busy loop,足以把逻辑正确的按键路径拖成可感知的输入延迟。[5]
定制行为宜放在足以表达需求、同时尽量靠后的接缝处。键盘移植属于硬件配置和矩阵代码,重映射属于 keymap,特殊事件属于 process_record_user(),周期性的非紧急工作通常属于 housekeeping。为了编写宏而另行改写扫描器,得到的能力相同,却把普通行为搬进整套系统对时序最敏感的位置。
USB 传递 usage,成品文本留给主机
处理流程产出一个对主机可见的基础按键动作时,QMK 会更新发往主机的 keyboard report。这里的关键边界是:键盘通常发送 USB 人机接口设备(Human Interface Device,HID)规范定义的标识符,键帽上印着的字符则留在主机一侧。QMK 把一个常见的基础 keycode 称为 KC_A,文档列出的数值为 0x04;主机栈会结合当前键盘布局、修饰键、锁定状态和应用状态,决定这个 usage 最终成为文本,还是参与快捷键。[6]
因此,固件重映射与操作系统布局各自负责一段流程,彼此配合。QMK 可以决定某个实体位置报告哪一项 usage,也可以让宏发出一串 usage。单个 HID usage 只携带规范约定的用途;任意 Unicode 字符仍依赖主机专用的输入方法。QMK 的 Unicode 功能正是调用这些方法,操作系统与应用兼容性也随之重新进入这份约定。[6]
这种可移植性依然真实存在。Ars Technica 在一篇针对 System76 基于 QMK 的 Launch 键盘的独立评测中发现,板载配置文件会随键盘转移到另一台计算机,即使那里没有安装配置应用,配置依然生效。这展示了由固件掌握映射带来的收益:实体位置与行为的对应关系可以保存在设备中。主机的文本布局、输入法和应用快捷键仍旧留在键盘之外。[7]
从最早断裂的边界解读故障
沿着这条流水线观察,每类常见故障都以不同方式显现。特定按键组合中冒出的虚假位置,指向矩阵拓扑或缺失的二极管。某个坐标本身正确,却反复产生按下或释放边沿,检查范围便落到开关、电气噪声、扫描时序和去抖策略。稳定坐标只在某一层触发错误动作,则指向布局与 keymap 状态。
标准 keycode 一切正常,自定义动作却消失时,应检查处理链:前面的功能或 hook 是否消费了记录,自定义代码是否终止了常规处理却没有安排替代动作。按键或修饰键在释放后仍保持激活,则要检查代码是否注册了按下动作,却遗漏了与之配对的释放路径。预期 usage 已经到达主机,出现的字形却不对,通常说明固件已经完成了自己的工作,最终解释权属于主机栈。只有加入显示或动画代码后,所有动作才变得迟缓时,扫描与 housekeeping hook 比 USB 更值得怀疑。[1][3][5][6]
这套顺序能避开跨层补偿。鬼键的修复在矩阵电路,去抖间隔只负责触点稳定性;接错的行引脚要改硬件配置,keymap 只负责坐标含义;跨操作系统的 Unicode 输入取决于主机输入法,宏仍在这份约定之内。扫描已经成功时,应处理随后阻塞主循环的工作;替换矩阵扫描器落错了层级。
QMK 最深的一项设计选择,在于让主机可见动作由显式接缝之后的证据逐段组装;这套架构没有规定每个开关都要发出一份 USB report。矩阵说明哪一条电路已经闭合,去抖判断这次变化是否可信,键盘定义与编译期布局确定坐标从何处进入 keymap,活动层与处理链再决定相应行为,以及是否有内容离开设备。一旦越过设备边界,USB report 携带 usage,主机负责最终解释。这些答案各自分开,可编程键盘的行为便始终有迹可循。
来源
- QMK Firmware,“Understanding QMK's Code”——主循环、矩阵状态比较、
LAYOUT()映射、keymap 组织方式与process_record处理链。 - QMK Firmware,“How a Keyboard Matrix Works”——行列扫描、多键 ghosting 与逐键二极管的作用。
- QMK Firmware,“Contact bounce / contact chatter”——去抖时序、即时与延后行为、全局/逐行/逐键状态、支持的算法与 ErgoDox 示例。
- QMK Firmware,“
info.jsonReference”——处理器与 bootloader 元数据、矩阵引脚、二极管方向、直连/自定义矩阵、去抖配置与布局数据。 - QMK Firmware,“How to Customize Your Keyboard's Behavior”——键盘与用户 hook、处理返回值、扫描循环注意事项与 housekeeping 时序。
- QMK Firmware,“How Keys Are Registered, and Interpreted by Computers”——USB HID 边界、基础 usage 数值、主机布局与固件级字符输入的限制。
- Scharon Harding,“System76 Launch review: Linux-friendly keyboard with a USB hub”,Ars Technica,2022 年 3 月 8 日——对基于 QMK 的重映射开展独立测试,并验证板载配置文件可以随键盘转移到另一台计算机。
- Ryan Forsythe,“Ergodox”(2014 年 9 月 22 日),经 Wikimedia Commons——本文题图所用历史硬件照片的来源与许可记录。
- QMK Firmware,“Layouts: Using a Keymap with Multiple Keyboards”——共享布局约定、与具体键盘无关的 keymap,以及键盘专用代码带来的限制。