Arm 系统上的 ACPI

ACPI 可用于 Armv8 和 Armv9 系统,这些系统设计为遵循 BSA (Arm 基本系统架构) [0] 和 BBR (Arm 基本启动要求) [1] 规范。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 允许内核和固件就一致的抽象达成一致,即使硬件或软件发生更改,也可以随着时间的推移进行维护。只要支持抽象,就可以更新系统,而无需更换内核。

当首次使用 ACPI 实现 Linux 驱动程序或子系统时,按照定义,它最终需要特定版本的 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 已被弃用,并且在 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 节)也可用于将设备属性传达给驱动程序。 仅当 _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缺少某些功能而导致无法在平台上使用,则应向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”, 版本 1.0C, 日期 2022年10月6日

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

文档 Arm-DEN-0044: “Arm Base Boot Requirements”, 版本 2.0G, 日期 2022年4月15日

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

文档 Arm-DEN-0029: “Arm Server Base System Architecture”, 版本 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 中找到。

作者