ACPI表定制开发实战 - 第2节
2.1 ACPI 表结构:系统固件与 OS 的契约书
ACPI 表是系统固件(BIOS/UEFI)传递给操作系统的一组结构化数据块,它们描述了硬件拓扑、电源管理能力、设备配置和运行时接口。OS 在引导早期通过 RSDP(Root System Description Pointer) 找到这些表,然后按需解析。
2.1.1 核心表分类
| 表名 | 全称 | 作用 | 是否必选 |
|---|---|---|---|
| RSDP | Root System Description Pointer | ACPI 入口指针,位于 EBDA 或 UEFI 配置表 | 是 |
| RSDT | Root System Description Table | 32 位表指针数组,指向其他 ACPI 表 | 是 |
| XSDT | Extended System Description Table | 64 位版本的 RSDT,现代系统优先使用 | 是 |
| FADT | Fixed ACPI Description Table | 定义电源管理硬件寄存器、中断模型、SCI 路由 | 是 |
| DSDT | Differentiated System Description Table | 主命名空间定义,包含设备对象和控制方法 | 是 |
| SSDT | Secondary System Description Table | 辅助命名空间,用于扩展或覆盖 DSDT | 否 |
| MADT | Multiple APIC Description Table | 描述中断控制器(APIC、I/O APIC、GIC)拓扑 | 是 |
| SLIT | System Locality Information Table | NUMA 节点间的距离矩阵 | 否 |
| SRAT | System Resource Affinity Table | CPU/内存与 NUMA 节点的亲和关系 | 否 |
实战要点:定制开发中最常修改的表是 DSDT 和 SSDT。DSDT 是主表,SSDT 用于打补丁或添加新设备。FADT 中的 SCI 中断号和 SMI 命令端口在调试时经常需要核对。
2.1.2 表头通用结构
每个 ACPI 表(除 RSDP 外)都以相同的 36 字节表头开始:
typedef struct {
char Signature[4]; // 表签名,如 "DSDT"
uint32_t Length; // 表总长度(含表头)
uint8_t Revision; // 表版本
uint8_t Checksum; // 校验和,使整个表所有字节和为 0
char OEMID[6]; // OEM ID
char OEMTableID[8]; // OEM 表 ID
uint32_t OEMRevision; // OEM 版本
uint32_t CreatorID; // 创建者工具 ID
uint32_t CreatorRevision; // 创建者工具版本
} __attribute__((packed)) ACPI_TABLE_HEADER;
校验和陷阱:修改 DSDT/SSDT 后必须重新计算 Checksum,否则 OS 会拒绝加载整张表,导致系统无法进入 ACPI 模式。计算方式:所有字节累加后取低 8 位,结果为 0 则校验通过。
2.2 命名空间:设备与对象的层次树
ACPI 命名空间(Namespace)是一棵以 \(根)为起点的树形结构,所有设备、对象、控制方法都挂载在这棵树上。DSDT 和 SSDT 的内容共同构成完整的命名空间。
2.2.1 命名规则
- 根节点:
\(反斜杠),命名空间的起点。 - 分隔符:
.(点号),用于分隔路径层级。 - 名称长度:每个名称段固定为 4 个字符(如
_SB_、PCI0)。 - 绝对路径:从根开始,如
\_SB_.PCI0.I2C0。 - 相对路径:从当前作用域开始,如
PCI0.I2C0。
2.2.2 标准命名空间分支
| 路径 | 含义 | 常见子对象 |
|---|---|---|
\_SB_ |
系统总线(System Bus) | PCI0、I2C0、SPI0、UAR0 |
\_PR_ |
处理器(Processor) | CPU0、CPU1、… |
\_TZ_ |
热区(Thermal Zone) | TZ00、TZ01、… |
\_GPE |
通用事件(General Purpose Events) | L01、L02、… |
\_SI_ |
系统指示器(System Indicators) | 用于睡眠/唤醒状态 |
开发习惯:自定义设备建议放在
\_SB_ 下,避免与标准分支冲突。例如添加一个虚拟传感器设备:\_SB_.VSEN。
2.3 设备对象:硬件在 ACPI 中的化身
设备对象(Device Object)是命名空间中代表物理或逻辑硬件单元的节点。每个设备对象可以包含 _ADR(地址)、_HID(硬件 ID)、_CID(兼容 ID)、_STA(状态) 等控制方法,以及子设备。
2.3.1 设备对象声明示例
Device (\_SB_.PCI0.I2C0)
{
Name (_HID, "ACPI0011") // I2C 控制器硬件 ID
Name (_CID, "PNP0C02") // 兼容 ID
Name (_ADR, 0x001F0001) // PCI 地址
Method (_STA, 0, NotSerialized)
{
Return (0x0F) // 设备存在且启用
}
Device (TPAD) // 子设备:触摸板
{
Name (_HID, "ELAN0100")
Name (_CRS, ResourceTemplate()
{
I2cSerialBus (0x15, ControllerInitiated, 400000, AddressingMode7Bit, "\\_SB_.PCI0.I2C0", 0, ResourceConsumer)
})
}
}
2.3.2 常用设备对象属性
| 属性/方法 | 类型 | 说明 |
|---|---|---|
_HID |
Name (String) | 硬件 ID,如 "PNP0C0C"(电源按钮) |
_CID |
Name (String/List) | 兼容 ID,用于驱动匹配 |
_ADR |
Name (Integer) | 设备地址(PCI 总线号/设备号/功能号) |
_STA |
Method | 返回设备状态(0x0F=启用,0x00=禁用) |
_CRS |
Name (ResourceTemplate) | 当前资源设置(IRQ、I/O、内存、I2C 地址等) |
_PRW |
Name (Package) | 电源唤醒能力(GPE 位 + 睡眠状态) |
调试技巧:在 Linux 下使用
acpidump 导出 DSDT,然后用 iasl -d 反编译。搜索 Device (XXX) 可以快速定位设备对象。Windows 下可用 RWEverything 查看 ACPI 表。
2.4 控制方法:ACPI 的“可执行代码”
控制方法(Control Method)是用 ACPI 源语言(ASL)编写的函数,由 OS 调用以执行硬件操作。方法名以 下划线开头 的是 ACPI 规范定义的标准方法(如 _STA、_ON、_OFF),以字母开头的为 OEM 自定义方法。
2.4.1 方法定义语法
Method (方法名, 参数个数, 序列化标志)
{
// 局部变量:Local0 ~ Local7
// 参数:Arg0 ~ Arg7
// 操作:Store、Add、Subtract、Notify 等
Return (结果)
}
2.4.2 标准控制方法速查
| 方法 | 参数 | 返回值 | 用途 |
|---|---|---|---|
_STA |
0 | Integer | 查询设备状态(0x0F=启用) |
_ON |
0 | Void | 开启设备电源 |
_OFF |
0 | Void | 关闭设备电源 |
_PS0 |
0 | Void | 进入 D0 状态(全功率) |
_PS3 |
0 | Void | 进入 D3 状态(关闭) |
_CRS |
0 | Buffer | 返回当前资源描述 |
_SRS |
1 (Buffer) | Void | 设置资源描述 |
_DSM |
4 (UUID, Rev, Func, Args) | Buffer/Integer | 设备特定方法(通用扩展接口) |
实战案例:自定义方法实现 GPIO 翻转。在
\_SB_.GPIO 设备下添加 Method (TOGG, 0, Serialized) { Store (0x01, Local0); Notify (\_SB_.GPIO, 0x80); Return (Local0); }。OS 调用 TOGG() 即可触发 GPIO 状态切换。
2.5 全局锁:多处理器环境下的同步机制
全局锁(Global Lock)是 ACPI 提供的一种硬件级互斥机制,用于在多个处理器(或 OS 与固件)之间同步对共享资源的访问。它通过 FADT 表中的 Global Lock 寄存器 实现。
2.5.1 全局锁的工作原理
- 请求锁:OS 或固件向 Global Lock 寄存器写入请求位。
- 仲裁:硬件仲裁器决定哪个请求者获得锁。
- 持有锁:获得锁的实体可以安全访问共享资源(如 ACPI 控制方法中的全局变量)。
- 释放锁:操作完成后,写入释放位,仲裁器将锁交给下一个等待者。
2.5.2 全局锁在 ASL 中的使用
// 在控制方法中获取全局锁
Method (LOCK_ACQ, 0, Serialized)
{
Acquire (\_GL, 0xFFFF) // 等待超时 0xFFFF 毫秒
// 访问共享资源
Store (0x01, Local0)
Release (\_GL) // 释放全局锁
Return (Local0)
}
性能警告:全局锁的获取和释放有较大开销(通常需要 SMI 或 SCI 交互)。不要在频繁调用的方法(如
_STA)中使用全局锁。优先使用 ASL 的 Serialized 标志或局部变量 Local0~Local7 来避免竞争。
2.5.3 全局锁 vs 序列化方法
| 特性 | 全局锁(\_GL) | 序列化方法(Serialized) |
|---|---|---|
| 作用域 | 跨处理器、跨方法 | 仅单个方法实例 |
| 实现方式 | 硬件寄存器 + 仲裁器 | AML 解释器内部互斥 |
| 开销 | 高(SMI/SCI 交互) | 低(纯软件) |
| 适用场景 | 固件与 OS 共享数据 | 防止同一方法重入 |
| 超时机制 | 支持(Acquire 超时参数) | 不支持(等待当前实例完成) |
2.6 本章知识结构总结
| 核心概念 | 关键要点 | 常见陷阱 |
|---|---|---|
| ACPI 表结构 | RSDP→RSDT/XSDT→DSDT/SSDT;表头含校验和 | 修改后未更新 Checksum 导致表被拒绝 |
| 命名空间 | 树形结构,根为 \,名称段固定 4 字符 | 路径大小写错误或名称段长度不对 |
| 设备对象 | Device 声明,包含 _HID/_ADR/_STA/_CRS | 缺少 _STA 导致 OS 认为设备不存在 |
| 控制方法 | ASL 函数,标准方法以下划线开头 | 序列化标志选择不当导致重入问题 |
| 全局锁 | 硬件互斥,用于跨处理器同步 | 在频繁调用的方法中使用导致性能下降 |
下一步实践:使用
iasl 反编译你的机器 DSDT,找到 \_SB_ 下的 PCI0 设备,观察它的 _ADR 和 _CRS 内容。这是理解 ACPI 表结构最直接的方式。