第九章:PNP与电源管理——即插即用支持、设备启动与停止、电源状态转换、唤醒逻辑

说实话,PNP(即插即用)和电源管理这块,是很多驱动开发者容易忽略的“硬骨头”。我刚入行那会儿,总觉得只要把读写函数写对就行了,结果第一次做PCIe设备驱动时,设备插拔直接蓝屏,电源状态切换后设备“失联”……嗯,从那以后我再也不敢小看PNP和电源管理了。

这一章,我们就来聊聊Windows驱动中PNP和电源管理的核心机制。说白了,就是让系统知道你的设备什么时候来了、什么时候走了、什么时候该睡觉、什么时候该醒来。

9.1 即插即用(PNP)支持

PNP的核心思想是:设备可以随时接入或移除,系统自动识别并加载合适的驱动。你想想看,USB设备插上去就能用,背后就是PNP在干活。

9.1.1 PNP IRP 主功能码

PNP相关的请求通过IRP_MJ_PNP主功能码下发。常见的子功能码包括:

子功能码 含义 我遇到的坑
IRP_MN_START_DEVICE 启动设备,分配资源 资源冲突时记得检查CM_RESOURCE_LIST
IRP_MN_STOP_DEVICE 停止设备,释放资源 别忘了同步等待未完成的I/O
IRP_MN_REMOVE_DEVICE 移除设备,清理设备对象 我曾经忘记删除设备扩展中的自旋锁,导致内存泄漏
IRP_MN_SURPRISE_REMOVAL 意外移除(用户直接拔线) 这个必须快速响应,不能阻塞
IRP_MN_QUERY_CAPABILITIES 查询设备能力 设备描述符中的版本号要填对

9.1.2 设备启动流程

当系统检测到新设备时,会发送IRP_MN_START_DEVICE。驱动需要在这个请求中完成硬件初始化。我个人习惯在启动时做三件事:

  1. 解析资源列表——从AllocatedResources中提取I/O端口、内存范围、中断号
  2. 映射物理地址——如果是内存映射设备,调用MmMapIoSpace
  3. 连接中断——调用IoConnectInterrupt

核心要点:启动设备时,如果资源分配失败,必须返回失败状态,并回滚所有已分配的资源。我曾经见过一个驱动,中断连上了但内存映射失败,结果中断处理函数访问了无效地址,直接蓝屏。

9.1.3 设备停止与移除

停止和移除是启动的逆过程。顺序很重要:

// 停止设备时的典型处理顺序
1. 断开中断连接 (IoDisconnectInterrupt)
2. 取消所有未完成的I/O请求
3. 等待所有I/O完成
4. 取消映射内存 (MmUnmapIoSpace)
5. 释放硬件资源
6. 完成IRP

警告:在IRP_MN_REMOVE_DEVICE中,除了释放硬件资源,还必须删除设备对象(IoDeleteDevice)。但要注意,删除设备对象前,必须确保没有其他组件还在引用它。我建议在设备扩展中维护一个引用计数。

9.2 电源状态转换

Windows定义了六种系统电源状态(S0-S5),以及三种设备电源状态(D0-D3)。设备电源状态转换由IRP_MJ_POWER主功能码处理。

9.2.1 设备电源状态

状态 含义 功耗 恢复时间
D0 完全工作状态 最高 立即
D1 低功耗状态(上下文保留) 中等 几微秒
D2 更低功耗(上下文可能丢失) 几毫秒
D3 完全关闭 几乎为零 取决于硬件

说白了,D1和D2是“浅睡眠”,D3是“深睡眠”。我做过一个无线网卡驱动,D1状态下只需要关闭发射电路,接收电路还开着,这样唤醒时几乎无延迟。

9.2.2 电源IRP的处理

电源IRP分为两种:

  • IRP_MN_SET_POWER——设置电源状态(从D0到D3,或反过来)
  • IRP_MN_QUERY_POWER——查询是否允许状态转换

处理SET_POWER时,我建议遵循这个模式:

