news 2026/9/14 6:53:59

QMK 固件 Autocorrect(自动纠错)功能完全指南:原理、字典定制与回调扩展

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
QMK 固件 Autocorrect(自动纠错)功能完全指南:原理、字典定制与回调扩展

QMK 固件 Autocorrect(自动纠错)功能完全指南:原理、字典定制与回调扩展

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

本篇技术指南以 QMK Firmware 仓库中的 Autocorrect 功能文档为主体,结合 process_autocorrect.c、autocorrect_data.py 及 test_autocorrect.cpp 等源码与测试,系统讲解该功能的触发原理(反向 Trie 匹配)、启用方式、自定义字典语法、qmk generate-autocorrect-data生成流程、防误触发策略,以及三类用户回调函数(process_autocorrect_userapply_autocorrect、状态控制函数)的完整用法。读完本文,你可以在自己的键盘 keymap 中独立启用并深度定制自动纠错。

功能概述:在固件层面消灭习惯性错字

日常输入中,很多单词容易因肌肉记忆、按键顺序或手误而打错,例如thier(应为their)、fitler(应为filter)、lenght(应为length)。QMK 的 Autocorrect 特性在固件内部维护一个最近按键的小缓冲区,每次按键时检查缓冲区末尾是否命中字典中定义的错词(typo);一旦命中,固件会自动发送退格键删除错误字符,并补发正确的字符序列,从而在键盘端直接修正错字,减少输入错误的出现。

该特性完全运行在键盘固件内,不依赖操作系统或输入法的纠错能力,因此适用于任何连接该键盘的主机。其默认字典位于 autocorrect_data_default.h,共包含 70 条常见拼写错误的纠正条目(如fales -> falsebecuase -> becauseguage -> gauge等),开箱即可体验。

工作原理:反向 Trie 高效匹配

自动纠错的核心难点在于高效地检查缓冲区中是否存在错词,既要控制内存占用,也要保证查找速度快。文档与源码采用的方案是:将全部错词表示为一棵Trie(前缀树)数据结构——树中每个节点是一个字母,单词由通向叶子节点的路径构成。

由于需要判断"缓冲区是否以某个错词结尾",Trie 以倒序写入:查询时从缓冲区的最后一个字母开始,依次向前匹配倒数第二个、倒数第三个字母……直到某个字母不匹配(未命中),或到达叶子节点(命中错词,触发纠正)。以fitler为例,其倒序 Trie 路径为r → e → l → t → i → f,当用户敲入最后一个r时,缓冲区恰好以r-e-l-t-i-f结尾,随即触发纠正。

在 C 实现中,process_autocorrect维护typo_buffertypo_buffer_size(见 process_autocorrect.c):缓冲区按AUTOCORRECT_MAX_LENGTH上限滚动,满员时通过memmove丢弃最旧字符;缓冲区长度小于AUTOCORRECT_MIN_LENGTH时直接返回(最短错词之前的按键不可能命中)。随后从缓冲区末尾向前用pgm_read_byte逐字节读取存储在 PROGMEM 中的autocorrect_data数组并匹配 Trie 节点,命中叶子节点(首字节最高位为 1)即发现错词,计算需要退格的次数并发送替换字符串。

启用自动纠错

在键盘或 keymap 目录的rules.mk中加入一行:

AUTOCORRECT_ENABLE = yes

默认情况下,Autocorrect 处于关闭状态(对应源码中keymap_config.autocorrect_enable的默认值)。启用后还需在 keymap 中使用AC_TOGG键码打开它,其开关状态会持久化保存在 EEPROM 中(见autocorrect_enable()/autocorrect_disable()中对eeconfig_update_keymap的调用),因此通常只需开启一次。

状态控制键码

键码别名说明
QK_AUTOCORRECT_ONAC_ON开启 Autocorrect 功能
QK_AUTOCORRECT_OFFAC_OFF关闭 Autocorrect 功能
QK_AUTOCORRECT_TOGGLEAC_TOGG切换 Autocorrect 功能状态

