4、ACPI与热插拔:ACPI _EJ0/_STA方法、ACPI热插拔事件通知机制、ACPI与PCI子系统的交互

ACPI(Advanced Configuration and Power Interface)是x86/ARM64平台固件与操作系统之间的标准接口。在PCIe热插拔场景中,ACPI承担两个关键角色:电源/存在状态查询(通过AML方法)和热插拔事件通知(通过GPE/Notify)。本章深入分析ACPI与PCI子系统的交互机制,以及驱动开发者必须掌握的_EJ0、_STA等核心方法。

核心认知:ACPI热插拔是“固件告知OS,OS驱动执行”的协作模型。固件通过AML方法暴露槽位能力,OS通过ACPIdriver解析并触发PCI核心层完成枚举/销毁。

4.1 ACPI _EJ0与_STA方法:槽位能力描述

ACPI命名空间中的每个PCIe槽位设备(通常位于\_SB.PCI0.RP01等路径下)必须实现以下两个控制方法,OS才能识别其为热插拔槽位:

AML方法 功能定义 返回值 驱动调用时机
_EJ0 Eject(弹出)请求,OS调用此方法触发槽位断电/物理弹出 无(Void) 用户触发弹出操作时
_STA Status(状态),返回槽位当前是否存在设备、是否使能 Bitmask(0x0F标准位) 枚举/轮询/事件处理时
_PS0/_PS3 电源状态控制(可选,用于D0/D3hot切换) 无 电源管理事件

_STA返回值解析:

  • Bit 0:设备存在(1=存在)
  • Bit 1:设备使能(1=使能)
  • Bit 2:设备在UI中显示(1=显示)
  • Bit 3:设备功能正常(1=正常)
  • 完整值 0x0F 表示设备存在且正常工作
驱动开发者注意:在pciehp或acpiphp驱动中,_STA返回0x00表示槽位为空,驱动应销毁对应PCI设备;_STA返回0x0F表示设备在位,驱动应触发枚举。
// ASL示例:PCIe根端口RP01下的热插拔槽位
Device (RP01)
{
    Name (_ADR, 0x001C0000)  // PCI设备地址
    Method (_STA, 0, NotSerialized)
    {
        // 检查GPIO状态或槽位存在寄存器
        If (LEqual (SSTS, 0x01))
        {
            Return (0x0F)  // 设备存在且正常
        }
        Else
        {
            Return (0x00)  // 槽位为空
        }
    }
    Method (_EJ0, 1, NotSerialized)
    {
        // 执行物理弹出:断电、释放锁存器
        Store (0x00, PWRG)
        Sleep (100)
        Store (0x01, EJCTL)
    }
}
ACPI _EJ0 / _STA 方法调用流程 用户空间(sysfs) PCI热插拔核心层 ACPI驱动层 固件AML方法 物理槽位/GPIO 1. 用户写/sys/bus/pci/slots/ N/power或echo 1 > remove 2. 核心层调用acpi_bus_ trim/eject回调 3. ACPI驱动执行_EJ0 或_STA方法

4.2 ACPI热插拔事件通知机制

ACPI通过两种方式向OS传递热插拔事件:

  • GPE(General Purpose Event):硬件中断方式,槽位状态变化(如存在检测引脚电平跳变)触发GPE,ACPIdriver查询对应设备的_OST或_EXL方法。
  • Notify():AML代码主动调用Notify(Device, 0x00)或Notify(Device, 0x03),告知OS设备插入/移除。
通知类型 Notify值 含义 OS响应动作
Bus Check 0x00 设备插入或重新枚举 重新扫描PCI总线,枚举新设备
Device Check 0x01 特定设备状态变化 查询_STA,决定添加或移除
Eject Request 0x03 固件请求弹出设备 执行_EJ0,销毁PCI设备
Device Wake 0x02 设备唤醒系统 电源管理恢复
常见陷阱:部分固件在Notify(0x00)后不更新_STA返回值,导致驱动误判设备状态。驱动应在收到Notify后主动调用_STA确认,而非依赖缓存。
// 内核ACPIdriver热插拔事件处理核心路径
static void acpiphp_check_bridge(struct acpiphp_bridge *bridge)
{
    struct acpiphp_slot *slot;
    
    list_for_each_entry(slot, &bridge->slots, node) {
        unsigned long long sta;
        acpi_status status;
        
        // 1. 调用_STA获取当前状态
        status = acpi_evaluate_integer(slot->handle, "_STA", NULL, &sta);
        if (ACPI_FAILURE(status))
            continue;
        
        // 2. 判断设备是否存在
        if (sta & ACPI_STA_DEVICE_PRESENT) {
            if (!slot->device) // 新插入
                acpiphp_enable_slot(slot);
        } else {
            if (slot->device) // 已移除
                acpiphp_disable_slot(slot);
        }
    }
}
ACPI热插拔事件通知处理流程 硬件事件(GPE) AML Notify() ACPIdriver acpi_ev_queue_notify_request() Notify值 Bus Check (0x00) Device Check (0x01) Eject (0x03) PCI核心层:pci_scan_bus() / pci_stop_and_remove_bus_device()

