Arm 系统上的 ACPI¶
ACPI 可用于遵循 BSA(Arm 基础系统架构)[0] 和 BBR(Arm 基础启动要求)[1] 规范设计的 Armv8 和 Armv9 系统。BSA 和 BBR 都是公开可获取的文档。Arm 服务器除了符合 BSA 规范外,还符合 SBSA(服务器基础系统架构)[2] 中定义的一组规则。
Arm 内核实现了 ACPI 5.1 或更高版本的精简硬件模型。该规范及引用的所有外部文档的链接由 UEFI 论坛管理。该规范可在 http://www.uefi.org/specifications 获取,规范引用的文档可在 http://www.uefi.org/acpi 找到。
如果某个 Arm 系统不满足 BSA 和 BBR 的要求,或者无法使用所需 ACPI 规范中定义的机制来描述,那么 ACPI 可能不太适合该硬件。
虽然上述文档规定了构建行业标准 Arm 系统的要求,但它们也适用于多个操作系统。本文档的目的仅在于描述 Arm 系统上 ACPI 与 Linux 之间的交互——即 Linux 对 ACPI 的期望以及 ACPI 对 Linux 的期望。
为什么要在 Arm 上使用 ACPI?¶
在检查 ACPI 与 Linux 之间接口的细节之前,了解为什么要使用 ACPI 是很有用的。毕竟,Linux 中已经存在几种用于描述不可枚举硬件的技术。在本节中,我们总结了 Grant Likely 的一篇博客文章 [3],其中概述了在 Arm 系统上使用 ACPI 的原因。老实说,实际上我们几乎直接借鉴了大部分总结文本。
在 Arm 上使用 ACPI 的简要理由是:
ACPI 的字节码 (AML) 允许平台对硬件行为进行编码,而 DT 明确不支持这一点。对于硬件厂商来说,能够对行为进行编码是在新硬件上支持操作系统发布的一个关键工具。
ACPI 的 OSPM 定义了一个电源管理模型,将平台允许执行的操作约束在一个特定的模型中,同时在硬件设计中保持了灵活性。
在企业级服务器环境中,ACPI 已经建立了目前在生产系统中使用的绑定(例如针对 RAS 的绑定)。而 DT 没有。这些绑定可能在某个时候会在 DT 中定义,但这样做意味着 Arm 和 x86 最终将在固件和内核中使用完全不同的代码路径。
选择单一接口来描述平台与操作系统之间的抽象是很重要的。硬件厂商如果想要支持多个操作系统,将不需要同时实现 DT 和 ACPI。而且,就单一接口达成一致,而不是碎片化为每个操作系统各自的接口,有助于提高整体的互操作性。
新的 ACPI 治理流程运行良好,Linux 现在与硬件厂商和其他操作系统厂商平起平坐。事实上,再也没有理由认为 ACPI 仅属于 Windows,或者 Linux 在这个领域里比微软低人一等。将 ACPI 治理移至 UEFI 论坛极大地开放了规范开发过程,目前,ACPI 所做的大部分更改都是由 Linux 驱动的。
使用 ACPI 的关键在于支持模型。对于服务器而言,硬件行为的责任不能完全由内核承担,而必须在平台和内核之间进行划分,以便随时间进行有序的更改。ACPI 使操作系统无需了解硬件的所有微小细节,从而使操作系统无需单独移植到每个设备。它允许硬件厂商负责电源管理行为,而不依赖于他们无法控制的操作系统发布周期。
ACPI 也很重要,因为硬件和操作系统厂商已经制定了支持通用计算生态系统的机制。基础设施已经就绪,绑定已经就绪,流程也已经就绪。在处理垂直集成设备时,DT 完全能做到 Linux 需要的事情,但没有好的流程来支持服务器厂商的需求。Linux 可能会通过 DT 达到这一目标,但这样做实际上只是重复了已经能正常工作的东西。ACPI 已经满足了硬件厂商的需求,微软不会在 DT 上进行合作,而且硬件厂商最终还是会提供两个完全独立的固件接口——一个用于 Linux,另一个用于 Windows。
内核兼容性¶
ACPI 的主要动机之一是标准化,并利用标准化为 Linux 内核提供向后兼容性。在服务器市场中,软件和硬件通常会被长时间使用。ACPI 允许内核和固件就一致的抽象达成一致,该抽象可以在硬件或软件发生变化时长期维护。只要支持该抽象,系统就可以在不一定需要替换内核的情况下进行更新。
当 Linux 驱动程序或子系统首次使用 ACPI 实现时,根据定义,它最终需要特定版本的 ACPI 规范——即其基线。ACPI 固件必须能够与首次提供对该基线版本 ACPI 支持的最早内核版本一起继续工作(尽管可能不是最优的)。可能需要额外的驱动程序,但添加新功能(例如 CPU 电源管理)不应破坏较旧的内核版本。此外,ACPI 固件还必须与最新版本的内核协同工作。
与设备树的关系¶
在编译时,Arm 的驱动程序和子系统中的 ACPI 支持绝不能与 DT 支持相互排斥。
在启动时,内核将根据从引导加载程序传递的参数(包括内核 bootargs)仅使用一种描述方法。
无论使用 DT 还是 ACPI,内核必须始终能够使用这两种方案之一启动(在编译时启用了这两种方案的内核中)。
使用 ACPI 表启动¶
在 Arm 上向内核传递 ACPI 表的唯一已定义方法是通过 UEFI 系统配置表。为了明确起见,这意味着 ACPI 仅在通过 UEFI 启动的平台上受支持。
当 Arm 系统启动时,它可以拥有 DT 信息、ACPI 表,或者在某些非常罕见的情况下两者兼有。如果不使用命令行参数,内核将尝试使用 DT 进行设备枚举;如果不存在 DT,内核将尝试使用 ACPI 表(仅当它们存在时)。如果两者都不可用,内核将无法启动。如果在命令行上使用了 acpi=force,内核将首先尝试使用 ACPI 表,但如果不存在 ACPI 表,则回退到 DT。基本思想是,除非绝对别无选择,否则内核不会启动失败。
可以通过在内核命令行上传递 acpi=off 来禁用 ACPI 表的处理;这是默认行为。
为了让内核加载和使用 ACPI 表,UEFI 实现必须设置 ACPI_20_TABLE_GUID 以指向 RSDP 表(带有 ACPI 签名“RSD PTR ”的表)。如果此指针不正确且使用了 acpi=force,内核将禁用 ACPI,并转而尝试使用 DT 启动;实际上,此时内核已确定 ACPI 表不存在。
如果指向 RSDP 表的指针正确,ACPI 核心将使用 UEFI 提供的地址将该表映射到内核中。
然后,ACPI 核心将通过使用 RSDP 表中的地址查找 XSDT(扩展系统描述表)来定位并映射所有其他提供的 ACPI 表。XSDT 进而提供系统固件提供的所有其他 ACPI 表的地址;然后,ACPI 核心将遍历该表并映射所列出的表。
ACPI 核心将忽略提供的任何 RSDT(根系统描述表)。RSDT 在 arm64 上已被废弃并会被忽略,因为它们只允许 32 位地址。
此外,ACPI 核心将仅使用 FADT(固定 ACPI 描述表)中的 64 位地址字段。FADT 中的任何 32 位地址字段在 arm64 上都将被忽略。
ACPI 核心将在 arm64 上强制执行硬件精简模式(参见 ACPI 6.1 规范的第 4.1 节)。这样做允许 ACPI 核心运行较少复杂的代码,因为它不再需要为来自其他架构的传统硬件提供支持。任何不用于硬件精简模式的字段必须设置为零。
为了让 ACPI 核心正常运行,进而提供内核配置设备所需的信息,它期望找到以下表(所有节号均指 ACPI 6.5 规范)
RSDP(根系统描述指针),第 5.2.5 节
XSDT(扩展系统描述表),第 5.2.8 节
FADT(固定 ACPI 描述表),第 5.2.9 节
DSDT(微分系统描述表),第 5.2.11.1 节
MADT(多 APIC 描述表),第 5.2.12 节
GTDT(通用定时器描述表),第 5.2.24 节
PPTT(处理器属性拓扑表),第 5.2.30 节
DBG2(调试端口表 2),第 5.2.6 节,特别是表 5-6。
APMT(Arm 性能监视单元表),第 5.2.6 节,特别是表 5-6。
AGDI(Arm 通用诊断转储和重置设备接口表),第 5.2.6 节,特别是表 5-6。
如果支持 PCI,则为 MCFG(内存映射配置表),第 5.2.6 节,特别是表 5-6。
如果支持在没有 console=<device> 内核参数的情况下启动,则为 SPCR(串行端口控制台重定向表),第 5.2.6 节,特别是表 5-6。
如果需要描述 I/O 拓扑、SMMU 和 GIC ITS,则为 IORT(输入输出重映射表,第 5.2.6 节,特别是表 5-6)。
如果支持 NUMA,则需要以下表
SRAT(系统资源亲和性表),第 5.2.16 节
SLIT(系统局部性距离信息表),第 5.2.17 节
如果支持 NUMA 且系统包含异构内存,则为 HMAT(异构内存属性表),第 5.2.28 节。
如果需要 ACPI 平台错误接口,则条件性地需要以下表
BERT(启动错误记录表,第 18.3.1 节)
EINJ(错误注入表,第 18.6.1 节)
ERST(错误记录序列化表,第 18.5 节)
HEST(硬件错误源表,第 18.3.2 节)
SDEI(软件委派异常接口表,第 5.2.6 节,特别是表 5-6)
AEST(Arm 错误源表,第 5.2.6 节,特别是表 5-6)
RAS2(ACPI RAS2 特性表,第 5.2.21 节)
如果系统包含使用 PCC 通道的控制器,则为 PCCT(平台通信通道表),第 14.1 节
如果系统包含捕获板级系统状态并通过 PCC 与主机通信的控制器,则为 PDTT(平台调试触发表),第 5.2.29 节。
如果支持 NVDIMM,则为 NFIT(NVDIMM 固件接口表),第 5.2.26 节
如果存在视频帧缓冲,则为 BGRT(启动图形资源表),第 5.2.23 节
如果实现了 IPMI,则为 SPMI(服务器平台管理接口),第 5.2.6 节,特别是表 5-6。
如果系统包含 CXL 主机桥,则为 CEDT(CXL 早期发现表),第 5.2.6 节,特别是表 5-6。
如果系统支持 MPAM,则为 MPAM(内存分区与监视表),第 5.2.6 节,特别是表 5-6。
如果系统缺少持久存储,则为 IBFT(ISCSI 启动固件表),第 5.2.6 节,特别是表 5-6。
如果上述表不完全存在,内核可能能够也可能无法正常启动,因为它可能无法配置所有可用的设备。此表列表并非包罗万象;在某些环境中,可能需要其他表(例如第 18 节中的任何 APEI 表)来支持特定功能。
ACPI 检测¶
驱动程序应通过检查 ACPI_HANDLE 是否为空值、检查 .of_node 或设备结构中的其他信息来确定其 probe() 类型。这在“驱动程序建议”一节中有更详细的说明。
在非驱动程序代码中,如果在运行时需要检测 ACPI 的存在,则检查 acpi_disabled 的值。如果未设置 CONFIG_ACPI,acpi_disabled 将始终为 1。
设备枚举¶
ACPI 中的设备描述应使用标准认可的 ACPI 接口。与通过同一设备的设备树描述通常提供的信息相比,这些接口包含的信息可能较少。这也是 ACPI 有用原因之一——驱动程序考虑到它可能拥有关于该设备的较少详细信息,因而改用合理的默认值。如果在驱动程序中妥善处理,硬件可以随着时间的推移而改变和改进,而驱动程序完全无需更改。
时钟就是一个很好的例子。在 DT 中,需要指定时钟,并且驱动程序必须将其考虑在内。在 ACPI 中,假设 UEFI 会将设备(包括任何时钟设置)留在合理的默认状态下。如果由于某种原因驱动程序需要更改时钟值,这可以在 ACPI 方法中完成;驱动程序需要做的就是调用该方法,而不必关心该方法为更改时钟需要做什么。此后,可以通过更改 ACPI 方法所做的事情而不是驱动程序来随着时间的推移改变硬件。
在 DT 中,驱动程序如上例那样设置时钟所需的参数被称为“绑定”;在 ACPI 中,这些被称为“设备属性”,并通过 _DSD 对象提供给驱动程序。
ACPI 表是用一种称为 ASL(ACPI 源语言,规范第 19 节)的形式化语言描述的。这意味着总是有多种方法来描述同一事物——包括设备属性。例如,设备属性可以使用如下所示的 ASL 结构:Name(KEY0, “value0”)。然后,ACPI 设备驱动程序将通过评估 KEY0 对象来检索属性的值。但是,以这种方式使用 Name() 存在多个问题:(1) 与 DT 不同,ACPI 将名称(“KEY0”)限制为四个字符;(2) 没有维护名称列表的全行业注册表,从而阻碍了复用;(3) 属性值(“value0”)的定义也没有注册表,这同样使得复用变得困难;以及 (4) 当新硬件推出时,如何保持向后兼容性?_DSD 方法正是为了解决这类问题而创建的;Linux 驱动程序应**始终**将 _DSD 方法用于设备属性,而不要使用其他任何方法。
_DSM 对象(ACPI 第 9.14.1 节)也可用于向驱动程序传递设备属性。Linux 驱动程序仅在 _DSD 无法表示所需数据且无法为 _DSD 对象创建新的 UUID 时,才期望它被使用。请注意,对 _DSM 使用的规范甚至比 _DSD 还要少。正因如此,依赖于 _DSM 对象内容的驱动程序随时间推移将更难以维护;截至本文撰写时,_DSM 的使用导致了不少固件问题,因此不推荐使用。
驱动程序应**仅**在 _DSD 对象中查找设备属性;ACPI 规范第 6.2.5 节对 _DSD 对象进行了描述,但这仅描述了如何定义通过 _DSD 返回的对象的结构,以及如何通过特定的 UUID 定义特定的数据结构。Linux 应该只使用 _DSD 设备属性 UUID [4]
UUID: daffd814-6eba-4d8c-8a91-bc9bbf4aa301
可以通过向 [4] 提交 Pull Request 来注册通用的设备属性,以便它们可以在所有支持 ACPI 的操作系统中使用。尚未在 UEFI 论坛注册的设备属性也可以使用,但不能作为“uefi-”通用属性使用。
在创建新的设备属性之前,请务必检查它们以前是否未被定义过,并且既没有在 Linux 内核文档中注册为 DT 绑定,也没有在 UEFI 论坛中注册为设备属性。虽然我们不想简单地将所有 DT 绑定移到 ACPI 设备属性中,但我们可以从以前定义的内容中汲取经验。
如果需要定义新的设备属性,或者综合绑定定义以便其可用于任何固件是合理的,那么驱动程序的 DT 绑定和 ACPI 设备属性都有各自的审查流程。请同时使用它们。当驱动程序本身被提交到 Linux 邮件列表进行审查时,所需的设备属性定义必须同时提交。如果没有设备属性定义,支持 ACPI 并使用设备属性的驱动程序将不被认为是完整的。一旦该设备属性被 Linux 社区接受,它必须在 UEFI 论坛 [4] 进行注册,论坛将再次审查其在注册表中的一致性。这可能需要多次迭代。不过,UEFI 论坛将始终是设备属性定义的权威站点。
向 UEFI 论坛发出通知,表明有意注册一个以前未使用的设备属性名称,以此作为为以后使用保留该名称的手段,这可能是明智的。其他操作系统厂商也将提交注册请求,这可能有助于使流程更加顺畅。
一旦注册和审查完成,内核就会提供一个独立于是否正在使用 DT 或 ACPI 的接口来查找设备属性。应该使用此 API [5];它可以消除驱动程序探测函数中代码路径的一些重复,并防止 DT 绑定与 ACPI 设备属性之间产生分歧。
可编程电源控制资源¶
可编程电源控制资源包括电压/电流提供者(调节器)和时钟源等资源。
在使用 ACPI 时,预计根本不会使用内核时钟和调节器框架。
内核假定对这些资源的电源控制由电源资源对象(ACPI 第 7.1 节)表示。然后,ACPI 核心将根据需要正确处理资源的启用和禁用。为了使其工作,ACPI 假设每个设备都定义了 D 状态,并且这些状态可以通过可选的 ACPI 方法 _PS0、_PS1、_PS2 和 _PS3 进行控制;在 ACPI 中,_PS0 是用于将设备完全开启的方法,而 _PS3 用于将设备完全关闭。
使用这些电源资源有两个选项。它们可以
在进入电源状态 Dx 时被调用的 _PSx 方法中进行管理。
作为具有自己的 _ON 和 _OFF 方法的电源资源单独声明。然后通过 _PRx 将它们绑定回特定设备的 D 状态,_PRx 指定了设备处于 Dx 状态时需要开启哪些电源资源。内核随后跟踪使用电源资源的设备数量,并在需要时调用 _ON/_OFF。
内核 ACPI 代码还将假设 _PSx 方法遵循此类方法的正常 ACPI 规则
如果实现了 _PS0 或 _PS3 中的任意一个,则必须同时实现另一个方法。
如果设备在开启时需要使用或设置电源资源,ASL 应安排使用 _PS0 方法对其进行分配/启用。
在 _PS0 方法中分配或启用的资源应在 _PS3 方法中禁用或取消分配。
固件在将控制权移交给内核之前,会将资源保持在合理的状态。
_PSx 方法中的此类代码当然是非常特定于平台的。但是,这允许驱动程序抽象出操作设备的接口,并避免从 ACPI 表中读取特殊的非标准值。此外,对这些资源的使用进行抽象允许硬件随着时间的推移而改变,而无需更新驱动程序。
时钟¶
ACPI 假设时钟在控制权移交给内核之前已被固件(在本例中为 UEFI)初始化为某个工作值。这对外设(例如 UART 或 SoC 驱动的 LCD 显示器)具有重要影响。
当内核启动时,假定时钟已设置为合理的工作值。如果由于某种原因频率需要更改——例如为了电源管理而进行降频——设备驱动程序应期望该过程被抽象为可以调用的某个 ACPI 方法(有关应期望的标准方法的进一步建议,请参阅 ACPI 规范)。唯一的例外是 CPU 时钟,其中 CPPC 提供了比 ACPI 方法丰富得多的接口。如果未设置时钟,Linux 就没有直接的方法来控制它们。
如果 SoC 厂商希望提供对系统时钟的精细控制,他们可以通过提供可由 Linux 驱动程序调用的 ACPI 方法来实现这一点。但是,这**不**推荐,并且即使提供了此类方法,Linux 驱动程序也**不应**使用它们。此类方法目前尚未在 ACPI 规范中标准化,使用它们可能会将内核绑定到非常特定的 SoC,或者将 SoC 绑定到非常特定版本的内核,这都是我们极力避免的。
驱动程序建议¶
在为驱动程序添加 ACPI 支持时,**不要**移除任何 DT 处理。同一个设备可能会在许多不同的系统上使用。
尝试将驱动程序结构化为数据驱动的。也就是说,基于默认值以及驱动程序探测函数必须发现的其他任何内容,建立一个包含内部每个设备状态的 struct。然后,让驱动程序的其余部分根据该结构体的内容进行操作。这样做应该允许 ACPI 和 DT 功能之间的大多数分歧保持在探测函数内部,而不是分散在整个驱动程序中。例如
static int device_probe_dt(struct platform_device *pdev)
{
/* DT specific functionality */
...
}
static int device_probe_acpi(struct platform_device *pdev)
{
/* ACPI specific functionality */
...
}
static int device_probe(struct platform_device *pdev)
{
...
struct device_node node = pdev->dev.of_node;
...
if (node)
ret = device_probe_dt(pdev);
else if (ACPI_HANDLE(&pdev->dev))
ret = device_probe_acpi(pdev);
else
/* other initialization */
...
/* Continue with any generic probe operations */
...
}
将 MODULE_DEVICE_TABLE 条目保留在驱动程序中,以明确驱动程序从 DT 和 ACPI 探测的各种名称
static struct of_device_id virtio_mmio_match[] = {
{ .compatible = "virtio,mmio", },
{ }
};
MODULE_DEVICE_TABLE(of, virtio_mmio_match);
static const struct acpi_device_id virtio_mmio_acpi_match[] = {
{ "LNRO0005", },
{ }
};
MODULE_DEVICE_TABLE(acpi, virtio_mmio_acpi_match);
ASWG¶
ACPI 规范定期更改。例如,在 2014 年期间,发布了 5.1 版本,并且基本完成了 6.0 版本,其中大部分更改都是由 Arm 特定需求驱动的。拟议的更改在 UEFI 论坛的一部分 ASWG(ACPI 规范工作组)中提出和讨论。ACPI 规范的当前版本是 2022 年 8 月发布的 6.5 版。
所有 UEFI 成员均可参加该小组。有关小组会员资格的详细信息,请参阅 http://www.uefi.org/workinggroup。
Arm ACPI 内核代码的意图是尽可能紧密地遵循 ACPI 规范,并且只实现符合 UEFI ASWG 发布的标准的功能。在实际操作中,会有一些厂商提供糟糕的 ACPI 表或以某种方式违反标准。如果这是由于错误引起的,可能需要 quirks 和修复程序,但如果可能的话将予以避免。如果 ACPI 中缺少某些功能,导致无法在某个平台上使用,则应向 ASWG 提交 ECR(工程变更请求)并经过正常的审批流程;对于那些不是 UEFI 会员的人,Linux 社区的许多其他成员是会员,并且很可能愿意协助提交 ECR。
Linux 代码¶
包含在 Linux 源码中的、专属于 Arm 上 Linux 的各个项目如下列表所示
- ACPI_OS_NAME
此宏定义了当 ACPI 方法调用 _OS 方法时要返回的字符串。在 Arm 系统上,此宏默认将是“Linux”。可以使用命令行参数 acpi_os=<string> 将其设置为其他值。例如,其他架构的默认值是“Microsoft Windows NT”。
ACPI 对象¶
有关 ACPI 表和对象的详细期望列在文件 ACPI 表中。
参考资料¶
- [0] https://developer.arm.com/documentation/den0094/latest
文档 Arm-DEN-0094:“Arm Base System Architecture”(Arm 基础系统架构),版本 1.0C,日期:2022 年 10 月 6 日
- [1] https://developer.arm.com/documentation/den0044/latest
文档 Arm-DEN-0044:“Arm Base Boot Requirements”(Arm 基础启动要求),版本 2.0G,日期:2022 年 4 月 15 日
- [2] https://developer.arm.com/documentation/den0029/latest
文档 Arm-DEN-0029:“Arm Server Base System Architecture”(Arm 服务器基础系统架构),版本 7.1,日期:2022 年 10 月 6 日
- [3] http://www.secretlab.ca/archives/151,
2015 年 1 月 10 日,版权所有 (c) 2015,Linaro 有限公司,由 Grant Likely 撰写。
- [4] _DSD(设备特定数据)实现指南
- [5] 统一设备的内核代码
属性接口可以在 include/linux/property.h 和 drivers/base/property.c 中找到。