跳转至

触控板中断示例

前言

当你看到这部分内容的时候,表示你黑苹果已经研究的比较深了,触控板工作在轮询模式下已经无法满足你了,非得搞个 GPIO 中断才满意。实际上这块内容搬运自我博客的这篇文章:双屏笔记本在 macOS 下的触控板驱动研究

由于文章比较长,所以就从里面提起关键的部分放到了这个教程里面,感兴趣的朋友也可以去原文观看一下。下面直接开始触控板中断教程的部分。

硬件信息

中断之前先收集一下自己触控板的基础信息,先在 Windows 下对触控板信息进行一个简短的整理,使用的是标准的符合微软 I2C 协议触控板:

在 macOS 下使用IORegistryExplorer也是可识别到触控板的型号为:GDX1515

将以上可能有用的信息整理出来如下:

  • Windows 下这种二合一触控板叫法:ScreenPad
  • 触控板型号:GDX1515
  • 触控板的屏幕型号:ScreenXpert
  • IRQ0x0000006D (109)
  • APIC Pin6d(109)
  • 设备实例路径ACPI\GDX1515\1
  • 硬件 Id
  • ACPI\VEN_GDX&DEV_1515
  • ACPI\GDX1515
  • *GDX1515
  • BIOS 设备名称\_SB.PCI0.I2C1.ETPD
  • 位置路径
  • ACPI(_SB)#ACPI(PCI0)#(I2C1)#ACPI(ETPD)
  • ACPI(_SB)#ACPI(USBX)#(I2C1)#ACPI(ETPD)

触控板的工作模式

比较完美的情况是让触控板工作在中断模式(interrupts)下,关于轮询和中断可以参考下面几个概念:

  • APIC 中断
  • macOS 使用的中断模式,功能完美,极少数设备支持
  • 只有 APIC Pin 值小于 2F (47)的时候才支持
  • GPIO 中断
  • Windows 系统下大多使用的中断模式
  • 仅次于 APIC 中断,比较高效,但是需要自己更改定制 SSDT
  • 轮询
  • 比较低效的模式
  • 但是兼容性比较好,大部分触控板都适用
  • 容易出现指针漂移等不灵敏的 BUG

那我们目前的触控板工作在啥模式情况下呢?通过前面的信息收集,我们知道触控板的 APIC Pin 值为:

  • IRQ0x0000006D (109)
  • APIC Pin6d(109)

判断触控板的工作模式

那么如何判断当前触控板的工作模式呢?目前有下面 3 种方法:

查看 dmesg 日志

顺利的情况下使用 dmesg 命令可以直接根据触控板型号的关键词可以搜索到中断还是轮询的工作状态:

但是黑苹果总不会一帆风顺的,正如你所见,dmesg 查看的日志结果为空的,这是因为 macOS 12.0 系统限制了 dmesg 查看的内容,我们这里需要手动加载 kexts 来查看日志。

关闭 SIP

很多 OC 的配置默认是开启 SIP 的,因为我们要加载自己的 Kexts,所以需要在开机选择系统的界面手动切换下 SIP 状态:

如果你的 OC 没有这个选项,是因为没有勾选这个配置开关,具体细节可以参考我的这部分文章:国光的黑苹果安装教程:手把手教你配置 OpenCore

OC 里面禁用 Kexts

通过 OpenCore 注入的 kexts 是无法正常看到 dmesg 日志的,所以我们需要手动禁用下相关的 kexts:

手动加载 Kexts

VoodooI2C.kextVoodooI2CHID.kext 拷贝到桌面上,然后执行以下命令:

# 修改所有者
sudo chown -R root:wheel VoodooI2C*

# 修改权限
sudo chmod -R 755 VoodooI2C*

# 加载 kexts
sudo kextload -v VoodooI2C*

当然第一次加载 kexts 得在设置的「安全性与隐私」里面同意下权限,然后重启下电脑才可以成功加载:

经过上述折腾,最终查看的效果如下:

可以看到通过 dmesg 日志可以发现此 GDX1515 触控板工作在 polling 模式下,即轮询模式。

关于日志的查看细节,下面会单独开个子目录详细介绍。

使用 IORegistryExplore

