给带 QMK 功能的 Keychron 键盘增加 Apple 的 Globe 键

Apple Globe Key

为什么又发神经?

一个全屏快捷键引发的故事

事情是这样的,某天我在用倒霉的 Microsoft Teams 开会的时候,我习惯性地使用 ^ + ⌘ + F 快捷键尝试全屏显示。

结果反复按了几次快捷键都没有反应,我就奇了怪了,为什么不行呢?以前都可以的。

结果发现 Teams 的快捷键变了,跟原来的不一样了。如下图:

是不是只有 Teams 在作妖,其它的应用呢?是不是快捷键也变了?

于是我打开了万年不用的 Safari 和 Chrome,hmm……

buer,苹果你这么玩儿是吧?!没事儿就瞎改快捷键是吧?!

行行行~~~你赢了,我改习惯还不行吗?不就是 🌐︎ + F 吗?

盐鹅,一顿操作猛如虎,发现我这「高级」的 Keychron 货,并不是尊贵的苹果键盘,根本没有 🌐︎ 键啊……

自信满满的 QMK + VIA 之旅

那怎么办?简单,咱这可是带 QMK 固件,支持 VIA 的高级货。

理论上来说,简单地修改一下配置,就可以实现 🌐︎ 键的映射。

又天真了不是?!人家苹果的 🌐︎ 键,那可是仅属于苹果的存在,「杂牌货」的 Keychron 不配。

就算在 Keychron 里面配置了 fn 键,也无法在 macOS 上成功激活 🌐︎ 键。

探索之旅

不信邪的我,开始了漫长的寻找解决方案之路……

历代的苹果 Magic Keyboard

寻找的过程中,发现了一些苹果键盘变化的蛛丝马迹。

比如说,第一代的苹果 Magic Keyboard 是没有 🌐︎ 键的,只有 fn 键。如下图:

第一代的苹果 Magic Keyboard

然后到了第二代的时候,苹果在键盘的右上角增加了一个 🌐︎ 键和 fn 键共用的按键。如下图:

第二代的苹果 Magic Keyboard

到了最新的第三代嘛,你懂的,苹果直接把 🌐︎fn 键布置到了键盘的左下角,成为了新的「修饰键」(Modifier Keys)。如下图:

第三代的苹果 Magic Keyboard

至此,恭喜 fn 键成功晋级为 🌐︎ 键,并成为了 macOS 中的「修饰键」(Modifier Keys)。

从此在 macOS 中,和 ControlOptionCommand 一样,拥有了「一席之地」。

macOS 中的「修饰键」(Modifier Keys)

开始找解题思路

看一下 QMK 官方的 Issue 里面就有一个 Add support for globe key (🌐︎︎) for macOS/iPadOS/iOS 的讨论。

发现并没有什么好的解决方案,然后就顺藤摸瓜,找到了 QMK Apple Fn Key 这篇文章。

文中提到的方法,需要修改 USB 设备的 VID 和 PID,伪装成苹果的键盘,才能在 macOS 中正常使用。

个人使用来看似乎没什么问题,但是 VID 和 PID 也是要通过 USB 联盟认证的,直接修改 VID 和 PID 似乎有点不太优雅。

不过在搜索的过程中,发现了一个苹果的 Accessory Design Guidelines 文档,里面提到了苹果的 🌐︎ 键。哎嗨,这不就是我想要的吗?

Hid Consumer Globe Key

又到了快乐编程时间

经过了 N 个小时漫长的代码阅读,终于掌握了 Keychron QMK 实现的精髓。

这里省略一万字的心酸历程,直接上干货。

增加相对应的 Keycode

data/constants/keycodes/keycodes_0.0.2_basic.hjson

原始的 QMK 中,并没有 KC_GLOBE 这个 Keycode,所以需要我们自己增加。

先修改 data/constants/keycodes/keycodes_0.0.2_basic.hjson 文件,增加相对应的 Keycode。

1
2
3
4
5
6
7
8
"0x00C3": {
"group": "media",
"key": "KC_GLOBE",
"label": "Apple Globe/FN Key",
"aliases": [
"KC_GLOB"
]
}

在原文件中,0x00C2KC_LAUNCHPAD 的 Keycode,所以这里我们使用 0x00C3 作为 KC_GLOBE 的 Keycode。

并把 KC_GLOB 作为 KC_GLOBE 的别名。

quantum/keycodes.h

接下来修改 quantum/keycodes.h 文件,在 enum qk_keycode_defines 中增加相对应的 Keycode。