// 电源状态转换处理伪代码
if (Irp->Parameters.Power.State.DeviceState == PowerDeviceD0) {
    // 唤醒设备:恢复寄存器、重新初始化硬件
    RestoreHardwareContext(DeviceExtension);
    // 如果之前保存了上下文,现在恢复
} else {
    // 进入低功耗:保存上下文、关闭硬件
    SaveHardwareContext(DeviceExtension);
    // 根据目标状态决定关闭哪些功能
    if (Irp->Parameters.Power.State.DeviceState == PowerDeviceD3) {
        // 完全关闭
        PowerOffHardware(DeviceExtension);
    }
}

提示:我个人习惯在设备扩展中保存一个“当前电源状态”字段,这样在处理其他IRP时可以快速判断设备是否可用。如果设备处于D3状态,读写请求应该返回失败或排队等待唤醒。

9.3 唤醒逻辑

唤醒逻辑是电源管理中比较有意思的部分。设备在低功耗状态下,需要能够响应外部事件(比如网络数据包、用户按键)并唤醒系统。

9.3.1 唤醒机制

Windows支持两种唤醒方式:

  • 系统唤醒(System Wake)——设备将系统从S1-S3状态唤醒到S0
  • 设备唤醒(Device Wake)——设备从D1/D2状态恢复到D0,系统可能仍在S0

要实现唤醒,驱动需要:

  1. 在设备能力中声明支持唤醒(DeviceCapabilities->DeviceWake)
  2. 注册唤醒信号(比如GPIO中断或PCI PME#信号)
  3. 在进入低功耗前,使能唤醒逻辑

9.3.2 唤醒IRP的处理

唤醒请求通过IRP_MN_WAIT_WAKE处理。驱动收到这个IRP后,需要:

// 等待唤醒IRP的处理
1. 保存IRP到设备扩展(PendingWakeIrp)
2. 使能硬件唤醒信号
3. 返回STATUS_PENDING
4. 当唤醒事件发生时:
   a. 禁用唤醒信号
   b. 完成等待唤醒IRP(状态为STATUS_SUCCESS)
   c. 系统或设备开始恢复电源

注意:我曾经犯过一个错误——在完成等待唤醒IRP后,没有及时禁用唤醒信号。结果设备刚唤醒又被同一个信号再次唤醒,陷入了“唤醒-睡眠-唤醒”的死循环。正确的做法是:在唤醒处理函数中,先禁用唤醒源,再完成IRP。

9.4 知识体系总览

下面这张图总结了PNP与电源管理的核心逻辑:

PNP与电源管理核心逻辑 PNP流程 设备接入 → IRP_MN_START_DEVICE 资源分配 & 硬件初始化 正常工作(处理I/O请求) 设备移除 → IRP_MN_REMOVE_DEVICE 释放资源 & 删除设备对象 电源管理流程 D0(完全工作) IRP_MN_SET_POWER → D1/D2 保存上下文,使能唤醒 唤醒事件 → 恢复D0 恢复上下文,继续工作

9.5 实战经验总结

做了这么多年驱动,我总结了几条PNP和电源管理的“铁律”:

  • 资源分配必须可回滚——启动设备时,如果中途失败,要能干净地释放已分配的资源。我见过太多驱动在失败时直接返回,导致资源泄漏。
  • 电源状态转换要同步——设备扩展中维护一个电源状态锁,防止在状态转换过程中收到其他IRP。我曾经因为没加锁,导致在D0恢复过程中又收到了一个D3请求,设备直接“精神分裂”。
  • 唤醒逻辑要防抖——硬件唤醒信号可能有毛刺,建议在软件层面做去抖处理。我习惯在唤醒中断处理函数中加一个50ms的定时器,确认信号稳定后再完成唤醒IRP。
  • 意外移除要快速响应——IRP_MN_SURPRISE_REMOVAL不能阻塞,不能等待I/O完成。正确的做法是:标记设备为“已移除”,然后返回,让上层知道设备已经没了。

核心思想:PNP和电源管理,说白了就是让驱动学会“察言观色”——知道什么时候该干活,什么时候该休息,什么时候该说再见。把这些状态机理清楚了,你的驱动就成功了一大半。

嗯,这一章的内容就到这里。记住,PNP和电源管理不是可有可无的“附加功能”,而是驱动稳定性的基石。下次写驱动时,先把这些状态机画清楚,再动手写代码,你会发现少踩很多坑。