上面的方法可能有点繁琐,实际上使用IORegistryExplorer也是直接看到当前触控板的工作状态的,因为在 Windows 的设备管理器下我的触控板的位置路径为:

  • ACPI(_SB)#ACPI(PCI0)#(I2C1)#ACPI(ETPD)
  • ACPI(_SB)#ACPI(USBX)#(I2C1)#ACPI(ETPD)

所以搜索 ETPD 可以看到我们触控板的详细信息,一般会搜索出两个结果,下面是第一个结果:

不过第一个结果没有参考价值,我们一般关注搜索出来的第 2 个结果:

像上面这个图就是一个典型的没有工作在中断模式的情况。

这个时候肯定有网友会问,如果是 GPIO 中断模式的话,那这里应该长啥样子呢?

2333 这个问题问的不错,国光请教了 Bat.bat 大佬,下面是大佬的原话:


代码里走到 GPIO 会有个 set property 把 IRQ 和 Pin 写到 ioreg 下,所以使用 IORegistryExplorer 查看的话,关注是否有这两个新增的属性即可。


说到这里可能还有网友不是很明白,下面国光我帖一个处于 GPIO 中断模式下的 IORegistryExplorer 截图,大家应该就懂了:

定制修补 DSDT

提取 DSDT

因为制作触控板肯定是需要定制 SSDT 的,所以提取出主板原始的 DSDT 少不了。

提取 DSDT 的方法有很多,可以使用 Clover,Windows 下可以使用 AIDA64,macOS 下可以使用 DPCIManager ,直接打开点击左上角的「Extract DSDT」即可:

DSDT 排错

可以使用 OC 直接在 ACPI 里面加载 DSDT,也可以将 DSDT 里面触控板的相关代码单独提取出来,保存为 SSDT 文件。两个方法都可以,这里国光我是直接在 DSDT 这里修改的(简单粗暴)。

DSDT 排错的话这里要使用必备的 MaciASL 这个软件,直接点击「Compile」编译即可,如果没猜错的话肯定会有一堆报错:

这是因为每家主板的 DSDT 的 ACPI 规范不统一,所以我们得根据自己的编程经验去修改报错,这块不太好写文章描述出来,因为每个主板都不一样,国光我这里的报错还算比较容易理解,我直接删掉了这些Zero 代码即可,23333(具体排错得根据对应报错情况来)

最终成功 0 errors 了:

DSDT 没问题的话就可以直接使用 OC 加载了:

寻找触控板代码

因为根据前面的信息收集,我们知道了触控板的路径为 ETPD,所以直接搜索 ETPD 即可找到触控板的代码,

发现左下角的路径也满足我们上面信息收集(ACPI(_SB)#ACPI(PCI0)#(I2C1)#ACPI(ETPD))的情况:

下面将触控板的代码部分贴出来:

Scope (_SB.PCI0.I2C1)
    {
        Device (ETPD)
        {
            Name (_ADR, One)  // _ADR: Address
            Name (ETPH, Package (0x01)
            {
                "ASUE1407"
            })
            Name (FTPH, Package (0x09)
            {
                "FTE1001", 
                "FTE1200", 
                "FTE1200", 
                "FTE1300", 
                "FTE1300", 
                "FTE1201", 
                "FTE1200", 
                "FTE1200", 
                "FTE1200"
            })
            Name (GTPH, Package (0x05)
            {
                "GDX1505", 
                "GDX1300", 
                "GDX1200", 
                "GDX1301", 
                "GDX1515"
            })
            Method (_HID, 0, NotSerialized)  // _HID: Hardware ID
            {
                If (And (TPDI, 0x04))
                {
                    Return (DerefOf (Index (ETPH, TPHI)))
                }

                If (And (TPDI, 0x10))
                {
                    Return (DerefOf (Index (FTPH, TPHI)))
                }

                If (And (TPDI, 0x40))
                {
                    Return (DerefOf (Index (GTPH, TPHI)))
                }

                Return ("ELAN1010")
            }

            Name (_CID, "PNP0C50")  // _CID: Compatible ID
            Name (_UID, One)  // _UID: Unique ID
            Name (_S0W, 0x03)  // _S0W: S0 Device Wake State
            Method (_DSM, 4, NotSerialized)  // _DSM: Device-Specific Method
            {
                If (LEqual (Arg0, ToUUID ("3cdff6f7-4267-4555-ad05-b30a3d8938de") /* HID I2C Device */))
                {
                    If (LEqual (Arg2, Zero))
                    {
                        If (LEqual (Arg1, One))
                        {
                            Return (Buffer (One)
                            {
                                 0x03                                           
                            })
                        }
                        Else
                        {
                            Return (Buffer (One)
                            {
                                 0x00                                           
                            })
                        }
                    }

                  If (LEqual (Arg2, One))
                    {
                        Return (One)
                        }
                    }
                    Else
                    {
                        Return (Buffer (One)
                        {
                             0x00                                           
                        })
                    }
                }
          Method (_STA, 0, NotSerialized)  // _STA: Status
          {
                      If (LOr (LNotEqual (TPIF, One), LAnd (DSYN, One)))
                      {
                          Return (Zero)
                      }

                      Return (0x0F)
               }

          Method (_CRS, 0, Serialized)  // _CRS: Current Resource Settings
          {
                Name (SBFI, ResourceTemplate ()
                {
                    I2cSerialBusV2 (0x0015, ControllerInitiated, 0x00061A80,
                        AddressingMode7Bit, "\\_SB.PCI0.I2C1",
                        0x00, ResourceConsumer, , Exclusive,
                        )
                    Interrupt (ResourceConsumer, Level, ActiveLow, Exclusive, ,, )
                    {
                        0x0000006D,
                    }
                })
                Return (SBFI)
            }
        }
    }
}

