# 电池热补丁指南 (Guide for Battery Hotpatch)

# 前言

Last Updated : 4th April,2020 Saturday

为什么笔记本电池需要制作补丁?: macOS 使用的 ACPI 规范和解析器与常规 PC 不同,当电池驱动向 ACPI 底层发送指令时,如果读写了大于 8 位的寄存器,程序就会发生错误,导致无法获取到数据

实现原理: 1. 将电池 ACPI 方法相关的超过 8 位的寄存器(位于 Field 里面)拆分成若干个 8 位;2. 利用 ACPI 二进制更名使调用这些寄存器的 Method(函数方法)失效,并在新建的电池补丁中重新定义并修改代码实现电池信息的正确读取

技术要求: 具备一定的 ACPI 基础(请参考 OC-little 中的总述部分),会十六进制进制转换和计算(用于计算偏移量和理解拆分函数)。

必备工具: MaciASL、HEX Fiend、Hackintool(可选)。

文件准备

  • 提取原始 ACPI 文件:利用 CLOVER 引导界面的 F4 快捷键提取,文件将保存在 ACPI/origin 里面,你会看到除了 DSDT.aml 以外还有许多其它的 ACPI 文件,通常我们需要用的就是 DSDT.aml,少数机器需要用到其它的一些 SSDT-xxx.aml 文件。
  • 下载参考示例文件及模板文件,链接:https://pan.baidu.com/s/1hMfz7ayJlRx_JwqqPiebHg 提取码: 4wg7

提示

大部分笔记本的 EC 代码在 DSDT.aml 中,但也存在例外,如联想拯救者 14ISK/15ISK 的 EC 代码就在 SSDT-1-CB-01.aml 中,本教程所使用的案例就是属于这种情况。

# 电池 ACPI 方法和驱动原理

打开你的 DSDT.aml(本教程示例使用的为 SSDT-1-CB-01.aml),搜索 PNP0C0A,即可找到电池设备的定义和相关代码 ,且 BIOS 设备名称通常为 BAT0 或者 BAT1,如下所示

