ACPI表解析详解:RSDP到DSDT,_DSD/_HID/_CID到底怎么区分
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里的核心对象:
| 对象 | 全称 | 作用 |
|---|---|---|
_CRS | Current Resource Settings | 设备占用的资源——IO端口、MMIO范围、IRQ号 |
_PRT | PCI Routing Table | PCI设备INTx中断如何路由到中断控制器 |
_STA | Status | 设备是否存在、是否使能、是否正常 |
_HID | Hardware ID | 硬件标识符,OS据此加载驱动 |
_CID | Compatible ID | 兼容标识符,一个设备可以声明兼容另一个 |
_DSD | Device 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蓝屏0xA5 | AllocatePool不保证对齐 | 用AllocateAcpiMemory |
| IO APIC GSI基址写0 | PCIe设备中断随机挂死 | 前24个GSI留给Local APIC | GSI基址=Local APIC数量 |
| MCFG和E820重叠 | MMIO写到DRAM | MCFG地址落在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 |
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)