GPIO Pinning 固定

国光这里只介绍如何定制 GPIO 中断,轮询和其他模式不在本文的范畴内。

寻找触控板关键代码

根据_CRS方法这个特征,我们可以很容易直找到触控板代码中类似如下的代码片段:

Method (_CRS, 0, Serialized)  // _CRS: Current Resource Settings
{
  Name (SBFI, ResourceTemplate ()
        {
          I2cSerialBusV2 (0x0015, ControllerInitiated, 0x00061A80,
                          AddressingMode7Bit, "\\_SB.PCI0.I2C1",
                          0x00, ResourceConsumer, , Exclusive,
                         )
            Interrupt (ResourceConsumer, Level, ActiveLow, Exclusive, ,, )
          {
            0x0000006D,
          }
        })
    Return (SBFI)
}

重命名 SBFI

VoodooI2C 在以 GPIO 中断模式调用 DSDT 中触摸设备的_CRS 方法时,一律使用 SBFG 参数而不是 SBFI 参数。所以我们目前的触控板代码里面的 SBFI 变量是不满足要求的,我们先把 SBFI重命名为 SBFB,至于 SBFG 变量我们后面单独添加一下(如果你的 DSDT 里面没有的话)。

Method (_CRS, 0, Serialized)  // _CRS: Current Resource Settings
{
  Name (SBFB, ResourceTemplate ()
        {
          I2cSerialBusV2 (0x0015, ControllerInitiated, 0x00061A80,
                          AddressingMode7Bit, "\\_SB.PCI0.I2C1",
                          0x00, ResourceConsumer, , Exclusive,
                         )
  //         Interrupt (ResourceConsumer, Level, ActiveLow, Exclusive, ,, )

  //        {
  //          0x0000006D,
  //        }
        })
    Return (SBFB)
}

重命名后移除 _CRS 方法中的以下内容(即上面代码注释的部分):

Interrupt (ResourceConsumer, Level, ActiveLow, Exclusive, ,, )
{
  0x0000006D,
}

寻找 GPIO Pin

在触控板的代码中寻找类似如下的代码片段:

Name (SBFG, ResourceTemplate ()
{
    GpioInt (Level, ActiveLow, ExclusiveAndWake, PullDefault, 0x0000,
        "\\_SB.PCI0.GPI0", 0x00, ResourceConsumer, ,
    )
  {   // Pin list
      0x0000
  }
})

如果找到的话,那么恭喜你,你的设备可能都不需要计算 GPIO Pin 值。没有找到也没有关系,我们可以手动添加,具体参考下面内容。

添加 GPIO 模板

很明显我的触控板搜索不到 GPIO 相关的代码,所以我们需要复制上面的代码片段,将其添加到 _CRS 方法下面:

Method (_CRS, 0, Serialized)  // _CRS: Current Resource Settings
{
  Name (SBFB, ResourceTemplate ()
        {
          I2cSerialBusV2 (0x0015, ControllerInitiated, 0x00061A80,
                          AddressingMode7Bit, "\\_SB.PCI0.I2C1",
                          0x00, ResourceConsumer, , Exclusive,
                         )
        })
    // 下面是新增的 GPIO 模板代码片段
    Name (SBFG, ResourceTemplate ()
          {
            GpioInt (Level, ActiveLow, ExclusiveAndWake, PullDefault, 0x0000,
                     "\\_SB.PCI0.GPI0", 0x00, ResourceConsumer, ,
                    )
            {   // Pin list
              0x0000   // 这个值 我们待会要计算一下
            }
          })
    Return (SBFB)
}

修改 _CRS 返回值

因为我们 _CRS 方法里面还引入了 SBFG 变量,所以得将默认的 Return (SBFB) 返回值修改为:

Return (ConcatenateResTemplate (SBFB, SBFG))

Bat.bat 大佬原话:有 gpioint 的设备不用算 pin,没有的加上也基本上不可能成功,算 pin 其实是最后死马当活马医的方案。

计算 GPIO Pin 值

我们上面添加的模板中,GPIO Pin 值为 0x0000,这个一般是跑不起来的,只能起占位的作用,所以需要计算一个正确的 GPIO 值。这一步比较关键,不同的 CPU 有不同的计算公式,下面的这个公式也是 Bat.bat 大佬提供的:

  • Skylake(intel 6 代 CPU)
If APICPIN > 47 And APICPIN <= 79 Then     
    GPIOPIN = APICPIN - 24   
    GPIOPIN2 = APICPIN + 72  
ElseIf APICPIN > 79 And APICPIN <= 119 Then
    GPIOPIN = APICPIN - 24
End If
  • CoffeeLake-H(intel 8 代标压 CPU)
If APICPIN > 47 And APICPIN <= 71 Then   
    GPIOPIN = APICPIN - 16   
    GPIOPIN2 = APICPIN + 240 
    If APICPIN > 47 And APICPIN <= 59 Then GPIOPIN3 = APICPIN + 304  
ElseIf APICPIN > 71 And APICPIN <= 95 Then 
    GPIOPIN = APICPIN - 8    
    GPIOPIN3 = APICPIN + 152
    GPIOPIN2 = APICPIN + 120 
ElseIf APICPIN > 95 And APICPIN <= 119 Then 
    GPIOPIN = APICPIN        
If APICPIN > 108 And APICPIN <= 115 Then 
    GPIOPIN2 = APICPIN + 20 
End If
  • CoffeeLake-LF 和 Whiskylake(intel 8 代低压 CPU 和 Whiskylake 架构 CPU)
If APICPIN > 47 And APICPIN <= 71 Then      
    GPIOPIN = APICPIN - 16   
    GPIOPIN2 = APICPIN + 80  
ElseIf APICPIN > 71 And APICPIN <= 95 Then  
    GPIOPIN2 = APICPIN + 184 
    GPIOPIN = APICPIN + 88   
ElseIf APICPIN > 95 And APICPIN <= 119 Then 
    GPIOPIN = APICPIN        
ElseIf APICPIN > 108 And APICPIN <= 115 Then 
    GPIOPIN2 = APICPIN - 44 
End If

我的笔记本 CPU 是 i7-10510U,属于 Comet Lake 的低压 CPU,会发现上述公式并没有 Comet Lake 系列的,不过 Bat.bat 大佬说 10 代就是 8 代的马甲,所以使用 CoffeeLake-LF 和 Whiskylake 公式即可,带入公式可计算出我们的 GPIO Pin 的十进制:

我们的 APICPIN 的 16进制为 6d,转换为 10 进制为 109,满足公式如下条件:

ElseIf APICPIN > 95 And APICPIN <= 119 Then 
    GPIOPIN = APICPIN    // GPIOPIN = 109

即 GPIOPIN = 109,转换为 16 进制就是 6d

同时会发现我们的 APICPIN 也满足如下条件:

ElseIf APICPIN > 108 And APICPIN <= 115 Then 
    GPIOPIN2 = APICPIN - 44  // GPIOPIN2 = 109 - 44 = 65

即GPIOPIN = 65,转换为 16 进制就是 41

可以看到我们算出了 2 个值,分别是 6d41,得一个个尝试去验证就可以了。

在极少数情况下,计算出来的 GPIO Pin 值不起作用。在这种情况下,你可以尝试一些常见的值:0x170x1B0x340x55

最终国光尝试了我的这个 GDX1515 触控板的 GPIO Pin 值为 6d

放一个贴图,这样大家看起来应该会比较直观一点。

最终效果

使用IORegistryExplorer 可以看到我们的这个 GDX1515 冷门小众的触控板终于工作在 GPIO 中断模式下了,可以看到出现了 gpioPingpioIRQ 属性值,完美:

img

再来看下 dmesg 的日志情况:

# 发现了 GDX1515 的 I2C 协议的触控板
[   44.313108]: VoodooI2CControllerDriver::pci8086,2e9 Found I2C device: GDX1515

# ETPD 发现了有效的 _CRS 方法
[   44.313178]: VoodooI2CDeviceNub::ETPD Found valid resources from _CRS method
[   44.313182]: VoodooI2CControllerDriver::pci8086,2e8 Got bus configuration values
[   44.313231]: VoodooI2CDeviceNub::ETPD Returned index 0x0 from _DSM or XDSM method is not supported
[   44.313235]: VoodooI2CDeviceNub::ETPD Could not retrieve resources from _DSM or XDSM method

# ETPD 发现了有效的 GPIO 中断
[   44.313244]: VoodooI2CDeviceNub::ETPD Found valid GPIO interrupts
[   44.313344]: VoodooI2CControllerDriver::pci8086,2e8 Publishing device nubs

# ETPD 得到 GPIO 控制器
[   44.313347]: VoodooI2CDeviceNub::ETPD Got GPIO Controller! VoodooGPIOCannonLakeLP
[   44.816012]: VoodooI2CHIDDevice:0x100000738 start

# GDX1515 设备启动的重置完成
[   44.919729]: VoodooI2CHIDDevice::GDX1515 Device initiated reset accomplished
[   45.050029]: VoodooI2CHIDDevice:0x100000738 creating interfaces
[   45.051068]: VoodooI2CHIDDevice:0x100000738 Matching has vendor DeviceUsagePage : ff0c bundleIdentifier com.apple.AppleUserHIDDrivers ioclass AppleUserHIDEventService but transport and vendorID is missing
[   45.053582]: VoodooI2CPrecisionTouchpadHIDEventDriver:0x10000073d start
[   45.059693]: open by VoodooI2CPrecisionTouchpadHIDEventDriver 0x10000073d (0x0)

# GDX1515 进入  Precision Touchpad Mode (PTP)模式,即高精度触控板模式
[   45.059739]: VoodooI2CPrecisionTouchpadHIDEventDriver::GDX1515 Putting device into Precision Touchpad Mode

从日志中也可以看到完美工作了,工作在 GPIO 中断模式下。

为了有对比,下面再附上没有工作在 GPIO 中断模式下的日志情况:

  • 没有定制 DSDT 在轮询下的日志
[   48.517816]: VoodooI2CControllerDriver::pci8086,2e9 Found I2C device: GDX1515
[   48.517869]: VoodooI2CDeviceNub::ETPD Found valid resources from _CRS method
[   48.517913]: VoodooI2CDeviceNub::ETPD Returned index 0x0 from _DSM or XDSM method is not supported
[   48.517917]: VoodooI2CDeviceNub::ETPD Could not retrieve resources from _DSM or XDSM method

# ETPD 警告,发现了不兼容的 APIC pin 值 6d,它是大于 2f 的
[   48.517925]: VoodooI2CDeviceNub::ETPD Warning: Incompatible APIC interrupt pin (0x6d > 0x2f)

# ETPD 找不到任何 APIC 或 GPIO 中断。 您可能将在轮询模式下运行。
[   48.517931]: VoodooI2CDeviceNub::ETPD Warning: Could not find any APIC nor GPIO interrupts. Your chosen satellite will run in polling mode if implemented.
[   48.519529]: VoodooI2CHIDDevice:0x100000725 start