1
2
3
4
5
6
7
enum qk_keycode_defines {
...
KC_GLOBE = 0x00C3, // Added by Tommy for Apple's Globe Key, 2025-02-18
...
KC_GLOB = KC_GLOBE, // Added by Tommy for Apple's Globe Key, 2025-02-18
...
};

还记得苹果的 Accessory Design Guidelines 文档吗?

其中 KC_GLOBE 键对应的 Usage ID 是 0x029D,名称是 AC Keyboard Layout Select,属于 HID Consumer Page

所以我们还需要对应的修改 IS_CONSUMER_KEYCODECONSUMER_KEYCODE_RANGE 的宏定义。

把原来的 KC_LAUNCHPAD(0x00C2) 宏定义替换为数值更高的 KC_GLOBE(0x00C3)。

1
2
#define IS_CONSUMER_KEYCODE(code) ((code) >= KC_AUDIO_MUTE && (code) <= KC_GLOBE) // Added by Tommy for Apple's Globe Key, 2025-02-18
#define CONSUMER_KEYCODE_RANGE KC_AUDIO_MUTE ... KC_GLOBE // Added by Tommy for Apple's Globe Key, 2025-02-18

tests/test_common/keycode_table.cpp

修改 tests/test_common/keycode_table.cpp 文件,增加相对应的 Keycode。

1
2
3
4
5
std::map<uint16_t, std::string> KEYCODE_ID_TABLE = {
...
{KC_GLOBE, "KC_GLOBE"}, // Added by Tommy for Apple's Globe Key, 2025-02-18
...
};

至此,Keycode 部分的添加就算修改完成了。

修改按键触发逻辑

按键 Keycode 的定义有了,接下来就是修改按键触发逻辑了。

HID Consumer 上报

文件:tmk_core/protocol/report.h

要使得新增的 Keycode 可用,我们需要修改 HID Consumer 相关的代码。

本来我还想根据苹果的规范,自己添加映射的,但是发现已经有好心人做好了映射,如下:

1
2
3
4
enum consumer_usages {
// 15.16 Generic GUI Application Controls
AC_NEXT_KEYBOARD_LAYOUT_SELECT = 0x29D,
};

那我们就直接使用这个映射,来修改 KEYCODE2CONSUMER 函数,增加对 KC_GLOBE 按键的支持。

1
2
3
4
static inline uint16_t KEYCODE2CONSUMER(uint8_t key) {
case KC_GLOBE: // Added by Tommy for Apple's Globe Key, 2025-02-18
return AC_NEXT_KEYBOARD_LAYOUT_SELECT;
}

Keychron 部分的实现

因为我使用的是 Keychron 的键盘,所以修改的方式跟直接用 QMK 的不大一样。

需要修改 keychron_common.hkeychron_common.c 这两个文件,来增加对 KC_GLOBE 按键的支持。

文件:keyboards/keychron/common/keychron_common.h

这个很简单,只需要在 enumNEW_SAFE_RANGE 之前增加一个新的按键定义即可。

1
2
3
4
5
enum {
...
KC_APFN, // Added by Tommy for Apple's Globe Key, 2025-02-18
NEW_SAFE_RANGE,
};

需要注意的是 KC_APFN 是我根据 Keychron 的固件代码,自己定义的一个按键。

这个名字不能与之前定义的 KC_GLOBE 相同,因为 KC_GLOBE 是 QMK 的 Keycode,而 KC_APFN 是 Keychron 的 Keycode。

这里一定要区分清楚,不要混淆了,后面在配置键盘 Keymap 的时候,还会用到这个定义。

文件:keyboards/keychron/common/keychron_common.c

这个文件的修改也很简单,只需要在 process_record_user 函数中,增加对 KC_APFN 按键的判断即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
bool process_record_keychron_common(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
...
case KC_APFN: // Added by Tommy for Apple's Globe Key, 2025-02-18
if (record->event.pressed) {
register_code(KC_GLOBE);
} else {
unregister_code(KC_GLOBE);
}
return false; // Skip all further processing of this key
default:
return true; // Process all other keycodes normally
}

这里就是把 Keychron 的 KC_APFN 转换为 QMK 的 KC_GLOBE 并通过 HID Consumer 协议发给系统。

当按键按下的时候,通过 register_code(KC_GLOBE); 注册 KC_GLOBE 按键。

当按键释放的时候,通过 unregister_code(KC_GLOBE); 注销 KC_GLOBE 按键。

特定键盘的配置

经过上面的修改,Keychron 的键盘就可以正常使用 KC_GLOBE 按键了。

