第九章: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。驱动需要在这个请求中完成硬件初始化。我个人习惯在启动时做三件事:
- 解析资源列表——从AllocatedResources中提取I/O端口、内存范围、中断号
- 映射物理地址——如果是内存映射设备,调用MmMapIoSpace
- 连接中断——调用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
要实现唤醒,驱动需要:
- 在设备能力中声明支持唤醒(DeviceCapabilities->DeviceWake)
- 注册唤醒信号(比如GPIO中断或PCI PME#信号)
- 在进入低功耗前,使能唤醒逻辑
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与电源管理的核心逻辑:
9.5 实战经验总结
做了这么多年驱动,我总结了几条PNP和电源管理的“铁律”:
- 资源分配必须可回滚——启动设备时,如果中途失败,要能干净地释放已分配的资源。我见过太多驱动在失败时直接返回,导致资源泄漏。
- 电源状态转换要同步——设备扩展中维护一个电源状态锁,防止在状态转换过程中收到其他IRP。我曾经因为没加锁,导致在D0恢复过程中又收到了一个D3请求,设备直接“精神分裂”。
- 唤醒逻辑要防抖——硬件唤醒信号可能有毛刺,建议在软件层面做去抖处理。我习惯在唤醒中断处理函数中加一个50ms的定时器,确认信号稳定后再完成唤醒IRP。
- 意外移除要快速响应——IRP_MN_SURPRISE_REMOVAL不能阻塞,不能等待I/O完成。正确的做法是:标记设备为“已移除”,然后返回,让上层知道设备已经没了。
核心思想:PNP和电源管理,说白了就是让驱动学会“察言观色”——知道什么时候该干活,什么时候该休息,什么时候该说再见。把这些状态机理清楚了,你的驱动就成功了一大半。
嗯,这一章的内容就到这里。记住,PNP和电源管理不是可有可无的“附加功能”,而是驱动稳定性的基石。下次写驱动时,先把这些状态机画清楚,再动手写代码,你会发现少踩很多坑。