ACPI表是BIOS工程师每天打交道的东西,但大部分人只知其名不知其结构。本文从RSDP入口逐层拆解到DSDT设备描述,把_ CRS/_PRT/_STA/_DSD/_HID/_CID全部串清楚。

一、ACPI表是什么——操作系统和硬件的"菜单"

你去一家从没去过的餐馆,坐下来,服务员递给你一本菜单。你不知道厨房里有什么锅、什么灶,但你翻到"热菜"那页,看到了宫保鸡丁——你知道这道菜能点,而且知道它大概什么味。

ACPI表就是这本菜单。操作系统走进一台陌生机器,它不知道你用的是Intel还是AMD,不知道内存插在哪条总线上,不知道有几个PCIe Root Port。它翻开ACPI表:“哦,DSDT告诉我有个嵌入式控制器在IO 0x62/0x66”,“MADT告诉我IOAPIC地址是0xFEC00000”,“MCFG告诉我PCIe配置空间映射在0xE0000000”。

没有这本菜单,OS就得靠猜——猜错一次就是蓝屏。

BIOS工程师有一半的Bug都出在ACPI表上——要么表本身写错了,要么OS解析错了。


二、从RSDP开始的俄罗斯套娃

ACPI表的入口只有一个:RSDP(Root System Description Pointer)

OS怎么找到RSDP

传统BIOS:OS扫描0xE0000~0xFFFFF区域找"RSD PTR "签名(8字节,注意末尾有个空格)。

UEFI:固件通过EFI Configuration Table直接暴露RSDP。OS从EFI System Table的ConfigurationTable里按GUID捞:

// MdePkg/Include/Guid/Acpi.h
#define EFI_ACPI_TABLE_GUID \
  { 0x8868e871, 0xe4f1, 0x11d3, \
    { 0xbc, 0x22, 0x00, 0x80, 0xc7, 0x3c, 0x88, 0x81 } }

套娃结构

RSDP本身没多少东西,就几十个字节:一个签名、一个校验和、一个指向XSDT的物理地址。现代系统都用XSDT(64位指针),RSDT是给老32位系统准备的。

RSDP → XSDT → ┌─ FADT  (全局控制寄存器)
              ├─ DSDT  (设备描述,AML字节码)
              ├─ SSDT  (补充设备描述,可多个)
              ├─ MADT  (中断控制器拓扑)
              ├─ MCFG  (PCIe MMIO配置空间)
              ├─ FACS  (固件ACPI控制结构)
              └─ MIGD/HPET/DBG2/... (其他表)

XSDT就是个目录页,里面按Table Signature挂了一堆表指针。每张表独立,有自己的Signature和Checksum。

// MdeModulePkg/Universal/Acpi/AcpiTableDxe/AcpiTableProtocol.c
// InstallAcpiTable() 把BIOS构建好的表注册到UEFI系统
Status = gBS->InstallConfigurationTable (
                &gEfiAcpiTableGuid,
                Table->AcpiTable
                );

踩坑:XSDT里所有表指针必须4字节对齐。不对齐,Windows 10直接ACPI_BIOS_ERROR蓝屏。某ODM厂商调试一款AMD APU笔记本时,ACPI表分配用的是AllocatePool而不是AllocateAcpiMemory(后者保证4K对齐),搞了整整三天——Windows的ACPI驱动解析XSDT时对未对齐指针做了memcpy,触发页面错误。


三、FADT和DSDT——一张管全局,一张管所有设备

FADT:全局开关在哪

**FADT(Fixed ACPI Description Table)**告诉OS基本的硬件寄存器地址:PM1a_CNT_BLK在哪(控制休眠/关机的IO端口),SCI中断用哪个IRQ,ACPI Timer地址是什么。

// 构建FADT时填入核心字段
Fadt->FirmwareCtrl = (UINT32)(UINTN)Facs;
Fadt->Dsdt         = (UINT32)(UINTN)Dsdt;
Fadt->SmiCmd       = AcpiSmiCmd;        // SMI命令端口
Fadt->Pm1aEvtBlk   = Pm1aEventBlockAddress;
Fadt->Pm1aCntBlk   = Pm1aControlBlockAddress;

DSDT:每个设备长什么样

**DSDT(Differentiated System Description Table)**是ACPI的灵魂。它是一大段AML字节码,描述系统里每一个设备——CPU、PCIe控制器、USB、电池、风扇、温度传感器。

说人话:FADT是"全局开关在哪",DSDT是"每个设备长什么样、怎么操控它"。

DSDT里的核心对象:

对象全称作用
_CRSCurrent Resource Settings设备占用的资源——IO端口、MMIO范围、IRQ号
_PRTPCI Routing TablePCI设备INTx中断如何路由到中断控制器
_STAStatus设备是否存在、是否使能、是否正常
_HIDHardware ID硬件标识符,OS据此加载驱动
_CIDCompatible ID兼容标识符,一个设备可以声明兼容另一个
_DSDDevice Specific Data设备特定数据,扩展属性(现代ACPI新增)

