黑苹果自定义键盘 Fn 快捷键
闲来无聊,想把 95% 完美黑苹果推向 98% 完美,因此在 之前黑苹果驱动的基础 上,通过 SSDT 热补丁修复的方式实现在黑苹果中使用 Fn 快捷键。本文适用于所有黑苹果机型。
从亮度快捷键修复说起
在 OC-little 中有 ThinkPad 现成的亮度快捷键修复补丁,本质上是把 ThinkPad 的 Fn + F5 和 Fn + F6 映射到 F14
和 F15
上,而 F14
和 F15
是 macOS 中「系统偏好设置」中亮度调节的默认快捷键。
为什么这么说呢?让我们先来看一下 SSDT 补丁是怎么写的:
DefinitionBlock("", "SSDT", 2, "OCLT", "BrightFN", 0)
{
External(_SB.PCI0.LPCB.KBD, DeviceObj)
External(_SB.PCI0.LPCB.EC, DeviceObj)
External(_SB.PCI0.LPCB.EC.XQ14, MethodObj)
External(_SB.PCI0.LPCB.EC.XQ15, MethodObj)
Scope (_SB.PCI0.LPCB.EC)
{
Method (_Q14, 0, NotSerialized)//up
{
If (_OSI ("Darwin"))
{
Notify(\_SB.PCI0.LPCB.KBD, 0x0406)
}
Else
{
\_SB.PCI0.LPCB.EC.XQ14()
}
}
Method (_Q15, 0, NotSerialized)//down
{
If (_OSI ("Darwin"))
{
Notify(\_SB.PCI0.LPCB.KBD, 0x0405)
}
Else
{
\_SB.PCI0.LPCB.EC.XQ15()
}
}
}
}
把上述 SSDT 翻译成伪编程语言(人话)。为 _SB.PCI0.LPCB.EC
总线下的设备定义函数 _Q14
:如果当前操作系统是 macOS(Darwin),则向 \_SB.PCI0.LPCB.KBD
设备发送 0x0406
信息;否则,就执行函数 XQ14()
。函数 _Q15
同理。
需要注意的是,使用这个亮度补丁的前提是 DSDT 重命名、将 _Q14
重命名为 XQ14
。也就是说在原始 DSDT 中 _Q14
(也就是 Fn + F5)函数将会被重命名为 XQ14
,只有在非 macOS 操作系统下才会被调用;而在 macOS 中将不会执行 XQ14
(也就是原始的 _Q14
)函数,而是向 \_SB.PCI0.LPCB.KBD
(也就是键盘)发送 0x0406
和 0x10
。
这个 0x0406
其实就是 F15
的扫描码,这个之后再说。只从上述 SSDT 中我们可以得出什么结论呢?
- Fn + F5 和 Fn + F6 对应的是
_SB.PCI0.LPCB.EC
总线下的两个函数,Q15
和Q14
。 - 在 macOS 上,亮度的增减是通过向键盘设备发送一串十六进制实现的。
Q15
和Q14
发送的十六进制就是F14
和F15
,所以 Fn + F5 和 Fn + F6 其实就是F14
和F15
。
找出键盘上所有「额外的」快捷键
如果说 _SB.PCI0.LPCB.EC
下有两个函数实现了亮度快捷键,我们完全有理由推测这个总线下的其他函数定义了其它快捷键。
反编译原始的 DSDT 信息,用 MaciASL 打开,使用 Command + F 搜索 _SB.PCI0.LPCB.EC
,看看有没有别的函数。果然,可以找到许多类似的模式的函数定义:
Scope (\_SB.PCI0.LPCB.EC)
{
Method (_Q63, 0, NotSerialized) // _Qxx: EC Query, xx=0x00-0xFF
{
If (\_SB.PCI0.LPCB.EC.HKEY.MHKK (0x01, 0x00080000))
{
\_SB.PCI0.LPCB.EC.HKEY.MHKQ (0x1014)
}
\UCMS (0x0B)
}
}
那么 _Q63
就是一个快捷键函数(由于 ACPI 的命名必须是 4 位、不足的补下划线 _
,所以在下文中,我都会将形如 _Q63
的函数简称为 Q63
)。如法炮制,找出剩余的函数。
当然在实际操作中,我其实偷了一个懒。我已经知道了 ThinkPad 全线的键盘定义是一致的(由于 OC-little 中提供的亮度快捷键 SSDT 是 ThinkPad 通用的),所以我找了 ThinkPad 其他机型已经做好黑苹果的 EFI,去找他们的 SSDT 中有没有快捷键修复。果然我找到了 ThinkPad X1 Carbon 6th 的
SSDT-KBD.aml
文件,使用 MaciASL 反编译,可以找到 ThinkPad 快捷键有这么几个函数:Q14
、Q15
、Q16
、Q43
、Q60
、Q61
、Q62
、Q64
、Q65
、Q66
。
接下来的问题就是,如何找出每个实体按键和上述函数之间的关系呢?
使用 ACPIDebug 找出快捷键与 ACPI 的映射关系
Rehabman 提供了一系列 DSDT Patch 用于 Debug ACPI 函数。OC-little 将其中的 DSDT Patch 精简为通用的 SSDT 热补丁、可以直接使用。ACPIDebug 的本质是提供一组 ACPI 函数,可以在控制台中输出指定的信息,如同 printf
或 console.log
。我们只需要在需要打印调试信息的地方调用相关函数输出信息即可。
安装 ACPIDebug 的方法很简单,加载 SSDT-RMDT.aml
和内核驱动 ACPIDebug.kext
即可。相对困难的地方在于编写 SSDT 进行调试。
在 OC-little 中的样例 SSDT-BKeyQxx-Debug.dsl
,也给出了打印两个参数的 RMDT
函数的使用示例:
Scope (_SB.PCI0.LPCB.EC0)
{
Method (_QXX, 0, NotSerialized)
{
If (_OSI ("Darwin"))
{
//Debug...
\RMDT.P2 ("ABCD-_PTS-Arg0=", \_SB.PCI9.TPTS)
\RMDT.P2 ("ABCD-_WAK-Arg0=", \_SB.PCI9.TWAK)
//Debug...end
}
Else
{
\_SB.PCI0.LPCB.EC0.XQXX()
}
}
}
注意到 \RMDT.P2 ("ABCD-_WAK-Arg0=", \_SB.PCI9.TWAK)
没有?在 QXX
函数中,调用了 \RMDT.P2
函数打印了两个参数,第一个是 ABCD-_PTS-Arg0=
字符串,第二个是变量 \_SB.PCI9.TPTS
。按下 QXX
函数对应的快捷键、就会执行上述打印函数,就可以在 macOS 控制台 Console.app
中看到 ABCD-_PTS-Arg0=
和 \_SB.PCI9.TPTS
变量的值。
如果你能看懂一些 ACPI 的话,通过 SSDT-RMD
中定义的 \RMDT.P2
函数需要打印两个参数,而 P1
函数只打印一个参数。当然现在我们只需要这个结论就够了。
仿照 OC-little 给出的亮度快捷键补丁和 SSDT-BKeyQxx-Debug.dsl
的例子,编写如下 SSDT:
// 需要注意的是,注释里的中文只是解释说明
// 在实际编写时注释里不能有中文
DefinitionBlock("", "SSDT", 2, "OCLT", "ACPIDebug", 0) // 我们的表名是 ACPIDebug
{
External(_SB.PCI0.LPCB.KBD, DeviceObj) // 引用外部定义 KBD,以你机器中 DSDT 中的为准
External(_SB.PCI0.LPCB.EC, DeviceObj) // 引用外部定义 EC,以你机器中 DSDT 中的为准
External(_SB.PCI0.LPCB.EC.XQ14, MethodObj) // 引用外部定义的 XQ14 函数
External(RMDT.P1, MethodObj) // 引用外部定义的 RMDT.P1 函数
Scope (_SB.PCI0.LPCB.EC)
{
Method (_Q14, 0, NotSerialized)
{
If (_OSI ("Darwin"))
{
\RMDT.P1 ("SUKKA_DEBUG_KEYBOARD-Q14") // 打印一个参数:字符串 SUKKA_DEBUG_KEYBOARD-Q14
}
Else
{
\_SB.PCI0.LPCB.EC.XQ14()
}
}
}
}
然后,继续在文件头部使用 External
添加对 XQ15
函数的外部定义,并仿照 _Q14
函数,编写剩余的快捷键函数定义。当然别忘了还需要在 config.plist
中添加 ACPI 重命名,将 _Q14
等重命名为 XQ14
等、以避免冲突。最后 SSDT 类似下图所示:
重启以加载上述 SSDT,然后打开 macOS 控制台,在右上角搜索框中输入 SUKKA_DEBUG_KEYBOARD
并回车,过滤出只包含指定字符串的信息。
接着,按下 Fn + F5 ,看看控制台中是否会打印信息:
打印出 SUKKA_DEBUG_KEYBOARD-Q14
,表示 Fn + F5 就是 Q14
。继续按下其他快捷键,根据打印信息找出每一个快捷键分别对应的函数:
这里列出我用上述方法找到的 ThinkPad 键盘的函数:
- Fn + F1 =
Q43
- Fn + F5 =
Q15
- Fn + F6 =
Q14
- Fn + F7 =
Q16
- Fn + F8 =
Q64
- Fn + F9 =
Q66
- Fn + F10 =
Q60
- Fn + F11 =
Q61
- Fn + F12 =
Q62
- Fn + PrtScreen =
Q65
学习 PS2 和 ABD 键码
在 OC-little 中的「PS2 键盘映射」章节中指出,一个按键会产生两种扫描码,分别是 PS2 扫描码 和 ADB 扫描码。在 ApplePS2ToADBMap.h
文件中可以找到原始的 ADB 扫描码和 PS2 扫描码之间的对应关系。
如果你的键盘是使用 VoodooPS2Controller
驱动的,可以使用 Rehabman 开发的 ioio
工具获取每个按键的键码。下载 并解压 ioio
工具,在终端运行下述命令查看按键的扫描码:
ioio -s ApplePS2Keyboard LogScanCodes 1
回到刚才打开的 macOS 控制台,删去右上角搜素框中所有字符,输入 PS2
并回车。
Tips:如果控制台有很多信息,可以用顶部的按钮清理。
按下 F1 键,可以看到控制台打印出如下扫描码:
让我们看看 3b=7a
,等于号左边的 3b
是 PS2 扫描码,等于号右边的 7a
是 ADB 扫描码。还记得前文提到的 ApplePS2ToADBMap.h
文件么?看看在其中我们能不能找到什么:
0x7a, // 3b F1
啊,0x7a
对应的 3b
,按键是 F1!
还记得前文说的「 0x0406
就是 F15
的扫描码」么?让我们读读文件:
// These ADB codes are for F14/F15 (works in 10.12)
#define BRIGHTNESS_DOWN 0x6b
#define BRIGHTNESS_UP 0x71
BRIGHTNESS_DOWN, // e0 05 dell down
BRIGHTNESS_UP, // e0 06 dell up
啊哈!0x6b
是 F15
是 ADB 的扫描键码,同时又和 e0 06
是对应的。因此,0x0406
对应 e0 06
、0x0405
对应 e0 05
,最后两位都是相同的。这是不是巧合呢?肯定不是。
在这里我直接说结论。0x0406
中 04
指的就是 PS2 扫描码中的 e0
(即扩展键码),06
就是后两位。除了可以取 04
以外,还可以取 0x03
,表示 PS2 扫描码只有 2 位的。比如 F1 的 PS2 扫描码 3b
,就可以表示为 0x033b
。
再让我们回头来看看 OC-little 中提供的亮度快捷键 SSDT 补丁:
Scope (_SB.PCI0.LPCB.EC)
{
Method (_Q14, 0, NotSerialized)//up
{
If (_OSI ("Darwin"))
{
Notify(\_SB.PCI0.LPCB.KBD, 0x0406)
}
Else
{
\_SB.PCI0.LPCB.EC.XQ14()
}
}
}
所以按下 Fn + F6 ,ACPI 就会执行 _Q14
函数、向键盘 KBD
发送 0x0406
,翻译为 ADB 扫描码就是 e0 06
,对应 PS2 扫描码中的 0x71
、也就是 F15
,正好是系统偏好设置中的增加显示器亮度:
编写 SSDT 定义快捷键
还记得第一章节的第一句话是怎么说的么?
在 OC-little 中有 ThinkPad 现成的亮度快捷键修复补丁,本质上是把 ThinkPad 的 Fn + F5 和 Fn + F6 映射到
F14
和F15
上,而F14
和F15
是 macOS 中「系统偏好设置」中亮度调节的默认快捷键。
那么,我们可以用同样的方法,将 Fn + Fx 键分别映射 F13
、F14
、F15
一直到 F21
。然后在「系统偏好设置」或者第三方快捷键软件中为 F13
、F14
等按键定义操作。
首先列一张表将每个键、以及键码都对应起来。
原始按键 - 按键图标 - ACPI 函数 - 映射按键 - PS2 扫描码(十六进制)- ADB 扫描码
Fn + F1 - 静音 - Q43 - 静音 - e020 (0x0420) - 4a
Fn + F4 - 麦克风开关 - Q6A - F13 - 64 (0x0364) - d9
Fn + F5 - 亮度减 - Q15 - F14 - e005 (0x0405) - 6b
Fn + F6 - 亮度加 - Q14 - F15 - e006 (0x0406) - 71
Fn + F7 - 多屏幕 - Q16 - F16 - 67 (0x0367) - 6a
Fn + F8 - WIFI 开关 - Q64 - F17 - 68 (0x0368) - 40
Fn + F9 - 太阳 - Q66 - F18 - 69 (0x0369) - 4f
Fn + F10 - 蓝牙开关 - Q60 - F19 - 6a (0x036A) - 50
Fn + F11 - 键盘 - Q61 - F20 - 6b (0x036B) - 5a
Fn + F12 - 星星 - Q62 - F21 - 6c (0x036C) - DEADKEY
PrtScr - 截图 - N/A - F22 - e037 (0x0437) - 64
Fn + PrtScr - ThinkPad 触摸板开关 - Q65 - N/A - e01e (0x041e) - N/A
接下来,模仿亮度快捷键补丁的方式,按照上述表编写 SSDT:
// 需要注意的是,注释里的中文只是解释说明
// 在实际编写时注释里不能有中文
DefinitionBlock("", "SSDT", 2, "HACK", "Keyboard", 0)
{
External(_SB.PCI0.LPCB.KBD, DeviceObj) // 对键盘设备的外部引用
External(_SB.PCI0.LPCB.EC, DeviceObj) // 对 EC 总线的外部引用
External(_SB.PCI0.LPCB.EC.XQ43, MethodObj) // 对 XQ43 函数的引用
Scope (_SB.PCI0.LPCB.EC)
{
Method (_Q43, 0, NotSerialized) // Q43 函数
{
If (_OSI ("Darwin")) // macOS
{
Notify(\_SB.PCI0.LPCB.KBD, 0x0420) // 发送 PS2 扫描码 e020
}
Else // 非 macOS
{
\_SB.PCI0.LPCB.EC.XQ43() // 执行 XQ43 函数
}
}
}
}
然后依次添加原始函数的外部引用、依次添加 Notify
函数向键盘发送 PS2 键码。
需要注意的是,像 PrtScr 这种不是额外的快捷键,是不存在对应的 ACPI 函数的。我们可以使用 Custom PS2 Map 或者 Custom ADB Map 的方式进行映射:
Name(_SB.PCI0.LPCB.KBD.RMCF, Package()
{
"Keyboard", Package()
{
"Custom PS2 Map", Package()
{
Package(){},
"e037=64", // PrtSc = F13
},
// "Custom ADB Map", Package()
// {
// Package(){},
// "1e=06", // A = Z
// },
},
})
在这里我们需要了解一下 Custom PS2/ADB Map 的规则。等于号左边的永远是按下的按钮,等于号右边永远是原始的定义。
听不懂?我们来看这么个例子:
"Custom PS2 Map", Package()
{
Package(){},
"1e=2c",
"2c=1e"
}
其中,1e
是 A 的 PS2 扫描码,2c
是 Z 的 PS2 扫描码。所以 1e=2c
表示,按下 A 后会触发 2c
,而 2c 原始的定义是 Z ,因此输出字母 Z;同理,按下 Z 后,2c
被映射到 1e
、也就是原始的 A ,所以输出的是字母 A。
使用快捷键禁用触控板(还有 ThinkPad 小红点)
在使用上述 SSDT 将 e037
(PrtSc)映射到 6d
( F13
)之前,e037
其实是一个特殊的键码、用来开关 Trackpad(触控板)设备(在 ThinkPad 上,小红点也属于 Trackpad 设备)。虽然很少有人会用 macOS(特别是 Hackintosh)的笔记本玩游戏、因此没有禁用触控板的必要,但是凭着「我可以不用,你不能没有」的精神,我们还是希望能有快捷键可以用于关闭触控板、只是不能是 PrtSc 罢了。
就像前文所说,我们可以把 A 映射到 Z 的同时还把 Z 映射到 A;同理也可以先将一个按键映射到 e037
用于开关触控板,再将 PrtSc (e037
)映射到其它键(如 F13
)。
不过现在,我想把 Fn + F11 (ThinkPad 上 F11 画了一个键盘)映射到 e037
上,而 Fn + F11 由于是额外的快捷键、是没有 PS2 扫描码的,我该怎么办呢?答案是「无中生码」。
首先需要找一个我们用不到的 PS2 扫描码。再回头去看看 ApplePS2ToADBMap.h
去选择一个不是 DEADKEY
、同时键盘上又用不到的 PS2 扫描码。在这里我选的是 e01e
。我已经知道了 Fn + F11 对应的 ACPI 函数是 Q61
,因此在 Q61
函数中触发 e01e
即可:
Method (_61, 0, NotSerialized)
{
If (_OSI ("Darwin"))
{
Notify(\_SB.PCI0.LPCB.KBD, 0x041e) // e01e
}
Else
{
\_SB.PCI0.LPCB.EC.XQ61()
}
}
现在,如果按下 Fn + F11 就会发送 PS2 扫描码 e01e
。 接下来我们只要在 Custom PS2 Map 中分别定义两个映射即可:
Name(_SB.PCI0.LPCB.KBD.RMCF, Package()
{
"Keyboard", Package()
{
"Custom PS2 Map", Package()
{
Package(){},
"e01e=e037", // Fn + F11 = PrtSc
"e037=64", // PrtSc = F13
},
},
})