上述键码定义于 quantum/keycodes.h(QK_AUTOCORRECT_ON = 0x7C74),并在 process_autocorrect.c 中被处理:按下时分别调用autocorrect_enable()autocorrect_disable()autocorrect_toggle(),并返回false阻止该键码继续传递。

测试验证

仓库自带单元测试 tests/autocorrect/test_autocorrect.cpp 可验证开关行为与纠错效果:

  • OnOffToggle测试逐一验证autocorrect_is_enabled()disable()enable()toggle()的状态切换;
  • fales_to_false_autocorrection测试模拟依次按下F A L E S,断言 HID 输出序列为F A L E BACKSPACE S E——即输入fales时固件自动发送一次退格并补上se
  • fales_disabled_autocorrect测试在关闭状态下输入fales,断言原样输出F A L E S
  • falsify_should_not_autocorrectoverture_should_not_autocorrect测试验证fales/ture作为正确单词的子串时不会被误触发。

这些测试精确印证了文档所述的行为:命中错词即退格纠正,关闭则透传,且子串误触发需要通过词边界控制。

自定义自动纠错字典

字典文件语法

创建一个文本文件,每行一条typo -> correction记录,例如:

:thier -> their fitler -> filter lenght -> length ouput -> output widht -> width

语法要点:

  • 行格式为typo -> correctiontypocorrection之间的空白(包括行首行尾)会被忽略;
  • 错词与纠正词不区分大小写(生成器会把 typo 强制转为小写,见parse_file_lines);
  • 错词只允许字母a–z以及特殊字符:(表示词边界);纠正词可以是任意非 Unicode 字符;
  • #开头的行与空行会被忽略。

生成器 autocorrect_data.py 中的parse_file还会做额外校验:重复的 typo 会被警告并跳过;typo 不能互为子串,否则较长的那条永远不会触发;建议 typo 长度至少为 5 以避免误触发;typo 超过 127 字符会报错退出。

生成 C 头文件

在仓库根目录执行(将字典文件名替换为你的实际文件名):

qmk generate-autocorrect-data autocorrect_dictionary.txt

该命令解析字典、构建倒序 Trie 并序列化为 C 数组,最终在当前目录生成autocorrect_data.h。也可以指定键盘与 keymap,让文件直接输出到对应 keymap 目录:

qmk generate-autocorrect-data -kb planck/rev6 -km jackhumbert autocorrect_dictionary.txt

只要autocorrect_data.h位于你的 keymap 目录或 user 目录中,编译时就会被自动拾取——process_autocorrect.c 通过__has_include("autocorrect_data.h")优先加载用户字典,否则回退到默认字典并打印Autocorrect is using the default library.提示。

生成的头文件形如:

// :thier -> their // fitler -> filter // lenght -> length // ouput -> output // widht -> width #define AUTOCORRECT_MIN_LENGTH 5 // "ouput" #define AUTOCORRECT_MAX_LENGTH 6 // ":thier" #define DICTIONARY_SIZE 74 static const uint8_t autocorrect_data[DICTIONARY_SIZE] PROGMEM = {85, 7, 0, 23, 35, 0, 0, 8, 0, 76, 16, 0, 15, 25, 0, 0, 11, 23, 44, 0, 130, 101, 105, 114, 0, 23, 12, 9, 0, 131, 108, 116, 101, 114, 0, 75, 42, 0, 24, 64, 0, 0, 71, 49, 0, 10, 56, 0, 0, 12, 26, 0, 129, 116, 104, 0, 17, 8, 15, 0, 129, 116, 104, 0, 19, 24, 18, 0, 130, 116, 112, 117, 116, 0};

其中AUTOCORRECT_MIN_LENGTH/AUTOCORRECT_MAX_LENGTH由最短/最长 typo 推导而来,DICTIONARY_SIZE是序列化数组的总字节数,固件据此分配缓冲区并做越界保护(process_autocorrect.c 中state >= DICTIONARY_SIZE即安全返回)。

避免误触发(词边界:的使用)

默认情况下,typo 会在单词内部被搜索,这有利于修正maxFitlerOuput这类长标识符中的错误;但副作用是:当 typo 恰好是某个拼写正确单词的子串时会被误触发。例如若字典中有thier -> their,它会在wealthierfilthier等正确单词上错误触发。

