本教程转载自远景论坛,发布于 2011-11-21 11:16:20,作者为:suhetao。
由 Bat.bat(williambj1) 于 2020-2-14 重新排版为 Markdown,并对部分内容进行了添加及修正。
http://bbs.pcbeta.com/forum.php?mod=viewthread&tid=944566&archive=2&extra=page%3D1&page=1
鄙人不是一个主板 BIOS 开发工作者,以下对 ASL 的理解仅仅来源于 http://www.acpi.info/ [注: 已失效, 文档已转移至 https://uefi.org] 上的 ACPI Specification 文档。因此难免会出现不少错误的理解,以及错误的观点,希望大家谅解以及纠正。
首先,不得不说一下 DSDT (Differentiated System Description Table Fields) 和 SSDT (Secondary System Description Table Fields) 什么是 DSDT 和 SSDT 呢?其实它们都属于 ACPI 其中的一个表格,而 ACPI 是 Advanced Configuration & Power Interface 的缩写,高级配置和电源接口,从文字上就可以理解 ACPI 是一系列的接口,这个接口包含了很多表格,所以 DSDT 和 SSDT 既是其中的表格同时也是一些接口。所以不难想象 ACPI 主要的功能就是提供操作系统一些服务以及提供一些讯息给操作系统使用。DSDT 和 SSDT 自然也不例外。ACPI 的一个特色就是专有一门语言来编写 ACPI 的那些表格。它就是 ASL (ACPI Source Language) 也就是这盘文章的主角,ASL 经过编译器编译后,就变成了 AML (ACPI Machine Language),然后由操作系统来执行。既然 ASL 是一门语言,那就有它的准则。
-
变量命名不超过 4 个字符,且不能以数字开头。联想一下看到过的 DSDT 代码看看,绝对不会超过。
-
Scope 形成作用域,概念类似于数学中的集合
{}
。有且仅有一个根作用域,所以 DSDT 都以DefinitionBlock ("xxxx", "DSDT", 0x02, "xxxx", "xxxx", xxxx) {
开始,同时以
}
结束。这个就是根作用域.
xxxx
参数依次表示输出文件名
、OEMID
、表ID
、OEM版本
。第三个参数根据第二个参数指定,如上面所示。如果是DSDT
就一定是0x02
,其他参数都可以自由修改。 -
以
_
字符开头的函数和变量都是系统保留的,这就是为什么反编译某些AML以后得到的 ASL 出现_T_X
,重新编译的时候会出现警告。 -
Method
定义函数,函数可以定义在Device
下或者Scope
下,但是不能脱离Scope
定义单独的函数,所以不会有 以下这种情况出现。Method (xxxx, 0, NotSerialized) { ... } DefinitionBlock ("xxxx", "DSDT", 0x02, "xxxx", "xxxx", xxxx) { ... }
-
根作用域
\
下有\_GPE
,\_PR
,\_SB
,\_SI
,\_TZ
五个作用域。\_GPE
--- ACPI 事件处理\_PR
--- 处理器\_SB
--- 所有的设备和总线\_SI
--- 系统指示灯\_TZ
--- 热区,用于读取某些温度
不同属性的东西放在对应的作用域下。例如:
-
设备
Device (PCI0)
放在Scope (\_SB)
里面Scope (\_SB) { Device (PCI0) { ... } ... }
-
CPU 相关的信息放在
Scope (_PR)
不同的 CPU 所在的域会不同,常见的有
_PR
,_SB
,SCK0
等Scope (_PR) { Processor (CPU0, 0x00, 0x00000410, 0x06) { ... } ... }
-
Scope (_GPE)
放着相关的事件处理Scope (_GPE) { Method (_L0D, 0, NotSerialized) { ... } ... }
乍一看不是函数吗?当然函数也可以放在这里。但是请注意函数名
_
开头的是系统保留的函数。
-
Device (xxxx)
也可看做是一个作用域,里面包含对设备的各种描述,例如_ADR
、_CID
、_UID
、_DSM
、_STA
等对设备的说明和状态控制 -
符号
\
引用根作用域,^
引用上级作用域,同理^^
为^
的上级作用域 -
符号
_
本身没有任何意义,在 ASL 中只用于补齐位置,如_OSI
的_
用于补齐Method
名称所需的四个字符 -
ACPI 为了更好理解,规范后来使用了新的 ASL 语法 ASL 2.0 (也叫做 ASL+), 新语法引入了与 C 语言等同的
+-*/=
,<<
,>>
和逻辑判断==
,!=
等等 -
函数最多可以传递 7 个参数,在函数里用
Arg0
~Arg6
表示,不可以自定义。 -
函数最多可以用 8 个局部变量,用
Local0
~Local7
,不用定义,但是需要初始化才能使用,也就是一定要有一次赋值操作。
ASL | 中文 |
---|---|
Integer |
整数 |
String |
字符串 |
Event |
事件 |
Buffer |
数组 |
Package |
对象集合 |
-
定义整数
Name (TEST, 0)
-
定义字符串
Name (MSTR,"ASL")
-
定义 Package
Name (_PRW, Package (0x02) { 0x0D, 0x03 })
-
定义 Buffer Field
Buffer Field 一共有 6 种,分别是
创建语句 | 大小 |
---|---|
CreateBitField | 1-Bit |
CreateByteField | 8-Bit |
CreateWordField | 16-Bit |
CreateDWordField | 32-Bit |
CreateQWordField | 64-Bit |
CreateField | 任意大小 |
CreateBitField (AAAA, Zero, CCCC)
CreateByteField (DDDD, 0x01, EEEE)
CreateWordField (FFFF, 0x05, GGGG)
CreateDWordField (HHHH, 0x06, IIII)
CreateQWordField (JJJJ, 0x14, KKKK)
CreateField (LLLL, Local0, 0x38, MMMM)
可以发现定义变量的时候不需要显式声明其类型
赋值方法有且仅有一个
Store (a,b) /* 传统 ASL */
b = a /* ASL+ */
例子:
Store (0, Local0)
Local0 = 0
Store (Local0, Local1)
Local1 = Local0
ASL+ | 传统 ASL | 中文含义 | 举例 |
---|---|---|---|
+ | Add | 整数相加 | Local0 = 1 + 2 Add (1, 2, Local0) |
- | Subtract | 整数减法 | Local0 = 2 - 1 Subtract (2, 1, Local0) |
* | Multiply | 整数相乘 | Local0 = 1 * 2 Multiply (1, 2, Local0) |
/ | Divide | 整数除法 | Local0 = 10 / 9 Divide (10, 9, Local1(余数), Local0(结果)) |
% | Mod | 整数求余 | Local0 = 10 % 9 Mod (10, 9, Local0) |
<< | ShiftLeft | 左移 | Local0 = 1 << 20 ShiftLeft (1, 20, Local0) |
>> | ShiftRight | 右移 | Local0 = 0x10000 >> 4 ShiftRight (0x10000, 4, Local0) |
-- | Decrement | 整数自减 1 | Local0-- Decrement (Local0) |
++ | Increment | 整数自增 1 | Local0++ Increment (Local0) |
& | And | 整数于 | Local0 = 0x11 & 0x22 And (0x11, 0x22, Local0) |
| | Or | 或 | Local0 = 0x01 |0x02 Or (0x01, 0x02, Local0) |
~ | Not | 取反 | Local0 = ~(0x00) Not (0x00,Local0) |
无 | Nor | 异或 | Nor (0x11, 0x22, Local0) |
等等,具体请查阅 ACPI Specification
ASL+ | 传统 ASL | 中文含义 | 举例 |
---|---|---|---|
&& | LAnd | 逻辑与 | If (BOL1 && BOL2) If (LAnd(BOL1, BOL2)) |
! | LNot | 逻辑反 | Local0 = !0 Store (LNot(0), Local0) |
| | LOr | 逻辑或 | Local0 = (0 |1) Store (LOR(0, 1), Local0) |
< | LLess | 逻辑小于 | Local0 = (1 < 2) Store (LLess(1, 2), Local0) |
<= | LLessEqual | 逻辑小于等于 | Local0 = (1 <= 2) Store (LLessEqual(1, 2), Local0) |
> | LGreater | 逻辑大于 | Local0 = (1 > 2) Store (LGreater(1, 2), Local0) |
>= | LGreaterEqual | 逻辑大于等于 | Local0 = (1 >= 2) Store (LGreaterEqual(1, 2), Local0) |
== | LEqual | 逻辑相等 | Local0 = (Local0 == Local1) If (LEqual(Local0, Local1)) |
!= | LNotEqual | 逻辑不等于 | Local0 = (0 != 1) Store (LNotEqual(0, 1), Local0) |
不难发现逻辑运算只会有两种结果 0
或者 1
-
定义函数
Method (TEST) { ... }
-
定义有两个输入参数的函数,以及使用局部变量
Local0
~Local7
参数个数如果不定义默认是
0
Method (MADD, 2) { Local0 = Arg0 Local1 = Arg1 Local0 += Local1 }
实现了两个参数的加法运算,因此传入的参数一定要隐式整形数。
-
定义带返回值的函数
Method (MADD, 2) { Local0 = Arg0 Local1 = Arg1 Local0 += Local1 Return (Local0) /* 在此处返回 */ }
例子为自定义加法的实现。函数实现的是如同系统函数 Add 一样的相加
Local0 = 1 + 2 /* ASL+ */ Store (MADD (1, 2), Local0) /* 传统 ASL */
-
定义可序列化的函数
如果不定义
Serialized
或者NotSerialized
默认为NotSerialized
Method (MADD, 2, Serialized) { Local0 = Arg0 Local1 = Arg1 Local0 += Local1 Return (Local0) }
这个有点类似于多线程同步的概念,也就是说,当函数声明为
Serialized
,内存中仅能存在一个实例。一般应用在函数中创建一个对象。应用举例说明:Method (TEST, Serialized) { Name (MSTR,"I will sucess") }
如果这样子声明
TEST
这个函数,那么在两个地方同时调用这个函数Device (Dev1) { TEST () } Device (Dev2) { TEST () }
如果先执行
Dev1
的TEST
,Dev2
中的TEST
将等待Dev1
中的TEST
函数执行完毕再执行。如果声明为Method (TEST, NotSerialized) { Name (MSTR, "I will sucess") }
那么将在其中一个
Devx
调用TEST
的时候,另外一个调用试图创建相同的字符串MSTR
就会失败。
通过调用 _OSI
函数,我们可以不费吹灰之力知道当前系统运行的是什么系统,轻而易举地区分 Windows 和 macOS,从而为某个系统创建单独的补丁。
_OSI
函数需要输入一个参数,这个参数必须是一个操作系统定义的用于识别的字符串
操作系统 | 字符串 |
---|---|
macOS | "Darwin" |
Linux (包括基于 Linux 内核的操作系统) |
"Linux" |
FreeBSD | "FreeBSD" |
Windows | "Windows 20XX" |
与其它操作系统不同的是,每个 Windows 版本都有不同的字符串,参阅:
https://docs.microsoft.com/en-us/windows-hardware/drivers/acpi/winacpi-osi
当 _OSI
内的参数对应了当前操作系统时,_OSI
就会返回 1
,If
条件语句判断成立
If (_OSI ("Darwin")) /* 判断当前的系统是不是 macOS */
注意_STA
分为两种,请勿与 PowerResource
的 _STA
混淆!!!
_STA
方法的返回值最多包含 5 个 Bit,每个 Bit 的含义如下
Bit 位 | 含义 |
---|---|
Bit [0] | 设备是否存在 |
Bit [1] | 设备是否被启用且可以解码其资源 |
Bit [2] | 设备是否在 UI 中显示 |
Bit [3] | 设备是否正常工作 |
Bit [4] | 设备是否存在电池 |
看完上面的表可能就有人要问了,“我们平时见到的 _STA
都是 0x0F
,Zero
”,和这些 Bit 有啥关系?其实只要把 16进制 值转换成 2 进制自然就一目了然了。0x0F
的意思是前四个 Bit 都为 1
,等于是完全启用,反之 Zero
就是完全禁用。
那么有时候遇到的 0x0B
,0x1F
又是什么呢?SSDT-PNLF
里面的 0x0B
转成 2 进制 是 1011
,也就是说设备处于一个半开的状态,不允许解码其中的资源。0x1F
只会在笔记本的电池设备中出现,多出的那一个 Bit 用于通知 电池控制设备 (Control Method Battery Device PNP0C0A
) 设备存在电池。
这里再啰嗦一句
PowerResource
的_STA
PowerResource
的_STA
只有两个返回值One
和Zero
,有兴趣可以查阅 ACPI 规范,作用不再赘述
_CRS
函数返回的是一个 Buffer
,在触摸设备中会返回触摸设备所需的 GPIO Pin
,APIC Pin
等等,可以控制设备的中断模式。
和常见的高级编程语言一样,ASL 也有与之对应的控制流程语句。
- Switch
- Case
- Default
- BreakPoint
- While
- Break
- Continue
- If
- Else
- ElseIf
- Stall
例如下面的语句判断一下当前系统的接口是不是 Darwin
,如果是把 OSYS = 0x2710
If (_OSI ("Darwin"))
{
OSYS = 0x2710
}
下面的例子中如果系统结构不是 Darwin
,另外如果系统不是 Linux
,那么 OSYS = 0x07D0
If (_OSI ("Darwin"))
{
OSYS = 0x2710
}
ElseIf (_OSI ("Linux"))
{
OSYS = 0x03E8
}
Else
{
OSYS = 0x07D0
}
Switch (Arg2)
{
Case (1) /* 条件 1 */
{
If (Arg1 == 1)
{
Return (1)
}
BreakPoint /* 条件判断不符合,退出 */
}
Case (2) /* 条件 2 */
{
...
Return (2)
}
Default /* 如果都不符合,则 */
{
BreakPoint
}
}
Switch
可以看做是一系列 If...Else
的集合。BreakPoint
相当于断点,意味着退出当前 Swtich
Local0 = 10
While (Local0 >= 0x00)
{
Local0--
Stall (32)
}
Local0
等于 10
,如果 Local0
逻辑不等于 0
不为真,Local0
自减 1
,暂停 32μs
, 所以这段代码延时 10 * 32 = 320 μs
。
ASL 中的 For 循环和 C
, Java
中的如出一辙
for (local0 = 0, local0 < 8, local0++)
{
...
}
上面的 For
循环和下面的 While
是等效的
Local0 = 0
While (Local0 < 8)
{
Local0++
}
引用类型 | 中文(仅供参考) | 外部 SSDT 引用 | 被引用 |
---|---|---|---|
UnknownObj | 未知 | External (\_SB.EROR, UnknownObj |
(只有 iasl 无法判断类型时才会出现,请避免使用) |
IntObj | 整数 | External (TEST, IntObj |
Name (TEST, 0) |
StrObj | 字符串 | External (\_PR.MSTR, StrObj |
Name (MSTR,"ASL") |
BuffObj | 缓冲 | External (\_SB.PCI0.I2C0.TPD0.SBFB, BuffObj |
Name (SBFB, ResourceTemplate () Name (BUF0, Buffer() {"abcde"}) |
PkgObj | 包 | External (_SB.PCI0.RP01._PRW, PkgObj |
Name (_PRW, Package (0x02) { 0x0D, 0x03 }) |
FieldUnitObj | 字段单元 | External (OSYS, FieldUnitObj |
OSYS, 16, |
DeviceObj | 设备 | External (\_SB.PCI0.I2C1.ETPD, DeviceObj |
Device (ETPD) |
EventObj | 事件 | External (XXXX, EventObj |
Event (XXXX) |
MethodObj | 函数 | External (\_SB.PCI0.GPI0._STA, MethodObj |
Method (_STA, 0, NotSerialized) |
MutexObj | 互斥体 | External (_SB.PCI0.LPCB.EC0.BATM, MutexObj |
Mutex (BATM, 0x07) |
OpRegionObj | 操作区 | External (GNVS, OpRegionObj |
OperationRegion (GNVS, SystemMemory, 0x7A4E7000, 0x0866) |
PowerResObj | 电源资源 | External (\_SB.PCI0.XDCI, PowerResObj |
PowerResource (USBC, 0, 0) |
ProcessorObj | 处理器 | External (\_SB.PR00, ProcessorObj |
Processor (PR00, 0x01, 0x00001810, 0x06) |
ThermalZoneObj | 温控区 | External (\_TZ.THRM, ThermalZoneObj |
ThermalZone (THRM) |
BuffFieldObj | 缓冲区 | External (\_SB.PCI0._CRS.BBBB, BuffFieldObj |
CreateField (AAAA, Zero, BBBB) |
DDBHandleObj 几乎不可能遇到因此这里不讨论
在 SSDT 中外部引用时,使用 External (
+ 路径和名称
+ ,
+ 引用类型)
,把光标定位到被引用 Object 里之后 MaciASL 会在左下角显示路径
CondRefOf
是 ASL 中一个非常实用的函数,可以用来判断所有类型 Object 的存在与否
Method (SSCN, 0, NotSerialized)
{
If (_OSI ("Darwin"))
{
...
}
ElseIf (CondRefOf (\_SB.PCI0.I2C0.XSCN))
{
If (USTP)
{
Return (\_SB.PCI0.I2C0.XSCN ())
}
}
Return (Zero)
}
上面的代码节选自 SSDT-I2CxConf
,在启动非 macOS 的系统时,如果 I2C0
下面存在 XSCN
(原为 SSCN
,已被被重命名成 XSCN
,创建一个新的函数并使用原来的名称 SSCN
),则返回原来函数返回的值
累了,今天就写到这里,有了这些基础,鄙人觉得现在大家对着 DSDT,SSDT 的时候就不会是发呆又发呆,至少可以明白些少语句了。