但是对于特定的键盘,比如我现在正在使用的 K7 Max,我们还需要修改 Keymap 才能正常使用。

Keymap 配置

Keymap 的配置,根据不同的键盘是不一样的。

我这里以我自己修改的 K7 Max 为例,只需把你想作为 🌐︎ 的按键定义为 KC_APFN 即可。

1
2
3
4
5
6
7
8
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
[MAC] = LAYOUT_ansi_68(
QK_GESC, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_MINS, KC_EQL, KC_BSPC, KC_DEL,
KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS, KC_HOME,
LT(FN1, KC_BSPC), KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, KC_PGUP,
KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT, KC_UP, KC_PGDN,
KC_LCTL, KC_LOPTN, KC_LCMMD, KC_SPC, KC_RCMMD, KC_APFN, MO(KCF), KC_LEFT, KC_DOWN, KC_RGHT),
};

像上面这样,在 KC_RCMMD 后面增加一个 KC_APFN 按键。

在键盘上的位置,就是空格右边 键再右边的 fn1 键,按下去就相当于按下了 🌐︎ 键。

对于只使用 🌐︎ 键切换输入法的朋友,看到这里就可以结束了。

但是对于想要使用 🌐︎ 键作为修饰键的朋友,请继续往下看。

Makefile 配置

文件:keyboards/keychron/k7_max/ansi/rgb/keymaps/tommy/rules.mk

默认情况下,经过上述的修改,我们就可以正常使用 🌐︎ 键了。

但这里有一个小问题,就是 QMK 默认情况下会使用不同的 USB Endpoint 来发送数据。

而苹果会把不同 Endpoint 的数据认为是独立的键盘输入,从而导致 🌐︎ + 其他按键 无法正常工作。

比如说,单独按 🌐︎ 键,可以正常切换输入法。

但是当我按下 🌐︎ + F 键的时候,系统会认为我分别按下了 🌐︎ 键和 F 键,而不是 🌐︎ + F 键。

所以,我们需要增加 KEYBOARD_SHARED_EP = yes 这个配置,让设备以 Shared Endpoint 的方式来发送数据。

这样,苹果会把不同 Endpoint 的数据认为是同一个键盘输入,从而使得 🌐︎ + 其他按键 可以正常工作。

1
KEYBOARD_SHARED_EP = yes

如果不使用 VIA 功能的话,到这里就可以结束了。

但是要想临时改个按键什么的,也不能每次都编译固件吧?

既然来都来了,那么……

VIA 支持

文件:keyboards/keychron/k7_max/via_json/k7_max_ansi_rgb_v1.0.json

其实也很简单,只需要在上述文件的 customKeycodes 字段中,增加 APFn 的定义即可。如下:

1
2
3
4
"customKeycodes": [
...
{"name": "Globe Key", "title": "Apple Globe/FN Key", "shortName": "APFn"}
],

这样我们就可以在 VIA 中看到我们自定义的 APFn,也就是 🌐︎ 按键了。

需要特别注意的是,我们之前编辑过的 Makefile 文件中,必须要有 VIA_ENABLE = yes 这个配置,否则 VIA 功能无法正常工作。

不过一般默认情况下,新的 Keychron 键盘都是默认开启 VIA 功能的。

综合上面的配置,Makefile 文件内容类似如下:

1
2
VIA_ENABLE = yes
KEYBOARD_SHARED_EP = yes

编译固件

我这里选择编译和烧录固件同时进行:

1
make keychron/k7_max/ansi/rgb:tommy:flash

编译成功后,会输出类似如下的信息:

1
2
3
4
5
6
7
8
9
Linking: .build/keychron_k7_max_ansi_rgb_tommy.elf                                                  [OK]
Creating binary load file for flashing: .build/keychron_k7_max_ansi_rgb_tommy.bin [OK]
Creating load file for flashing: .build/keychron_k7_max_ansi_rgb_tommy.hex [OK]

Size after:
text data bss dec hex filename
0 61450 0 61450 f00a keychron_k7_max_ansi_rgb_tommy.bin

Copying keychron_k7_max_ansi_rgb_tommy.bin to qmk_firmware folder [OK]

表示成功生成了可烧录的二进制文件 keychron_k7_max_ansi_rgb_tommy.bin

接下来程序会进入刷机模式,并出现类似的提示:

1
2
3
Flashing for bootloader: stm32-dfu
Bootloader not found. Make sure the board is in bootloader mode. See https://docs.qmk.fm/#/newbs_flashing
Trying again every 0.5s (Ctrl+C to cancel)...

