# 电池热补丁指南 (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
这两个方法分别获取电池信息和电池状态的。
现在常用的电池驱动为
ACPIBatteryManager
和SMCBatteryManager
,其中前者为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 名称不一样,常见的为
EC
、EC0
、H_EC
,戴尔机器通常使用ECDV
,而华为通常使用HWEC
,尽管如此,它们的_HID
都是PNP0C09
,这也就是为什么我们选择搜索这个关键词的原因
上述示例代码中可以看出 EC 的 ACPI 路径为 _SB.PCI0.LPCB.H_EC
,我们需要寻找位于该范围下 OperationRegion
和 Field
# 语法解析
OperationRegion (RegionName, RegionSpace, Offset, Length)
- RegionName: 操作区名称,EC 下的通常为
ERAM
、ECF2
、ECF3
、ECOR
等,并且有的机器可能不止一个。 - 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)))
}
参数解释:Arg0
和 Arg1
为你拆分后的两个8位寄存器,请按照 Field
中的顺序填写参数,如果 Arg1
在 Field
中处在 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)
}
参数解释:Arg0
和 Arg1
为你拆分的两个 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) // 返回最终结果
}
参数解释:
- 对于
RECB
,Arg0
是原寄存器的偏移量(即Offset
),Arg1
是原寄存器的长度 - 对于
RE1B
,Arg0
是偏移量
函数原理:RECB
通过 Arg1
确定需要拆分的 8 位寄存器个数,通过 While
循环及偏移量自增方法调用 RE1B
从 Field
中循环读取出每个 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++ // 索引自增
}
}
参数解释:
- 对于
WECB
,Arg0
是原寄存器的偏移量(即Offset
),Arg1
是原寄存器的长度,Arg2
为被写入的数据或对象 - 对于
WE1B
,Arg0
是偏移量,Arg1
为从 Buffer 取出的一个字节数据(即 8 位长度数据)
函数原理:WECB
通过 Arg1
确定需要拆分的 8 位寄存器个数,创建 Buffer 对象将 Arg2
转化为若干个字节的数据(一个字节等于 8 位),通过 While
循环以及偏移量自增方法调用 WE1B
,向 Field
中循环写入每个 8 位数据,具体见上述代码注释
注意
有些笔记本的 EC
使用 SystemMemory
作用域,则 EC
、RE1B
和 WE1B
的 Field
起始偏移量也需要加上原始定义数值,参照如下所示代码进行修改
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 下寻找所有的 OperationRegion
和 Field
,并从中筛选出超过 8 位的寄存器
- 根据每个
OperationRegion
的操作区名称找到所有对应的Field
,筛选超过 8 位的寄存器,并检查是否存在被调用的情况。 - 若存在被调用的情况,对应源代码的情况,按顺序排列需要拆分的寄存器(当存在多个
Field
里面的寄存器需要拆分时请分组排列,便于后续处理),并注明它们的长度。 - 计算它们的偏移量,如果是几个连续的寄存器,只需要计算第一个的偏移量,因为相邻的寄存器都是按顺序进行操作的,不需要额外计算。
TIP
如果某个寄存器被全局定义过,例如在根路径(\
)或者(\_SB
)路径下的 Field
里面有相同名字的寄存器,那么当你搜索该寄存器被调用情况时应注意区分此处的调用是不是 EC 下的这一个,如果不是,说明此处调用使用的是全局定义,对 EC 没有影响,不需要记录下来。
对照我们的示例文件,整理记录如下
16位: B1DT
,B1CY
,RTEP
,BET2
,B1TM
,BAPV
,BARC
,BADC
,BADV
,BAFC
,B1CR
32位: B1CH
大于32位: SMD0, 256
,B1MA, 64
,B2MA, 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 设备路径),创建 OperationRegion
和 Field
,将已经拆分命名并计算好偏移量的寄存器填入代码中,示例如下:
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
拆分为 BAC0
和 BAC1
,应该在原始 ACPI
中搜索是否存在同样名字的寄存器,最佳方法是直接在 orgin
目录下打开终端使用 grep
命令搜索,方便快捷。
# 对调用的寄存器进行拆分处理
注意
根据我们之前记录的路径将需要修改的 Method
代码完整复制过来,注意大括号千万别复制错了,不然后面排查错误会很麻烦。
修改 16 位以上寄存器拆分读取:
语法:
B1B2 (Arg0, Arg1)
Arg0
、Arg1
为你拆分后的两个 8 位寄存器名字,注意顺序。
示例:
原始语句:
If ((^^PCI0.LPCB.EC0.BADC < 0x0C80))
一个比较判断语句,属于读取操作,我将 BADC
拆分为 ADC0
和 ADC1
,并进行如下修改:
If ((B1B2 (^^PCI0.LPCB.EC0.ADC0, ^^PCI0.LPCB.EC0.ADC1) < 0x0C80))
修改 16 位寄存器拆分写入:
语法:
W16B (Arg0, Arg1,Arg2)
Arg0
、Arg1
为你拆分后的两个 8 位寄存器名字,注意顺序。Arg2
为被写入的数值或数据对象。
示例:
原始语句:
SMW0 = Arg3
普通的赋值语句,属于写入数据操作,我将 SMW0
拆分为 MW00
和 MW01
,并进行如下修改:
W16B (MW00, MW01, Arg3)
修改 32 位寄存器拆分读取:
语法:
B1B4 (Arg0, Arg1, Arg2, Arg3)
Arg0
、Arg1
、Arg2
和Arg3
为你拆分后的 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 补丁中重新定义它,以方便我们直接修改里面的代码。
用 MaciASL 查看补丁中修改的
Method
,确认它们的参数个数以及是否可序列化(NotSerialized
和Serialized
)。用 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
重名以外,也要避免更名后出现重名的情况。
例如
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% 的情况),此时应打上Mutex
置0
补丁来解决目前所知道的绝大多数笔记本 ACPI 的
Mutex
都是默认置0
的,但是对于一些联想品牌的笔记本,它们往往有几个Mutex
的SyncLevel
并不是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
则代表SyncLevel
的0x07
,我们的目的是使
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
设备用于合并计算两块电池的信息和状态,代替原来两块电池设备的代码为驱动提供信息
参考链接:
# 总结
# 一些经验
- 对于没有用到的工具方法,可以从补丁中移除,减少补丁代码,例如
W16B
和WECB(WE1B)
这类写入操作有些机器的 ACPI 不需要。 - 大多数调用的情况都是属于读取操作,而写入操作很少。
- 根据经验,同一个超过 32 位的字段单元通常只会被调用一次,尽管有些机器存在2次调用的情况,但分析代码可知,通常这两种调用不会同时进行(常见情况为当既有读取又有写入时,两种操作被控制语句限制只执行其中一种)。
- 根据经验观察,并非所有涉及到字段单元拆分的
Method
都需要修改代码,事实上,有些Method
和电池没有太大的关系,即使不对其调用的超过 8 位字段单元进行拆分也没有影响,但是为了保险起见我们还是选择了全部进行拆分修改。如果你能深入了解电池 ACPI 的读写原理,明确真正和电池相关的Method
,你能很轻松地从你的补丁中移除一些无关紧要的代码修改和二进制更名,最简化你的补丁,关于这方面的内容,需要一定的经验且较为深入的理解 ACPI 才能做到。 - 极少数笔记本可能根本就搜不到
PNP0C09
(EC
的_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