Device (BAT0)
{
    Name (_HID, EisaId ("PNP0C0A") /* Control Method Battery */)  // _HID: Hardware ID

在电池 ACPI 代码中,通常具备以下方法,具体解释如下

  • _BIF (Battery Information) 用于获取电池基本信息,包括设计容量、电池代号、序列号、电池类型、OEM信息等
  • _BIX (Battery Information Extended) 是 _BIF 的拓展方法,电池驱动通常优先使用 _BIX 方法获取电池信息,但是并不是所有笔记本的 ACPI 都具备该方法
  • _BST (Battery Status) 用于获取电池实时状态,返回电池充电状态、剩余容量百分比和数值以及当前电池电压
  • _BIP (Battery Trip Point) 用于设置电池低电量触发值,大部分电池不支持此功能,这种情况下系统会轮询 _BST 中的剩余电量来判断是否处于警戒水平

电池驱动原理:明确电池 ACPI 方法的功能后,我们能够知道电池驱动主要是通过操作 _BIF_BST 这两个方法分别获取电池信息和电池状态的。

现在常用的电池驱动为 ACPIBatteryManagerSMCBatteryManager ,其中前者为 Rehabman 大神开发的驱动,尽管已经不再更新,但是对于部分机器而言更适合使用。

# ACPI 寄存器映射原理

  • 名词解释:给已经分配好地址的有特定功能的内存单元取一个别名,这样的过程叫寄存器映射,这个别名就是我们常说的寄存器。
  • ACPI 实现方法:通过 OperationRegion 指定作用域(即被映射区域),并在 Field 中指定寄存器的偏移量、长度和名称,使其能够取出被映射区域中的指定数据

打开你的 DSDT.aml(本教程示例中位于 DSDT.aml ),搜索 PNP0C09,即可找到 EC 设备的路径,EC 设备的定义代码如下所示

Scope (_SB.PCI0.LPCB)
{
    Device (H_EC)
    {
        Name (_HID, EisaId ("PNP0C09") /* Embedded Controller Device */)  // _HID: Hardware ID

不同机器的 EC 名称不一样,常见的为 ECEC0H_EC,戴尔机器通常使用 ECDV,而华为通常使用 HWEC,尽管如此,它们的 _HID 都是 PNP0C09,这也就是为什么我们选择搜索这个关键词的原因

上述示例代码中可以看出 EC 的 ACPI 路径为 _SB.PCI0.LPCB.H_EC,我们需要寻找位于该范围下 OperationRegionField

# 语法解析

OperationRegion (RegionName, RegionSpace, Offset, Length)
  • RegionName: 操作区名称,EC 下的通常为 ERAMECF2ECF3ECOR 等,并且有的机器可能不止一个。
  • RegionSpace: 操作空间,又称作用域,通常 EC 使用的作用域都是 EmbeddedControl,但是对于某些厂商,他们会选择将 EC 数据映射到系统内存中,因此作用域为 SystemMemory
  • Offset: 作用域内每个 Field 的起始偏移量,EC 作用域的起始偏移量通常为 Zero,而系统内存作用域中用于映射 EC 数据的起始偏移量值由厂商决定
  • Length: 作用域内每个 Field 的最大长度。
Field (RegionName, AccessType, LockRule, UpdateRule) {FieldUnitList}
  • RegionName:对应 OperationRegion 的操作区名称。
  • AccessType:访问类型,EmbeddedControl 只能是 ByteAcc,代表按字节访问,因此偏移量是以字节来计算的,即每 8 位进 1 。
  • LockRule:锁定规则,与多线程相关,通常为 NoLock
  • UpdateRule:更新规则,用来指定如何处理未产生改动的映射区域,通常为 Preserve,即维持原值。
  • FieldUnitList:字段单元列表,即寄存器列表,参考下方的示例

# 代码示例与偏移量计算方法

  • Field 中每一行的元素由两部分组成,寄存器名称和寄存器长度(单位为 Bit)
  • Offset 用于指定一系列相邻寄存器的起始偏移量,计算机在访问时将通过第一个寄存器的 Offset 和后续寄存器的长度自动确认每个寄存器的偏移量,在后面的热补丁制作中这将是一大重点和难点
OperationRegion (ECF3, EmbeddedControl, Zero, 0xFF) // 作用域为 EmbeddedControl,起始偏移量为0,最大长度 0xFF,即 255个字节
Field (ECF3, ByteAcc, Lock, Preserve) // 按字节访问,即每 8 位进 1(1 Byte = 8 Bits)
{
    VCMD,   8, // 0x01
    VDAT,   8, // 0x02
    VSTA,   8, // 0x03
    Offset (0x04),
    AIND,   8, // 0x05
    ANUM,   8, // 0x06
    F1PW,   8, // 0x07
    ...
    Offset (0x60),
    B1CH,   32, // 0x64 = 0x60 + 0x04 (32/8 = 4 to HEX)
    B2CH,   32, // 0x68
    B1MO,   16, // 0x6A
    B2MO,   16, // 0x6C
    B1SN,   16, // 0x6E
    B2SN,   16, // 0x70
    B1DT,   16, // 0x72
    B2DT,   16, // 0x74
    B1CY,   16, // 0x76
    ...
    Offset (0xC2),
    BARC,   16, // 0xC4 = 0xC2 + 0x02 (16/8 = 2 to HEX)
    BADC,   16, // 0xC6
    BADV,   16, // 0xC8
    BDCW,   16, // 0xCA
    BDCL,   16, // 0xCC
    BAFC,   16, // 0xCE
    BAPR,   16, // 0xD0
    B1CR,   16, // 0xD2
    B1AR,   16, // 0xD4
    ...

}

# 拆分函数原理

  • 将所有大于 8 位的寄存器拆分为若干个 8 位寄存器,如 16 位拆分为 2 个 8 位,32 位拆分为 4 个 8 位,48 位拆分为 6 个 8 位,依次类推
  • 寄存器本身的作用没有发生改变,改变的只是每个寄存器存取数据的大小
  • 本节只是对拆分函数的原理进行解释,相关的应用示例请参见下一节-热补丁制作详解

注意

在处理小于等于 32 位的寄存器时,需要在热补丁中的 EC 范围下创建一个新的操作区为拆分后的寄存器重新映射,具体将在后面的热补丁制作中详细解释

# 16 位拆分读取 B1B2

Method (B1B2, 2, NotSerialized)
{
    Return ((Arg0 | (Arg1 << 0x08)))
}

参数解释Arg0Arg1 为你拆分后的两个8位寄存器,请按照 Field 中的顺序填写参数,如果 Arg1Field 中处在 Arg0 前面,最后拼接的数据将是错误的

函数原理:取 Arg0 作为低 8 位,将 Arg1 运用左移运算变成 16 位数据,此时它的的低 8 位全是 0 ,再运用或运算拼接成完整的 16 位数据

# 32 位拆分读取 B1B4

Method (B1B4, 4, NotSerialized)
{
    Local0 = (Arg2 | (Arg3 << 0x08))
    Local0 = (Arg1 | (Local0 << 0x08))
    Local0 = (Arg0 | (Local0 << 0x08))
    Return (Local0)
}

提示:原理基本和 B1B2 是一样的,唯一的区别就是此时变成将 4 个 8 位数据拼成 32 位数据了

# 16 位拆分写入 W16B

Method (W16B, 3, NotSerialized)
{
    Arg0 = Arg2
    Arg1 = (Arg2 >> 0x08)
}

参数解释Arg0Arg1 为你拆分的两个 8 位寄存器,Arg2 为需要写入的数据或对象

函数原理:将 Arg2 直接赋值于 Arg0,此时计算机会将 Arg2 的低 8 位给 Arg0;然后将 Arg2 进行右移计算,将高 8 位数据变为低 8 位数据,同样直接赋值

注意

对于大于 32 位的寄存器,我们不在新建的操作区里面进行拆分,而将它交给我们的自定义函数进行自动化处理,具体如下

# 32 位以上读取 RECB RE1B

Method (RE1B, 1, NotSerialized)
{
    OperationRegion (ERM2, EmbeddedControl, Arg0, One) // 作用域为 EmbeddedControl,Arg0 定义起始偏移量
    Field (ERM2, ByteAcc, NoLock, Preserve)
    {
        BYTE,   8 // 指定一个 8 位寄存器映射对应区域数据
    }

    Return (BYTE) // 返回结果
}

Method (RECB, 2, Serialized)
{
    Arg1 = ((Arg1 + 0x07) >> 0x03) // 计算 Arg1 除 8 并向上取整,位移运算更快
    Name (TEMP, Buffer (Arg1){}) // 初始化作为返回值的 Buffer
    Arg1 += Arg0 // 加上偏移量,即循环终止值
    Local0 = Zero // 定义 Buffer 索引为 0
    While ((Arg0 < Arg1)) // 进行循环,循环次数为初次计算的 Arg1,自行理解
    {
        TEMP [Local0] = RE1B (Arg0) // 调用 RE1B 依次返回 8 位数据
        Arg0++ // 偏移量自增
        Local0++ // 索引自增
    }

    Return (TEMP) // 返回最终结果
}

参数解释

  • 对于 RECBArg0 是原寄存器的偏移量(即 Offset),Arg1 是原寄存器的长度
  • 对于 RE1BArg0 是偏移量

函数原理RECB 通过 Arg1 确定需要拆分的 8 位寄存器个数,通过 While 循环及偏移量自增方法调用 RE1BField 中循环读取出每个 8 位数据,并拼接成原始寄存器定义长度的数据返回结果,具体见上述代码注释

# 32 位以上写入 WECB WE1B

Method (WE1B, 2, NotSerialized)
{
    OperationRegion (ERM2, EmbeddedControl, Arg0, One) // EmbeddedControl 为 EC 作用域,Arg0 定义起始偏移量
    Field (ERM2, ByteAcc, NoLock, Preserve)
    {
        BYTE,   8 // 指定一个 8 位寄存器映射对应区域数据
    }

    BYTE = Arg1 // 将 Arg1 通过寄存器间接写入对应区域
}

Method (WECB, 3, Serialized)
{
    Arg1 = ((Arg1 + 0x07) >> 0x03) // 计算 Arg1 除 8 并向上取整,位移运算更快
    Name (TEMP, Buffer (Arg1){}) // 初始化作为写入值的 Buffer
    TEMP = Arg2 // 将被写入的数据或对象赋值给 TEMP
    Arg1 += Arg0 // 加上偏移量,即循环终止值
    Local0 = Zero // 定义 Buffer 索引为 0
    While ((Arg0 < Arg1)) // 进行循环,循环次数为初次计算的 Arg1,自行理解
    {
        WE1B (Arg0, DerefOf (TEMP [Local0])) // 调用 WE1B 依次写入 8 位数据
        Arg0++ // 偏移量自增
        Local0++ // 索引自增
    }
}

参数解释

  • 对于 WECBArg0 是原寄存器的偏移量(即 Offset),Arg1 是原寄存器的长度,Arg2 为被写入的数据或对象
  • 对于 WE1BArg0 是偏移量,Arg1 为从 Buffer 取出的一个字节数据(即 8 位长度数据)

函数原理WECB 通过 Arg1 确定需要拆分的 8 位寄存器个数,创建 Buffer 对象将 Arg2 转化为若干个字节的数据(一个字节等于 8 位),通过 While 循环以及偏移量自增方法调用 WE1B,向 Field 中循环写入每个 8 位数据,具体见上述代码注释

注意

有些笔记本的 EC 使用 SystemMemory 作用域,则 ECRE1BWE1BField 起始偏移量也需要加上原始定义数值,参照如下所示代码进行修改

Scope (_SB.PCI0.LPCB.EC0)
{
    OperationRegion (ERAX, SystemMemory, 0xFE708300, 0x0100)
    Field (ERAX, ByteAcc, Lock, Preserve)
    {
    ···
    Method (RE1B, 1, NotSerialized)
    {
        Local0 = (0xFE708300 + Arg0)
        OperationRegion (ERM2, SystemMemory, Local0, One)

# 热补丁方法详解

如果你有认真学习和理解拆分函数的原理,相信接下来的步骤将会进行起来非常顺利

# 搜索寄存器,创建补丁

在 ACPI 寄存器映射原理中,我已经介绍了如何确认 EC 路径以及各拆分函数的原理和语法解释,接下来我们需要在 EC 下寻找所有的 OperationRegionField,并从中筛选出超过 8 位的寄存器

  • 根据每个 OperationRegion 的操作区名称找到所有对应的 Field,筛选超过 8 位的寄存器,并检查是否存在被调用的情况。
  • 若存在被调用的情况,对应源代码的情况,按顺序排列需要拆分的寄存器(当存在多个 Field 里面的寄存器需要拆分时请分组排列,便于后续处理),并注明它们的长度。
  • 计算它们的偏移量,如果是几个连续的寄存器,只需要计算第一个的偏移量,因为相邻的寄存器都是按顺序进行操作的,不需要额外计算。

TIP

如果某个寄存器被全局定义过,例如在根路径(\)或者(\_SB)路径下的 Field 里面有相同名字的寄存器,那么当你搜索该寄存器被调用情况时应注意区分此处的调用是不是 EC 下的这一个,如果不是,说明此处调用使用的是全局定义,对 EC 没有影响,不需要记录下来。

对照我们的示例文件,整理记录如下

16位B1DTB1CYRTEPBET2B1TMBAPVBARCBADCBADVBAFCB1CR

32位B1CH

大于32位SMD0, 256B1MA, 64B2MA, 64

需要修改的 Method

  • _SB.PCI0.LPCB.H_EC.BAT1._BIF
  • _SB.PCI0.LPCB.H_EC.BAT1._BST
  • _SB.PCI0.LPCB.H_EC.VPC0.MHPF
  • _SB.PCI0.LPCB.H_EC.VPC0.SMTF
  • _SB.PCI0.LPCB.H_EC.VPC0.GSBI
  • _SB.PCI0.LPCB.H_EC.VPC0.GBID

打开模板文件 SSDT-BATT.dsl,在 EC 路径的 Scope 下(请务必修改为自己的 EC 设备路径),创建 OperationRegionField,将已经拆分命名并计算好偏移量的寄存器填入代码中,示例如下:

OperationRegion (XCF3, EmbeddedControl, Zero, 0xFF)
Field (XCF3, ByteAcc, NoLock, Preserve)
{
    Offset (0x60),
    BC0H,   8,
    BC1H,   8,
    BC2H,   8,
    BC3H,   8,
    Offset (0x70),
    BDT0,   8,
    BDT1,   8,
    Offset (0x74),
    BCY0,   8,
    BCY1,   8,
    Offset (0xAA),
    RTP0,   8,
    RTP1,   8,
    B0ET,   8,
    B1ET,   8,
    Offset (0xB6),
    BTM0,   8,
    BTM1,   8,
    B0PV,   8,
    B1PV,   8,
    Offset (0xC2),
    BAC0,   8,
    BAC1,   8,
    BDC0,   8,
    BDC1,   8,
    BDV0,   8,
    BDV1,   8,
    Offset (0xCC),
    BFC0,   8,
    BFC1,   8,
    Offset (0xD0),
    BCR0,   8,
    BCR1,   8
}

TIP

上述代码中,为了避免与原始 ACPI 中的操作区映射定义发生冲突,我们创建的操作区名称为 XCF3,区别于原始 ACPI 中的 ECF3

当然,我们对寄存器的拆分也要注意避免重复,比如我把 BADC 拆分为 BAC0BAC1,应该在原始 ACPI 中搜索是否存在同样名字的寄存器,最佳方法是直接在 orgin 目录下打开终端使用 grep 命令搜索,方便快捷。

# 对调用的寄存器进行拆分处理

注意

根据我们之前记录的路径将需要修改的 Method 代码完整复制过来,注意大括号千万别复制错了,不然后面排查错误会很麻烦。

修改 16 位以上寄存器拆分读取

语法

B1B2 (Arg0, Arg1)
  • Arg0Arg1 为你拆分后的两个 8 位寄存器名字,注意顺序。

示例

原始语句:

If ((^^PCI0.LPCB.EC0.BADC < 0x0C80))

一个比较判断语句,属于读取操作,我将 BADC 拆分为 ADC0ADC1,并进行如下修改:

If ((B1B2 (^^PCI0.LPCB.EC0.ADC0, ^^PCI0.LPCB.EC0.ADC1) < 0x0C80))

修改 16 位寄存器拆分写入

语法

W16B (Arg0, Arg1,Arg2)
  • Arg0Arg1 为你拆分后的两个 8 位寄存器名字,注意顺序。
  • Arg2 为被写入的数值或数据对象。

示例

原始语句:

SMW0 = Arg3

普通的赋值语句,属于写入数据操作,我将 SMW0 拆分为 MW00MW01,并进行如下修改:

W16B (MW00, MW01, Arg3)

修改 32 位寄存器拆分读取

语法

B1B4 (Arg0, Arg1, Arg2, Arg3)
  • Arg0Arg1Arg2Arg3 为你拆分后的 4 个 8 位寄存器名字,注意顺序。

示例

原始语句:

If ((B1CH == 0x0050694C))

修改结果:

If ((B1B4 (BC0H, BC1H, BC2H, BC3H) == 0x0050694C))

修改 32 位以上寄存器读取

语法

RECB (Offset, Length)
  • Offset 为原寄存器的偏移量
  • Length 为原寄存器的长度

示例

原始定义:

Offset (0x8F),
B1MA,   64,

调用语句:

IFMN = B1MA

修改结果:

IFMN = RECB (0x8F, 0x40)

修改 32 位以上寄存器写入

语法

WECB (Offset, Length, Obj)
  • Offset 为原寄存器的偏移量
  • Length 为寄存器的长度
  • Obj 为被写入的值或者数据对象

示例

原始定义:

Offset (0x18),
SMPR,   8,
SMST,   8,
SMAD,   8,
SMCM,   8,
SMD0,   256,

调用语句:

SMD0 = FB4

修改结果:

WECB (0x1C, 0x0100, FB4) // 0x1C=0x18+0x04

# 添加 _OSI 判断

OpenCore 在引导时对于任何系统都是加载的同一套 ACPI,我们应确保我们的补丁只对 macOS 生效,此时我们需要通过添加 If(_OSI("Darwin")){} 判断的方法使其在其他系统下不生效,避免 OC 在引导其它系统时出现不必要的 ACPI 错误。

在已经完成的补丁文件中,在每一个 Method 的开始部分加上 _OSI 系统判断并在结尾处回调原始方法,示例如下:

Method (_BIF, 0, NotSerialized)  // _BIF: Battery Information
{
    If (_OSI ("Darwin"))
    {
    ...
        Return (...)
    ...
    }
    Else
    {
        Return (XBIF ())
    }
}

以上述代码为例 ,Else 后面的代码为回调原始方法,如果原始方法没有出现 Return 语句,则可直接以 XBIF() 的方式回调;如果原始方法的代码中出现了 Return 语句,则在回调时也需要以 Return 形式回调原方法。

如果原始方法带参数则应该在回调时将参数传递过去,如下代码:

Method (SMTF, 1, NotSerialized)
{
    If (_OSI ("Darwin"))
    {
        If ((Arg0 == Zero))
        {
            Return (B1B2 (B0ET, B1ET))
        }

        If ((Arg0 == One))
        {
            Return (Zero)
        }

        Return (Zero)
    }
    Else
    {
        Return (XMTF (Arg0))
    }
}

注意:ASL 语言中方法的参数是从 Arg0 开始的。

# 添加外部引用声明

TIP

在确认修改结束后,我们点击编译会报告许多对象找不到的错误,一般来说,这些错误是由于我们复制过来的代码引用了原始 ACPI 中的一些对象但在 SSDT 中缺少引用声明导致的。在这里我们只需要搜索它们在原始 ACPI 中的定义路径和类型,并在补丁文件头部(介于 DefinitionBlock 和后面的 Method 代码之间)添加上引用声明即可解决大多数编译错误。

注意

添加外部引用声明时应注意路径和类型应严格对应,参照下列示例

Device:原始 ACPI 定义示例

Scope (_SB.PCI0.LPCB)
{
    Device (H_EC)
    {

补丁中添加的代码示例

External (_SB_.PCI0.LPCB.H_EC, DeviceObj)

Method:原始 ACPI 定义示例

Method (_STA, 0, NotSerialized)  /* _STA: Status */

补丁中添加的代码示例

External (_SB_.BAT0._STA, MethodObj)

Mutex:原始 ACPI 定义示例

Mutex (BATM, 0x07)

补丁中添加的代码示例

External (_SB_.PCI0.LPCB.H_EC.BATM, MutexObj)

FieldUnit:原始 ACPI 定义示例

Field(...)
{
...
BCNT,   8,
...
}

补丁中添加的代码示例

External (_SB_.PCI0.LPCB.H_EC.BCNT, FieldUnitObj)

Integer:原始 ACPI 定义示例

Name (ECA2, Zero)

补丁中添加的代码示例

External (_SB_.PCI0.LPCB.H_EC.ECA2, IntObj)

Package:原始 ACPI 定义示例

Name (PBIF, Package (0x0D)
{
    One,
    0xFFFFFFFF,
    0xFFFFFFFF,
    One,
    0xFFFFFFFF,
    0xFA,
    0x96,
    0x0A,
    0x19,
    "BAT0",
    " ",
    " ",
    " "
})

补丁中添加的代码示例

External (_SB_.BAT0.PBIF, PkgObj)

# 更名 Method 使其失效

Hotpatch 的原理是通过 ACPI 二进制更名使原来 ACPI 中的 Method失效,并在新的 SSDT 补丁中重新定义它,以方便我们直接修改里面的代码。

  1. 用 MaciASL 查看补丁中修改的 Method,确认它们的参数个数以及是否可序列化(NotSerializedSerialized)。

  2. 用 HEX Fiend 打开 DSDT.aml(本教程的示例是 SSDT-1-CB-01.aml

    • 同时按住 command + F 调出搜索框,切换到 Text 模式,输入要更名的 Method 名字。

    • 切换到 HEX 模式,此时刚刚输入的名字已经变成了十六进制代码,接下来我们需要在后面加上方法规则代码(参数个数+是否可序列 化),对应关系如下:

      • Method(xxxx,a,N) --> xxxx 的十六进制代码 + a 的十六进制代码,最后两位范围为 00 - 07

      • Method(xxxx,b,S) --> xxxx 的十六进制代码 + (b+8) 的十六进制代码,最后两位范围为 08 - 0F

      • 示例:

        Method (MHPF,1,N) --> `4D485046 01`
        Method (BTST,2,S) --> `42545354 0A`
        
    • 在输入上述方法定义的完整十六进制代码后,按 Next 或 Previous 进行全文搜索,一般只会出现一个结果,该结果就是我们在 MaciASL 编辑器里面看到的原始方法定义。

    • 我们需要该方法更名为一个未被利用的名称,通常习惯以 X 替换第一个字母,只要不出现重复定义即可。

    • 切换回 Text 模式,将第一位改为 X,并再次切换为 HEX 模式,可以发现 X 对应的十六进制代码为 58,以后我们可以凭经验直接在 HEX 模式下修改。再次搜索,如果没有结果,证明该方法名没有被利用过,可以用于更名。

    • 当然如果要更名的 Method 里面存着多个除了第一位不一样其它三位一样情况时,也很容易应对,分别更名为 X 开头的和 Y 开头的就行,只要不出现重名的情况都是可以的,也就是说除了要避免与现有 Method 重名以外,也要避免更名后出现重名的情况。

  3. 例如 BTST,2,S 更名为 XTST,2,S 最终 config 的更名应填:

    Comment      change BTST,2,S to XTST
    Find         42545354 0A
    Replace      58545354 0A
    

# 检查Mutex是否已经置0

引用Rehabman大神的原话:

Some DSDTs use Mutex objects with non-zero a SyncLevel. Evidently, OS X has some difficulty with this part of the ACPI spec, either that or the DSDTs are, in fact, codec incorrectly and Windows is ignoring it.

The common result of a non-zero SyncLevel is failure of methods at the point of Acquire on the mutext in question. This can result in strange behavior, failed battery status, or other issues.

结合ACPI规范,我的理解如下(若用语有误请指出):

有些机器的 Mutex(互斥体,用于处理多线程同步)对象的 SyncLevel(同步等级)不为 0 ,而这种情况下 macOS 无法正常执行多线程同步,造成的结果是电池状态等可能无法获取(如果电池相关的 Method 处于不同的同步等级,在 macOS 下会造成数据获取的异常,出现 0% 的情况),此时应打上 Mutex0 补丁来解决

目前所知道的绝大多数笔记本 ACPI 的 Mutex 都是默认置 0 的,但是对于一些联想品牌的笔记本,它们往往有几个 MutexSyncLevel 并不是 0,我们在完成电池补丁后应检查 Mutex 是否属于这种情况。OpenCore 没有 CLOVER 那样方便的补丁选项能直接将所有 Mutex 对象的同步等级修改为 0,这里我们需要利用 ACPI 二进制更名的方法实现置 0

具体方法如下

  • 在当前使用的 DSDT 文件里搜索 Mutex,看出现的几个对象的 SyncLevel 是否为 0

  • Mutex (BATM, 0x07) 为例,先转换BATM为十六进制代码,得到 42 41 54 4D

  • 在前后加上完整定义的十六进制代码,最终得到 01 42 41 54 4D 07

  • 其中 01 代表 Mutex; 07 则代表 SyncLevel0x07

  • 我们的目的是使 Mutex 对象置 0,所以 config 的更名应填

    Comment       Set Mutex BATM, 0x07 to 0x0
    Find          01 42 41 54 4D 07
    Repalce       01 42 41 54 4D 00
    

其它 Mutex 对象按照同样的方法处理即可。

# ACPI 特殊处理方案集合

# 惠普笔记本 ACEL 设备禁止

问题描述:由于部分惠普笔记本配备机械硬盘防护传感器,该设备实际为一个加速度传感器,即便没有驱动也能保持运行,持续向 EC 中读写数据,导致电池状态刷新异常

解决方案:在 Windows 下已经确认该设备 ACPI 名称为 ACEL,通过 ACPI 更名其 _STA,并在热补丁中要求 macOS 下禁止该设备

# ECRD 和 ECWT 读写控制

问题描述:部分机器的 ACPI 对于 EC 作用域下的寄存器读写有严格控制,有时我们需要稍微修改一下其中的代码解除一些限制

解决方案:目前使用的方法未在多数机器上验证,暂时不给出相关方案,请等待后续更新

# 双电池系统解决方案

双电池系统通常分为两种情况:

  • 第一种:只安装了一块电池,并且也没打算再装一块,这种最容易解决,利用 ACPI 更名另一块电池设备的 ACPI _STA 方法;
  • 第二组:安装了两块电池,并希望 macOS 下都可使用。这种情况需要更改两块电池设备的 ACPI _HID 名称使其保持运行的情况下不被电池驱动识别,同时新建一个 BATC 设备用于合并计算两块电池的信息和状态,代替原来两块电池设备的代码为驱动提供信息

参考链接:

# 总结

# 一些经验

  • 对于没有用到的工具方法,可以从补丁中移除,减少补丁代码,例如 W16BWECB(WE1B) 这类写入操作有些机器的 ACPI 不需要。
  • 大多数调用的情况都是属于读取操作,而写入操作很少。
  • 根据经验,同一个超过 32 位的字段单元通常只会被调用一次,尽管有些机器存在2次调用的情况,但分析代码可知,通常这两种调用不会同时进行(常见情况为当既有读取又有写入时,两种操作被控制语句限制只执行其中一种)。
  • 根据经验观察,并非所有涉及到字段单元拆分的 Method 都需要修改代码,事实上,有些 Method 和电池没有太大的关系,即使不对其调用的超过 8 位字段单元进行拆分也没有影响,但是为了保险起见我们还是选择了全部进行拆分修改。如果你能深入了解电池 ACPI 的读写原理,明确真正和电池相关的 Method,你能很轻松地从你的补丁中移除一些无关紧要的代码修改和二进制更名,最简化你的补丁,关于这方面的内容,需要一定的经验且较为深入的理解 ACPI 才能做到。
  • 极少数笔记本可能根本就搜不到 PNP0C09EC_HID),这种情况下我们只能搜 PNP0C0A(电池的 _HID),并根据 _BIF_BST_BIX 等电池 ACPI 方法入手,分析它调用的寄存器和函数,最终找到所有电池相关寄存器所在的 Field 内的定义(即偏移量和长度),然后根据本教程的方法进行修改,通常这类机器的 ACPI 使用的 SystemMemory 作用域,请务必注意起始偏移量的修正,寄存器拆分函数结尾处已经提到。

# 如何排查错误

  • 当我们完成了所有的电池补丁后如果发现电池还是未能正确显示怎么办呢?
    • 这种情况通常是由于我们在制作补丁没有充分考虑重名情况导致的。
    • 打开 Hackintool,切换到日志选项卡,选择系统,点击下方的刷新按钮生成,搜索 ACPI Error,看是否出现了和 EC 相关的错误,如果有,那么极有可能是出现了重复定义造成的,这时候我们需要修改我们电池补丁中相关对象的名称避免重名。
  • 如果出现电池在拔下外置电源的情况下变红该怎么办?
    • 这种情况同样需要排查 ACPI Error,通常这是由于其它 SSDT 缺失或者冲突造成 AC 适配器代码部分发生了异常,利用终端的 grep 命令在整个 origin 目录进行搜索一般能准确定位到问题,具体方法这里不作介绍,相对来说还是比较简单的。

# 参考来源

  • Guide using clover to hotpatch acpi and battery status hotpatch @Rehabman
  • tonymacx86, PCBeta 远景黑苹果论坛, 黑果小兵的部落阁
  • ACPI Specification 6.1