# GDX1515 无法获取中断事件源,改用轮询模式
[   48.519539]: VoodooI2CHIDDevice::GDX1515 Warning: Could not get interrupt event source, using polling instead
[   48.722472]: VoodooI2CHIDDevice::GDX1515 Device initiated reset accomplished
[   48.854707]: VoodooI2CHIDDevice:0x100000725 creating interfaces
[   48.856351]: VoodooI2CHIDDevice:0x100000725 Matching has vendor DeviceUsagePage : ff0c bundleIdentifier com.apple.AppleUserHIDDrivers ioclass AppleUserHIDEventService but transport and vendorID is missing
[   48.860549]: VoodooI2CPrecisionTouchpadHIDEventDriver:0x10000072a start
[   48.866671]: open by VoodooI2CPrecisionTouchpadHIDEventDriver 0x10000072a (0x0)

# GDX1515 进入  Precision Touchpad Mode (PTP)模式,即高精度触控板模式
[   48.866716]: VoodooI2CPrecisionTouchpadHIDEventDriver::GDX1515 Putting device into Precision Touchpad Mode
  • 定制了 DSDT,但 GPIO 不正确的日志
[   37.835221]: VoodooI2CControllerDriver::pci8086,2e9 Found I2C device: GDX1515
[   37.835266]: VoodooI2CDeviceNub::ETPD Found valid resources from _CRS method
[   37.835298]: VoodooI2CDeviceNub::ETPD Returned index 0x0 from _DSM or XDSM method is not supported
[   37.835306]: VoodooI2CDeviceNub::ETPD Could not retrieve resources from _DSM or XDSM method
[   37.835309]: KextLog: AuxKC bundle com.alexandred.VoodooI2CServices marked as loadable

# ETPD 发现了有效的 GPIO 中断
[   37.835312]: VoodooI2CDeviceNub::ETPD Found valid GPIO interrupts
[   37.835408]: VoodooI2CDeviceNub::ETPD Got GPIO Controller! VoodooGPIOCannonLakeLP
[   38.337232]: VoodooI2CHIDDevice:0x100000761 start

# 无法获取 GPIO 引脚的硬件引脚 81(错误的 GPIO Pin 的10进制),无法获取中断事件源,改用轮询
[   38.337263]: VoodooGPIOCannonLakeLP::Failed getting hardware pin for GPIO pin 81VoodooI2CHIDDevice::GDX1515 Warning: Could not get interrupt event source, using polling instead
[   38.539715]: VoodooI2CHIDDevice::GDX1515 Device initiated reset accomplished
[   38.670485]: VoodooI2CHIDDevice:0x100000761 creating interfaces
[   38.672136]: VoodooI2CHIDDevice:0x100000761 Matching has vendor DeviceUsagePage : ff0c bundleIdentifier com.apple.AppleUserHIDDrivers ioclass AppleUserHIDEventService but transport and vendorID is missing
[   38.676469]: VoodooI2CPrecisionTouchpadHIDEventDriver:0x100000766 start
[   38.682612]: open by VoodooI2CPrecisionTouchpadHIDEventDriver 0x100000766 (0x0)

# GDX1515 进入  Precision Touchpad Mode (PTP)模式,即高精度触控板模式
[   38.682668]: VoodooI2CPrecisionTouchpadHIDEventDriver::GDX1515 Putting device into Precision Touchpad Mode

炫耀一下

双屏笔记本真的很帅,等我最近做一期视频记录一下,到时候也又可以去 B 站水了:

另外有对这个设备感兴趣的朋友,也欢迎进群艾特我哦,做完视频就准备出掉了,价格不贵,16+512GB MX250 双屏只要 4-4.5k元,可小刀很香哦。

支持一下?

在这个喧嚣浮躁的时代,坚持写博客输出原创文章的人还有多少呢?写博客感觉一直是用爱发电的状态......

如果你恰巧财力雄厚,感觉本文对你有所帮助的话,可以考虑打赏一下本文,用以维持高昂的服务器运营费用(域名费用、服务器费用、CDN 费用等)

微信
支付宝

国光我也写了一个打赏页面用以感谢支持我的朋友,详情请看 打赏列表 | 国光


最后更新: 2022-09-19