4.3 ACPI与PCI子系统的交互接口

Linux内核通过以下数据结构将ACPI设备与PCI设备绑定:

内核组件 文件/模块 职责
acpiphp drivers/pci/hotplug/acpiphp_*.c ACPI热插拔驱动,监听ACPI事件并调用PCI核心
pci-acpi drivers/pci/pci-acpi.c PCI设备与ACPI句柄绑定,电源管理
pciehp drivers/pci/hotplug/pciehp*.c 原生PCIe热插拔(不依赖ACPI,但可共存)

交互关键函数:

  • acpi_pci_find_root():根据PCI域号查找对应ACPI根桥对象
  • acpi_bus_trim():移除ACPI设备及其子设备,触发PCI设备销毁
  • acpi_bus_scan():扫描ACPI命名空间,发现新设备并触发PCI枚举
  • pci_acpi_scan_root():ACPI描述PCI根桥时调用,创建PCI域
// ACPI事件到PCI动作的完整链路(以设备插入为例)
// 1. ACPIdriver收到Notify(0x00)
acpi_os_execute_deferred(acpi_ev_notify_dispatch);

// 2. 调用acpiphp的notify回调
acpiphp_notify(acpi_handle handle, u32 type, void *data)
{
    struct acpiphp_context *context = data;
    acpiphp_check_bridge(context->bridge);
}

// 3. 内部调用pci_scan_slot()枚举新设备
void acpiphp_enable_slot(struct acpiphp_slot *slot)
{
    struct pci_bus *bus = slot->bridge->pci_bus;
    struct pci_dev *dev;
    
    pci_lock_rescan_remove();
    dev = pci_scan_single_device(bus, slot->device);
    if (dev) {
        pci_bus_assign_resources(bus);
        pci_bus_add_device(dev);
    }
    pci_unlock_rescan_remove();
}
工程经验:在调试ACPI热插拔问题时,优先检查/sys/firmware/acpi/tables/DSDT中的槽位定义。使用acpidump导出AML,用iasl反编译后搜索_EJ0和_STA方法,确认固件是否正确实现了槽位控制。
ACPI与PCI子系统交互架构 ACPI子系统 ACPIdriver AML解释器 ACPI Namespace PCI子系统 PCI核心层 pciehp/acpiphp PCI设备驱动 pci-acpi.ko acpi_pci_bind() acpi_pci_unbind()

4.4 实际工程中的ACPI热插拔调试

在嵌入式平台(如ARM64服务器)上,ACPI热插拔是主要方案。以下是调试检查清单:

  1. 确认ACPI表:检查DSDT中PCIe根端口下是否有_EJ0和_STA方法
  2. 确认驱动加载:lsmod | grep acpiphp 确保acpiphp模块已加载
  3. 查看槽位状态:cat /sys/bus/pci/slots/*/power
  4. 模拟热插拔:echo 1 > /sys/bus/pci/slots/N/remove 触发_EJ0
  5. 跟踪事件:echo 'file drivers/pci/hotplug/acpiphp* +p' > /sys/kernel/debug/dynamic_debug/control
快速验证:在系统启动后,执行acpi_listen命令,然后物理插入/拔出PCIe设备,观察是否有ACPI事件输出。若无输出,说明固件未正确生成GPE或Notify。

通过本章学习,你应该掌握ACPI _EJ0/_STA方法的作用、ACPI热插拔事件通知的两种途径(GPE和Notify),以及ACPI与PCI子系统通过pci-acpi桥接模块的交互机制。这些知识是编写可靠PCIe热插拔驱动的理论基础。