从亮度快捷键修复说起

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 060x0405 对应 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
        },
    },
})