解决方案是在 typo 前后加上词边界:来约束匹配范围。:匹配空格、句点、逗号、下划线、数字以及大多数非字母字符。以下表总结thier在不同边界写法下的匹配行为:

文本thier:thierthier::thier:
看到thier错词匹配匹配匹配匹配
单词thiers匹配匹配不匹配不匹配
单词wealthier匹配不匹配匹配不匹配

:thier:最为严格,仅当thier作为完整独立单词出现时才触发。

qmk generate-autocorrect-data在生成时还会尽力检查"typo 作为正确单词子串会误触发"的情况:它会将每个 typo 与english_wordsPython 包中的约 2.5 万个英文单词比对,未安装该包时可运行python3 -m pip install english_words安装。需要注意,目前该检查仅覆盖英文单词。对应的警告逻辑位于parse_filecheck_typo_against_dictionary:例如不带边界的 typo 命中某个正确单词时会提示 "would falsely trigger on correctly spelled word",而完整成词(:typo:)且本身是正确单词时也会收到警告。未安装english_words时,脚本会退化为一份极小的内建单词表(含wealthierloosest等)作为兜底。

覆盖自动纠错(临时输入错词)

偶尔你确实需要原样输入一个错词(例如正在编辑autocorrect_dict.txt时),可以通过以下方式避免被纠正:

  1. 先输入该错词的前半部分;
  2. 在输入最后一个字母之前,按下并松开 Ctrl 或 Alt 键;
  3. 再输入剩余字母。

其原理是:自动纠错实现不解析热键,只要检测到除 Shift 以外的修饰键被按住,就会清空 typo 缓冲区并重置自身状态(见 process_autocorrect.c:if ((*mods & ~MOD_MASK_SHIFT) != 0)时重置缓冲区并跳过处理)。此外,也可以用AC_TOGG键码直接切换 Autocorrect 的开关。

用户回调函数(深度定制)

process_autocorrect_user:输入净化与异常处理

原型:bool process_autocorrect_user(uint16_t *keycode, keyrecord_t *record, uint8_t *typo_buffer_size, uint8_t *mods)

该回调允许在按键进入纠错引擎之前对其进行自定义处理(即"净化"输入)。净化是必需的,因为自动纠错只对 8 位的基础键码(基础键码说明)做 typo 字母匹配:如果带有修饰键的键码或 16 位键码(如 Shift + A)被直接传入,字母检测会失败。例如 Mod-Tap 键LCTL_T(KC_A)是 16 位键码,应被掩码还原为 8 位的KC_A

默认的process_autocorrect_user是一个weak弱定义函数,其实现委托给process_autocorrect_default_handler(见 process_autocorrect.c),覆盖了 QMK 绝大多数特殊功能键码的使用场景,包括本文前述"用非 Shift 修饰键覆盖纠错"的逻辑。用户可以在自己的keymap.c(或其他代码文件)中重新定义同名函数来覆盖它。

自定义示例:假设你有一个自定义键码QMKBEST应被当作单词的一部分忽略,另一个自定义键码QMKLAYER应覆盖自动纠错,可以在你的源码中扩展switch语句:

bool process_autocorrect_user(uint16_t *keycode, keyrecord_t *record, uint8_t *typo_buffer_size, uint8_t *mods) { // 各匹配区间可参考 quantum_keycodes.h。 switch (*keycode) { // 排除以下键码,不参与处理。 case KC_LSFT: case KC_RSFT: case KC_CAPS: case QK_TO ... QK_ONE_SHOT_LAYER_MAX: case QK_LAYER_TAP_TOGGLE ... QK_LAYER_MOD_MAX: case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX: return false; // 从带 Shift 的键码中提取基础键码。 case QK_LSFT ... QK_LSFT + 255: case QK_RSFT ... QK_RSFT + 255: if (*keycode >= QK_LSFT && *keycode <= (QK_LSFT + 255)) { *mods |= MOD_LSFT; } else { *mods |= MOD_RSFT; } *keycode &= 0xFF; // 取出基础键码。 return true; #ifndef NO_ACTION_TAPPING // 按住时排除 tap-hold 键,轻点时提取基础键码。 case QK_LAYER_TAP ... QK_LAYER_TAP_MAX: # ifdef NO_ACTION_LAYER // 层功能被禁用但 action tapping 仍启用时排除 Layer Tap。 return false; # endif case QK_MOD_TAP ... QK_MOD_TAP_MAX: // 非 Shift 修饰被按住时排除(按住状态)。 if (!record->tap.count) { return false; } *keycode &= 0xFF; break; #else case QK_MOD_TAP ... QK_MOD_TAP_MAX: case QK_LAYER_TAP ... QK_LAYER_TAP_MAX: // 相关功能被禁用时排除。 return false; #endif // 按住时排除换手(swap hands)键,轻点时提取基础键码。 case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX: #ifdef SWAP_HANDS_ENABLE if (*keycode >= 0x56F0 || !record->tap.count) { return false; } *keycode &= 0xFF; break; #else return false; #endif // 处理自定义键码 case QMKBEST: return false; case QMKLAYER: *typo_buffer_size = 0; return false; } // 非 Shift 修饰键激活时禁用自动纠错。 if ((*mods & ~MOD_MASK_SHIFT) != 0) { *typo_buffer_size = 0; return false; } return true; }

注意:在该回调中return false表示跳过该键码的自动纠错处理;同时设置*typo_buffer_size = 0会一并清空纠错缓冲区,取消已存储的字母。默认处理器process_autocorrect_default_handler可在用户回调中调用,以复用内置的键码区间处理逻辑。

apply_autocorrect:接管或扩展纠正动作

原型:bool apply_autocorrect(uint8_t backspaces, const char *str, char *typo, char *correct)

该回调在错词被命中后调用,传入:需要退格删除的字符数backspaces、替换字符串str部分单词而非完整单词)、完整的错词与纠正词字符串typocorrect。用户可以在此增加额外处理,或完全替换默认的纠正动作。

由于实现本身没有"单词"概念(只有字母流),传入的typo/correct只是最佳推断,可能并不精确——例如可能得到wordtpyowordtypo,而不是预期的tpyotypo

示例一:命中错词时播放提示音并自行执行纠正(return false停止默认处理,需手动退格并发送新字符):

#ifdef AUDIO_ENABLE float autocorrect_song[][2] = SONG(TERMINAL_SOUND); #endif bool apply_autocorrect(uint8_t backspaces, const char *str, char *typo, char *correct) { #ifdef AUDIO_ENABLE PLAY_SONG(autocorrect_song); #endif for (uint8_t i = 0; i < backspaces; ++i) { tap_code(KC_BSPC); } send_string_P(str); return false; }

重要str指向 PROGMEM 中的数据。若你return false并希望发送该字符串,必须使用send_string_P而非send_stringSEND_STRING

示例二:只检测并展示事件,仍由内部代码执行纠正(return true):