这个时候我们需要把键盘上的开关调整到 Cable 模式,然后按住 Esc 键不放,再重新插入 USB 数据线。

找到 DFU 模式的设备后,程序会自动进入烧录阶段,出现类似如下的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
dfu-util 0.11

Copyright 2005-2009 Weston Schmidt, Harald Welte and OpenMoko Inc.
Copyright 2010-2021 Tormod Volden and Stefan Schmidt
This program is Free Software and has ABSOLUTELY NO WARRANTY
Please report bugs to http://sourceforge.net/p/dfu-util/tickets/

Opening DFU capable USB device...
Device ID 0483:df11
Device DFU version 011a
Claiming USB DFU Interface...
Setting Alternate Interface #0 ...
Determining device status...
DFU state(10) = dfuERROR, status(10) = Device's firmware is corrupt. It cannot return to run-time (non-DFU) operations
Clearing status
Determining device status...
DFU state(2) = dfuIDLE, status(0) = No error condition is present
DFU mode device DFU version 011a
Device returned transfer size 2048
DfuSe interface name: "Internal Flash "
Downloading element to address = 0x08000000, size = 93740
Erase [=========================] 100% 93740 bytes
Erase done.
Download [=========================] 100% 93740 bytes
Download done.
File downloaded successfully
Submitting leave request...
Transitioning to dfuMANIFEST state

EraseDownload 都完成后,程序会自动退出烧录模式,并提示固件烧录成功。

键盘会自动重启,并进入正常工作模式。

验证 VIA 模式

使用 USB 数据线连接键盘和电脑,并把键盘开关调整到 Cable 模式。

打开 Chrome 浏览器,访问 https://usevia.app/,会看到类似如下的界面:

VIA Authorize

点击 Authorize 按钮,选择 Keychron K7 Max 键盘并 Connect

对于第一次使用的用户,我们需要开启 Show Design tab 选项,否则无法看到键盘的配置界面。

VIA Design

不管以前是否有加载过配置的 json 文件,我们都需要重新加载一次,因为我们刚才修改了 json 配置文件。

点击 Load 按钮,选择我们刚才修改的 keyboards/keychron/k7_max/via_json/k7_max_ansi_rgb_v1.0.json 文件。

注意:第一次使用的用户,需要重新 AuthorizeConnect 一次键盘才能正常使用。

然后我们切换到首页,就可以看到我们刚才定义的 APFn 按键,并可以正常配置键位了。

VIA Globe Key

彩蛋

最后,给大家分享一个 🌐︎ 键的小彩蛋。

实际上,虽然 🌐︎ 键与 fn 键共用一个按键,但是它与 fn 键并不相同。

🌐︎ 键仅用于系统快捷键,以下是目前我在新版 macOS 系统上会用到的一些快捷方式:

  • 🌐︎ + A:把焦点转移到 Dock 栏,然后按应用的首字母,并输入 ↩︎ 可以快速切换到该应用。
  • 🌐︎ + ⇧ + A:打开应用程序库(Launchpad),按字母可以快速启动应用。
  • 🌐︎ + D:启动听写
  • 🌐︎ + E:选择 Emoji 表情
  • 🌐︎ + H:显示桌面
  • 🌐︎ + C:显示控制中心
  • 🌐︎ + N:显示通知中心
  • 🌐︎ + Q:启动快速笔记
  • 🌐︎︎ + F:进入全屏模式

源代码

不会吧,不会吧,不会吧,真的有人能坚持看到这里?

好吧,既然你都看到这里了,那么……

你不会真的以为我会跟你分享源代码吧?

恭喜你,答对了!

相关源代码已上传到 GitHub 仓库,感兴趣的朋友可以参考下面的 Commit:

https://github.com/TommyLau/qmk_firmware/commit/f403c031640d618dceeeb73d140ab4bf085ac251

Changelog 如下:

1
2
3
4
5
6
7
8
9
10
11
12
feat: Add support for Apple Globe/FN key (KC_GLOBE)

- Add new keycode KC_GLOBE (0x00C3) for Apple's Globe/FN key
- Implement KC_APFN custom keycode in Keychron common code
- Update keycode tables and ranges to include the new Globe key
- Add Globe key to VIA JSON configuration
- Modify Tommy K7 Max keymap to use the Globe key
- Map Globe key to AC_NEXT_KEYBOARD_LAYOUT_SELECT in USB HID report
- Enable shared endpoint for better macOS compatibility

This change implements proper support for the Apple Globe/FN key,
allowing for better integration with macOS keyboard layout switching.

参考