看一段实际的AML反编译结果:

Device (LPC0) {
    Name (_HID, EisaId("PNP0A05"))  // 通用LPC桥
    Name (_CRS, ResourceTemplate() {
        IO (Decode16, 0x0060, 0x0060, 1, 4)  // 键盘端口
        IO (Decode16, 0x0064, 0x0064, 1, 1)  // 键盘命令端口
    })
}

这段告诉OS:LPC桥下面的键盘控制器在IO 0x60和0x64。如果_CRS写错了,键盘不能用——不是硬件问题,是ACPI表告诉OS了一个错误地址。


四、_HID vs _CID vs _DSD——最容易搞混的三个

_HID:我是谁

_HID是设备的主标识符。OS看到_HID后去匹配驱动。两种格式:

  • EISA ID:如PNP0A05(通用LPC桥)、PNP0C14(ACPI Embedded Controller)
  • ACPI ID字符串:如"INTC1027"(Intel Thermal Sensor)、"AMDI0030"(AMD I2C Controller)
Device (TPM2) {
    Name (_HID, "MSFT0101")  // TPM 2.0
    Name (_CRS, ResourceTemplate() {
        Memory32Fixed (ReadWrite, 0xFED40000, 0x5000)
    })
}

OS看到MSFT0101就去加载TPM 2.0驱动。

_CID:我兼容谁

_CID兼容标识符。一个设备可以声明"我兼容另一个设备的接口",这样OS可以用那个设备的驱动来驱动它。

典型场景:你做了一个新的I2C控制器,但接口跟Intel LPSS I2C完全一样。你不想写新驱动,于是:

Device (I2C0) {
    Name (_HID, "MYNV0001")  // 你的新硬件ID
    Name (_CID, "INT3322")   // 兼容Intel LPSS I2C
}

OS先找MYNV0001的驱动,找不到就用INT3322的驱动。_CID是后备方案,不是替代。

_DSD:我的扩展属性

_DSD(Device Specific Data)是ACPI 5.1引入的。之前的_HID/_CID只能告诉OS"我是什么设备",但不能告诉OS"我的具体参数"。_DSD通过一个结构化的Property列表解决这个问题:

Device (I2C1) {
    Name (_HID, "INT3322")
    Name (_DSD, Package () {
        ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),  // Standard Property UUID
        Package () {
            Package () { "clock-frequency", 400000 },     // 400KHz
            Package () { "scl-gpio", 24 },                 // SCL引脚
            Package () { "sda-gpio", 25 },                 // SDA引脚
        }
    })
}

Linux内核大量使用_DSD来获取设备参数——I2C时钟频率、GPIO编号、中断号、 regulator名称。在DT(Device Tree)世界这些是dts文件里的节点属性,在ACPI世界就是_DSD。

对象一句话举例
_HID我是谁"INT3322" → Intel LPSS I2C
_CID我兼容谁"INT3322" → 用Intel LPSS驱动
_DSD我的参数clock-frequency=400000

五、_PRT:PCI中断路由——ACPI里最难的部分

_PRT(PCI Routing Table)描述PCI设备的INTx中断如何路由到中断控制器。这是ACPI里最难正确实现的部分之一。

// PCIe Root Port的_PRT示例
Device (PCI0) {
    Name (_PRT, Package () {
        // [设备号.功能号] → 中断映射
        // 格式: {PCI Address, Pin, Source, SourceIndex}

        // Slot 0, INTA → GSI 16
        Package () { 0x0000FFFF, 0, 0, 16 },
        // Slot 0, INTB → GSI 17
        Package () { 0x0000FFFF, 1, 0, 17 },
        // Slot 0, INTC → GSI 18
        Package () { 0x0000FFFF, 2, 0, 18 },
        // Slot 0, INTD → GSI 19
        Package () { 0x0000FFFF, 3, 0, 19 },

        // Slot 1, INTA → GSI 17 (swizzled)
        Package () { 0x0001FFFF, 0, 0, 17 },
    })
}

INTx Swizzling规则:PCI规范要求每个slot的INTA物理连接到上一级的INTB,INTB连到上一级的INTC,依此类推。这样同一根总线上不同slot的INTA不会全挤到同一个中断引脚。

_PRT写错了,现象是:设备能枚举到,驱动能加载,但只要一产生中断就随机挂死——因为IRQ号对不上。


六、MADT和MCFG——中断和PCIe的路标

MADT:中断控制器拓扑

**MADT(Multiple APIC Description Table)**描述中断控制器。最重要的两个子表:Local APIC地址和IO APIC地址+GSI映射范围。

Madt->LocalApicAddress = PcdGet32(PcdCpuLocalApicBaseAddress);
Madt->Flags            = EFI_ACPI_1_0_PCAT_COMPAT;

踩坑:IO APIC的GSI基址写成了0(应该是24),导致所有PCIe设备的中断路由偏移了24个GSI号。设备能枚举到,驱动能加载,但一产生中断就随机挂死。前24个GSI留给Local APIC,IO APIC从24开始。