bool apply_autocorrect(uint8_t backspaces, const char *str, char *typo, char *correct) { #ifdef OLED_ENABLE oled_write_P(PSTR("Auto-corrected"), false); #endif #ifdef CONSOLE_ENABLE printf("'%s' was corrected to '%s'\n", typo, correct); #endif return true; }

默认的apply_autocorrectweak定义且直接返回true(见 process_autocorrect.c),此时 process_autocorrect.c 会执行默认纠正:按backspaces次数发送KC_BSPC,再调用send_string_P(changes)发送替换文本。纠正后若触发键是空格,缓冲区重置为仅含空格(以便继续匹配下一单词),否则完全清空。

状态控制回调函数

以下函数可用于在自定义代码中编程控制 Autocorrect 状态(声明见 process_autocorrect.h):

函数说明
autocorrect_enable()开启 Autocorrect
autocorrect_disable()关闭 Autocorrect
autocorrect_toggle()切换 Autocorrect 状态
autocorrect_is_enabled()返回 Autocorrect 当前是否开启

附录:Trie 二进制数据格式

以下内容说明autocorrect_data中 Trie 的字节序列化方式。日常使用自动纠错无需关心这些细节,此处仅为有兴趣修改实现或了解原理的读者记录。

所有纠错数据存储在一个扁平数组autocorrect_data中,每个 Trie 节点关联一个字节偏移(根节点为 0)。节点首字节的最高两位决定节点类型:

  • 00链节点(chain node):只有一个子节点的 Trie 节点;
  • 01分支节点(branching node):有多个子节点的 Trie 节点;
  • 10叶子节点(leaf node):对应一个错词并保存其纠正数据。

分支节点:每个分支用"1 字节键码(KC_A–KC_Z)+ 指向子节点的 16 位小端字节偏移"编码。所有分支依次序列化,以零字节结尾;节点类型通过将首字节与 64 按位或(keycode | 64)标记为分支。示例根节点的序列化示意:

+-------+-------+-------+-------+-------+-------+-------+ | R|64 | node 2 | T | node 3 | 0 | +-------+-------+-------+-------+-------+-------+-------+

链节点:Trie 中常有很长的单子节点链(如fitler中的 f-i-t-l)。若按分支节点格式编码,每个节点都要附带 16 位链接,非常浪费空间;因此链采用"从最靠近根节点的字母开始的一串键码 + 零字节结尾"的紧凑格式,链尾子节点紧随其后编码(可为分支节点或叶子节点)。fitler中的 f-i-t-l 链编码为:

+-------+-------+-------+-------+-------+ | L | T | I | F | 0 | +-------+-------+-------+-------+-------+

链的中间位置也可被引用并按同样方式解码(例如从i而非l开始,子链格式一致)。

叶子节点:对应某个错词并保存纠正数据。首字节为需要退格的次数,随后是空字符结尾的替换文本 ASCII 字符串。触发后先按次数退格,再把该字符串交给send_string_P。以fitler为例,需要退格 3 次(不是 4 次——最后一个r按下时即捕获错词),并以lter替换;叶子类型通过将退格次数与 128 按位或(backspaces | 128)标记:

+-------+-------+-------+-------+-------+-------+ | 3|128 | 'l' | 't' | 'e' | 'r' | 0 | +-------+-------+-------+-------+-------+-------+

解码逻辑:一个 16 位变量state表示当前 Trie 位置(初始为 0 指向根节点)。对每个键码,测试state处字节的最高两位判断节点类型:

  • 00⇒ 链节点:若节点字节与键码匹配,state加一前进到下一字节;若下一字节为零,再加一跳到后续节点;
  • 01⇒ 分支节点:在分支中查找匹配键码,沿其节点链接前进;
  • 10⇒ 叶子节点:发现错词!读取首字节得到退格次数,将后续字节交给send_string_P发送纠正文本。

上述编码/解码逻辑分别对应生成端serialize_trie与运行端process_autocorrectcode & 64(分支)、code & 128(叶子)的判断;节点链接使用 16 位偏移,因此整个字典的序列化数据上限为 64KB(encode_link中超过0xffff会报错提示精简字典)。

结语

Autocorrect 是 QMK 中一个"小而精"的特性:反向 Trie 让错词匹配在常量级内存与线性时间内完成,:词边界机制有效规避了子串误触发,而三个层次的用户回调(输入净化、纠正动作、状态控制)使其可被深度定制。无论是直接启用内置字典、编写自定义纠错表,还是像 test_autocorrect.cpp 中那样精确验证每次按键的 HID 输出,本文介绍的配置与源码线索都能帮助你在自己的键盘上落地一套可靠、可预期的自动纠错方案。

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 6:53:46

PDF字体嵌入完整方法:PDF补丁丁一次搞定中文乱码与空白方块

PDF字体嵌入完整方法&#xff1a;PDF补丁丁一次搞定中文乱码与空白方块 【免费下载链接】PDFPatcher PDF补丁丁——PDF工具箱&#xff0c;可以编辑书签、剪裁旋转页面、解除限制、提取或合并文档&#xff0c;探查文档结构&#xff0c;提取图片、转成图片等等 项目地址: https…

作者头像 李华
网站建设 2026/9/14 6:52:26

C++竞赛模拟题实战:从BFS扩散到边界调试,复盘白蚁赛题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 6:52:23

AI论文写作工具千笔:三层智能辅助体系解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华