1. 一条断链背后的电源管理调度哲学
调试 ACPI 的 _STA 方法时,我盯着 ACPIDetectPdoDevices -> ACPIGetDevicePresenceSync -> SyncEvalObject -> RestartContext 这条调用链整整看了一个下午。这串函数名乍看像是一堆毫无感情的内存地址,但真正追进去之后才发现,它几乎串联了 Windows 电源管理框架中最核心的一段"设备存在性探测"逻辑。如果你也在折腾电池电量读不到、电源适配器插拔不识别、或者笔记本合盖不休眠这类问题,那你迟早要跟这条链路打交道。
先说清楚这条链路是干什么的。当系统要判断某个电池设备 BAT1 是否真实存在时,ACPI 固件暴露的 _STA 方法决定了操作系统对设备的"信任程度"——返回 0x0F 表示设备存在且功能正常,返回 0x0B 表示存在但未启用,返回 0x00 则视为不存在。而 ACPIDetectPdoDevices 这个函数,正是通过 ACPIGetDevicePresenceSync 同步调用 BATP/ADP1/BAT1 等电源设备的 _STA,随后一层层落入 SyncEvalObject 的求值队列,最终由 RestartContext 来接管异步上下文的重启逻辑。
这篇内容适合正在调电池驱动、修电源适配器识别、或者单纯想把 ACPI 设备枚举机制捋清楚的驱动开发者和系统调试者。我尽量用实际调试中遇到的场景来讲,不堆理论。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ACPIDetectPdoDevices 的调用起点:谁在触发电源设备的存在性检查
2.1 从电源策略引擎到 ACPI 驱动的事件循环
在 Windows 的电源架构中,ACPIDetectPdoDevices 并不是一个随时被调用的普通函数。它通常出现在系统电源状态切换、AC 插拔事件、或者设备新增/移除的异步通知流程中。比如你插上电源适配器,Embedded Controller 会触发一个 SCI (System Control Interrupt),ACPI 驱动收到这个中断后进行 _Qxx 事件分发,事件处理最终会回调到电源策略引擎,而电源策略引擎需要重新确认当前连接的电源设备列表——这时候就会走到 ACPIDetectPdoDevices。
这个函数的核心职责可以概括为:枚举所有定义为 Power Source Device 的 ACPI 节点,逐个查询其存在状态,并更新内部维护的设备映射表。比较关键的一点是,这里的枚举不是遍历一个新的设备树,而是基于 ACPI 固件中 _SB 下的 _HID 或 _ADR 匹配结果,找出一组预设的电源相关设备列表。
2.2 PDO 是什么:物理设备对象与电池设备的映射关系
PDO(Physical Device Object)在 WDM(Windows Driver Model)里是老熟人了。但在这个上下文里,ACPIDetectPdoDevices 里的 PDO 特指 ACPI 驱动为电源设备创建的物理设备对象,BAT1 存在的 _STA 检查结果最终决定这个 PDO 是否被创建。
调试时你可能需要区分几类不同的 PDO 命名方式:
BAT1:电池设备节点,支持_STA、_BST(电池状态)、_BIF(电池信息)等方法ACPI\ACPI0003\0:AC 适配器节点,通常也有_PSR(电源源状态)方法ADP1:另一个常见的 AC 适配器节点名
ACPIDetectPdoDevices 会针对这些节点调用 ACPIGetDevicePresenceSync,同步等待 _STA 的求值结果,因此这个函数的执行语义是"阻塞式探测",不能放在 DISPATCH_LEVEL 以上的中断上下文里调用。
2.3 同步探测与异步通知的取舍逻辑
为什么不干脆全部异步呢?原因在于:PDO 的创建是后续所有电源操作的前提。如果 _STA 还没有求值完成就创建了 PDO,那么后续对 _BST 或 _BIF 的调用可能拿到错误的状态,甚至导致 IoInvalidateDeviceRelations 之后设备栈毁灭性地重建。所以 ACPI 驱动在设计上选择了 SyncEvalObject 这个同步求值入口,让 PDO 生命周期管理与 ACPI 方法求值结果严格对齐。
从这个角度再回看 RestartContext 就清晰了:当同步求值内部发生对象冲突(比如目标对象正在被某个异步事件占用)时,它需要把当前求值请求打包成一个 Context,挂到事件队列末尾等待重试。这就是为什么函数名里有 "Restart"——不是重启电脑,而是重启这次求值请求。
3. ACPIGetDevicePresenceSync 的细节:_STA 方法如何被翻译成设备状态
3.1 方法存在性检查与调用参数准备
ACPIGetDevicePresenceSync 这个函数,我拆开来看其实做了三件事:
- 从 ACPI 命名空间中取出目标设备节点(比如
\_SB.PCI0.LPCB.EC0.BAT1),拿到它的DEVICE_OBJECT指针 - 检查该节点是否定义了
_STA方法,如果定义了,就为调用准备参数缓冲区 - 调用
SyncEvalObject,把_STA的执行结果转换成布尔型的存在状态
第三步是最有意思的地方。_STA 方法返回的是一个 32 位无符号整数,其中 bit 0 到 bit 3 分别代表:
- Bit 0:设备是否存在
- Bit 1:设备是否启用
- Bit 2:设备是否在 UI 中显示
- Bit 3:设备是否功能正常
ACPIGetDevicePresenceSync 并不关心全部位,它只关心 bit 0 和 bit 1 的组合结果。按 ACPI 规范,如果 bit 0 为 0,设备直接视为不存在;如果 bit 0 为 1 但 bit 1 为 0,则设备存在但处于禁用状态,驱动会创建 PDO 但不会启动它。
3.2 同步等待与消息机制:为什么是 Sync 而不是 Async
名字里的 "Sync" 不是白叫的。SyncEvalObject 内部会创建一个 KEVENT,把 _STA 的求值请求封装成 ACPI_EVAL_CONTEXT 结构体,然后通过 ACPI_DRIVER_COMMON.RequestToEvaluate 发送给 ACPI 解释器。解释器执行 AML 字节码时,当前线程会阻塞在 KeWaitForSingleObject 上,直到求值完成、解释器回调 SyncEvalObject 的完成例程、设置事件后才继续执行。
这种设计保证了 _STA 返回的状态一定是"此时此刻"的真实硬件状态。但也带来了一个副作用:如果 AML 里的 _STA 实现写得有缺陷(比如死循环、长期占用互斥锁),系统线程会被卡死。调试电池问题时,我经常先看 `npxc