MCFG:PCIe配置空间

MCFG告诉OS每个PCIe Segment的配置空间映射在MMIO的哪个地址:

McfgEntry->BaseAddress     = RootBridge->PciExpressBaseAddress;
McfgEntry->PciSegmentGroup = RootBridge->PciSegmentGroup;
McfgEntry->StartBusNumber  = 0;
McfgEntry->EndBusNumber    = 255;

MCFG的BaseAddress必须64KB对齐(PCIe规范要求),且Range不能和DRAM的E820预留区域重叠。重叠了?Windows用MCFG地址做MMIO读写,结果写到了DRAM——数据就是垃圾。


七、ASL→AML:从源码到字节码

ACPI源代码是ASL(ACPI Source Language),类似C语法。BIOS工程师写ASL,通过IASL编译器转成AML(ACPI Machine Language)字节码。OS里的ACPI解释器(ACPICA,由Intel维护)执行AML。

ASL源码 → [IASL编译器] → AML字节码 + .hex (C数组,方便嵌入固件)

在EDK2里,ASL文件放在Platform或Silicon的AcpiTables目录下:

Platform/Intel/YourBoard/AcpiTables/Dsdt.asl
Silicon/Intel/CoffeelakeSiliconPkg/AcpiTables/Ssdt.asl

编译后AML被嵌在Firmware Volume中。BIOS POST时,ACPI Table Protocol Driver读取FV中的表注册到UEFI系统:

// AcpiTableDriverEntryPoint遍历FV,发现所有ACPI表
for (Index = 0; Index < sizeof(mAcpiTableFileList); Index++) {
  AcpiTablePublishTables (ImageHandle, &mAcpiTableFileList[Index]);
}

AML调试技巧

AML出错了没有行号、没有函数名,只有一个冷冰冰的16进制offset。调试方法:

  • AcpiExec:ACPICA参考实现里的测试工具,在用户态加载ACPI表验证AML语法和逻辑
  • Store(“DEBUG:EnteringMethodX”, Debug):在ASL里插桩,通过ACPI Debug Port看执行流
  • WinDbg !amli:Microsoft的AML调试扩展,!amli dns能dump OS眼中的设备命名空间

八、校验和——一个字节不对整张表被丢弃

Windows和Linux加载ACPI表时,第一件事做校验和验证:从表头开始逐字节累加,结果必须是0。任何一个字节不对,整张表被丢弃。

// ACPI Spec 5.2.9 校验和计算
UINT8 Sum = 0;
for (UINTN i = 0; i < Table->Length; i++) {
  Sum += ((UINT8 *)Table)[i];
}
// Sum必须等于0

Windows的ACPI驱动比Linux严格得多。Linux内核能容忍一些非致命的表错误,Windows直接蓝屏0x000000A5(ACPI_BIOS_ERROR)。

踩坑:动态构造SSDT时内存没清零

某款笔记本BIOS动态构造SSDT描述TPM设备。TPM在SPI总线上,MMIO地址初始化后才知道。BIOS工程师用AllocatePool分配Buffer放SSDT,忘了清零。SSDT表头后面的padding区域有随机垃圾数据。校验和没问题(checksum不覆盖超出表长度的部分),但Windows的AML解释器扫描到随机字节,尝试解析成非法opcode,触发ACPI Exception然后蓝屏。

解决:用AllocateZeroPool代替AllocatePool,或写表前手动ZeroMem


九、实战踩坑总结

现象根因经验
XSDT指针未对齐Windows蓝屏0xA5AllocatePool不保证对齐用AllocateAcpiMemory
IO APIC GSI基址写0PCIe设备中断随机挂死前24个GSI留给Local APICGSI基址=Local APIC数量
MCFG和E820重叠MMIO写到DRAMMCFG地址落在DRAM预留区检查E820表避免重叠
动态SSDT没清零AML解析异常蓝屏AllocatePool有残留数据用AllocateZeroPool
_PRT中断路由写错设备枚举正常但中断挂死INTx Swizzling规则搞反按PCI规范逐slot核对
多了一个反斜杠INACCESSIBLE_BOOT_DEVICE后处理脚本误插0x5C永远不要脚本后处理AML

最后一条经验:蓝屏INACCESSIBLE_BOOT_DEVICE不一定是存储驱动问题。 有20%的case是ACPI表把存储设备路径搞丢了。如果你刚改了ACPI就蓝屏这个,先用acpidump对比改了什么——大概率是ACPI问题。


源码路径索引

内容关键文件
ACPI表注册MdeModulePkg/Universal/Acpi/AcpiTableDxe/AcpiTableProtocol.c
ACPI GUID定义MdePkg/Include/Guid/Acpi.h
ACPI表头结构MdePkg/Include/IndustryStandard/Acpi.h
ACPI DXE驱动MdeModulePkg/Universal/Acpi/AcpiTableDxe/AcpiTableDxe.inf
FADT结构定义MdePkg/Include/IndustryStandard/Acpi50.h
规范ACPI Specification 6.5 / PI Specification Volume 5
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