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 系统的要求,但它们也适用于多个操作系统。本文档的目的是仅描述 ACPI 和 Linux 在 Arm 系统上的交互——即 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 在这个领域以任何方式次于 Microsoft。将 ACPI 治理移至 UEFI 论坛已大大开放了规范开发流程,并且目前,对 ACPI 进行的大部分更改都是由 Linux 驱动的。

ACPI 使用的关键是支持模型。对于一般的服务器,硬件行为的责任不能仅仅是内核的领域,而必须在平台和内核之间进行划分,以便随着时间的推移进行有序的更改。ACPI 使操作系统无需了解硬件的所有细微细节,因此操作系统无需单独移植到每个设备。它允许硬件供应商负责电源管理行为,而无需依赖不受其控制的操作系统发布周期。

ACPI 也很重要,因为硬件和操作系统供应商已经制定了支持通用计算生态系统的机制。基础设施已到位,绑定已到位,流程已到位。当与垂直集成设备一起使用时,DT 确实满足了 Linux 的需求,但是没有好的流程来支持服务器供应商的需求。Linux 可能会通过 DT 实现这一目标,但这样做实际上只是重复了已经有效的东西。ACPI 已经满足了硬件供应商的需求,Microsoft 不会与 DT 合作,并且硬件供应商最终仍将提供两个完全独立的固件接口——一个用于 Linux,一个用于 Windows。

内核兼容性

ACPI 的主要动机之一是标准化,并使用它为 Linux 内核提供向后兼容性。在服务器市场中,软件和硬件通常会长期使用。ACPI 允许内核和固件就一个一致的抽象达成一致,即使硬件或软件发生变化,也可以随着时间的推移进行维护。只要支持该抽象,就可以更新系统,而不必更换内核。

当 Linux 驱动程序或子系统首次使用 ACPI 实现时,根据定义,它最终需要特定版本的 ACPI 规范——其基线。即使可能不是最佳的,ACPI 固件也必须继续使用最早的内核版本,该版本首先提供对该基线版本的 ACPI 的支持。可能需要额外的驱动程序,但是添加新功能(例如,CPU 电源管理)不应破坏旧内核版本。此外,ACPI 固件还必须与最新版本的内核一起使用。

与设备树的关系

Arm 的驱动程序和子系统中的 ACPI 支持在编译时绝不应与 DT 支持互斥。

在启动时,内核将仅使用一种描述方法,具体取决于从引导加载程序传递的参数(包括内核引导参数)。

无论使用 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 已被弃用,并且由于它们仅允许 32 位地址,因此在 arm64 上被忽略。

此外,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 是否为 null 值、检查 .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 节)也可用于将设备属性传达给驱动程序。仅当 _DSD 无法表示所需的数据,并且无法为 _DSD 对象创建新的 UUID 时,Linux 驱动程序才应期望使用它。请注意,与 _DSD 相比,对 _DSM 的使用甚至更少受到监管。由于这个原因,依赖于 _DSM 对象内容的驱动程序将更难以随着时间的推移进行维护;截至本文撰写之时,_DSM 的使用是造成相当多固件问题的原因,因此不建议使用。

驱动程序应仅在 _DSD 对象中查找设备属性;_DSD 对象在 ACPI 规范的 6.2.5 节中进行了描述,但这仅描述了如何定义通过 _DSD 返回的对象的结构,以及如何通过特定的 UUID 定义特定的数据结构。Linux 应仅使用 _DSD 设备属性 UUID [4]

  • UUID:daffd814-6eba-4d8c-8a91-bc9bbf4aa301

可以通过创建拉取请求到 [4] 来注册通用设备属性,以便它们可以在所有支持 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 处理。同一设备可以在许多不同的系统上使用。

尝试构建驱动程序,使其由数据驱动。也就是说,基于默认值和驱动程序探测函数必须发现的其他任何内容,设置一个包含内部每个设备状态的结构。然后,让驱动程序的其余部分根据该结构的内容进行操作。这样做应该允许 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 特定的要求驱动的。提出的更改将在 ASWG(ACPI 规范工作组)中进行介绍和讨论,该工作组是 UEFI 论坛的一部分。ACPI 规范的当前版本是 2022 年 8 月发布的 6.5 版本。

该小组的参与权向所有 UEFI 成员开放。有关小组会员资格的详细信息,请参阅 http://www.uefi.org/workinggroup

Arm ACPI 内核代码的目的是尽可能地遵循 ACPI 规范,并且仅实现符合 UEFI ASWG 发布的标准的功能。实际上,会有供应商提供错误的 ACPI 表或以某种方式违反标准。如果是由于错误,则可能需要怪癖和修复程序,但如果可能,将避免使用。如果 ACPI 中缺少阻止其在平台上使用的功能,则应将 ECR(工程变更请求)提交给 ASWG 并通过正常的批准流程;对于那些不是 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 基础系统架构”,版本 1.0C,日期为 2022 年 10 月 6 日

[1] https://developer.arm.com/documentation/den0044/latest

文档 Arm-DEN-0044:“Arm 基础启动要求”,版本 2.0G,日期为 2022 年 4 月 15 日

[2] https://developer.arm.com/documentation/den0029/latest

文档 Arm-DEN-0029:“Arm 服务器基础系统架构”,版本 7.1,日期为 2022 年 10 月 6 日

[3] http://www.secretlab.ca/archives/151,

2015年1月10日,版权 (c) 2015, Linaro Ltd.,作者:Grant Likely。

[4] _DSD(设备特定数据)实施指南

https://github.com/UEFI/DSD-Guide/blob/main/dsd-guide.pdf

[5] 统一设备的内核代码

属性接口可在 include/linux/property.h 和 drivers/base/property.c 中找到。

作者