WinXP下USB驱动开发(七)
2019-07-14 03:01 发布
生成海报
在 Windows 2000 和 Windows 98 中,操作系统接管了大部分电源管理工作。当然,这是因为只有操作系统才能真正了解电源管理的内部过程。例如,系统 BIOS 负责的电源3.3.2.6. 管理不能区分应用程序使用的屏幕和屏幕保护程序使用的屏幕之间的区别。但操作系统可以区分开这种不同,从而确定是否可以关闭显示器。
作为计算机全局电源策略,操作系统支持一些用户接口元素,用户可以通过这些接口元素控制最终的电源管理策略。这些用户接口元素包括控制面板、开始菜单上的命令、控制设备唤醒特征的 API 。通过向设备发送 IRP ,内核的电源管理部件实现了操作系统的电源策略。 WDM 驱动程序主要是作为响应这些 IRP 的被动角 {MOD}。
设备的某个驱动程序需要充当设备电源策略的管理者。通常都是由功能驱动程序充当这个角 {MOD}。电源管理器可以改变整个系统的电源状态。功能驱动程序接收电源管理器发来的 IRP( 系统 IRP) ,作为设备电源策略的管理者,功能驱动程序用设备理解的术语翻译这些 IRP 并引发新的 IRP( 设备 IRP) 。当响应设备 IRP 时,功能驱动程序需要关心设备专有的细节。设备硬件会有自己的上下文信息,不应该在设备处于低电源期间丢失这些信息。例如,键盘驱动程序会保存锁定键 ( 如 CAPS-LOCK 、 NUM-LOCK 、 SCROLL-LOCK) 、 LED 等信息。功能驱动程序有责任保存并恢复这些上下文信息。某些设备带有唤醒特征,当外部事件发生时,这些设备可以唤醒系统;功能驱动程序应与用户协同工作以确保唤醒特征在需要时有效。许多功能驱动程序还管理含有大量 IRP 的队列 ( 设备读写 IRP) ,因此当设备电源状态转变时需要停止或释放这些队列。
处于设备堆栈底端的总线驱动程序有责任控制设备的电流和执行任何与设备唤醒特征相关的电气步骤。过滤器驱动程序通常作为电源管理 IRP 通过的管道,它们用专用的协议向下层驱动程序传递电源管理请求。
WDM 模型使用与 ACPI(Advanced Configuration and Power Interface) 规范 ( 见 http://www.teleport.com/~acpi/spec.htm )相同的术语来描述电源状态。设备能呈现图 3-3-2 -0 所描述的四种电源状态。在 D0 状态中,设备处于全供电状态。在 D3 状态中,设备处于无供电 ( 或最小限度的电流 ) 状态。中间的 D1 和 D2 状态指出设备的两个不同睡眠状态。随着设备从 D0 状态变化到 D3 状态,设备将消耗越来越少的电力,同时需要保留的当前状态上下文信息也越来越少。而设备再转变回 D0 状态的延迟期则相应增加.
Microsoft 规定了不同类型设备的类专用电源需求。这个需求规范可以在 http://www.microsoft.com/hwdev/specs/PMref/ 找到。例如,这个规范要求每个设备至少要支持 D0 和 D3 两个状态。输入设备 ( 键盘、鼠标等 ) 还应该支持 D1 状态。 Modem 设备需要另外支持 D2 状态。设备类上的这些不同规定可能来源于设备的用途和工业上的实践。
操作系统不直接处理设备的电源状态,由设备驱动程序专门处理。系统使用一组与 ACPI 设备状态类似的系统电源状态来控制电源,见图 3-3-2 -1 。 Working 状态是全供电状态,计算机可以实现全部功能。程序仅能在 Working 状态下执行.
其它系统电源状态对应更小的电力需求配置,在这些系统电源状态中,计算机不能执行任何指令。 Shutdown 状态就是电源关闭状态。 Hibernate 状态是另一种 Shutdown 状态,它把计算机的整个状态都记录到硬盘上,因此在电源恢复供电时可以使计算机快速恢复到记录前的状态。在 Hibernate 和 Working 状态之间是三个有不同电力消耗级别的中间状态。
系统初始化后即进入 Working 状态。大部分设备也以 D0 状态启动,但某些设备的驱动程序会在设备启动时使设备进入低电源消耗状态。在系统启动并正常运行后,这些设备的驱动程序才使设备进入一个稳定的状态,在这个状态中,系统电源处于 Working 状态,而设备处于的状态取决于具体活动和设备自身的能力。
用户的活动或外部事件会导致电源状态的改变。一个常见的电源状态转换情景是用户在开始菜单上选择 “ 关闭系统 ” 中的 “standby” 选项,使计算机进入等待状态。在响应这个命令过程中,电源管理器首先向每个驱动程序发送带有 IRP_MN_QUERY_POWER 副功能码的 IRP_MJ_POWER 请求以询问设备能否接受即将到来的电源关闭请求。如果所有驱动程序都同意,电源管理器将发送第二个带有 IRP_MN_SET_POWER 副功能码的电源管理 IRP ,然后驱动程序把其设备置入低电源状态以响应这个 IRP 。如果有任何一个驱动程序否决了这个查询,电源管理器仍旧发出这个 IRP_MN_SET_POWER 请求,但它用原来的电源级别换成了请求的电源级别。
系统并不总是发送 IRP_MN_QUERY_POWER 请求。某些事件 ( 如电池电力将要耗尽 ) 必须被无条件接受,并且操作系统也不再发出查询请求。如果查询发出后,并且驱动程序也接受了请求的电源状态,那么驱动程序将不再启动任何会妨碍未来电源状态设置请求的操作。例如,磁带机驱动程序在使一个进入低电源状态的查询请求成功返回前先确保当前没有执行备份操作。另外,该驱动程序还拒绝任何后来的备份命令,除非是另一个电源状态设置请求。
该结构体在电源管理中占据很重要的地位,其主要进行细微的描述电源的状态和剩余时间等方面的信息,该结构体描述如下;
typedef struct _POWER_STATUS {
BYTE PS_AC_Line_Status;
BYTE PS_Battery_Status;
BYTE PS_Battery_Flag;
BYTE PS_Battery_Life_Percentage;
WORD PS_Battery_Life_Time;
} POWER_STATUS;
Ø PS_AC_Line_Status 描述电源 AC 输入线的状态。
Ø PS_Battery_Status 描述电池的状态。
Ø PS_Battery_Status 描述电池先前的状态或者是改变时的状态。
Ø PS_Battery_Life_Percentage 描述电池还剩余多少电,以百分计。
Ø PS_Battery_Life_Time 描述电池还能支持多少时间,以秒钟计。
通常,我们总是把设备设置成与设备的当前活动、设备的唤醒特征、设备的能力、以及迫近的系统状态,相一致的最低的电源状态。 PnP 管理器在启动设备后 ( 也可能是其它时间 ) 的很短时间内向设备发出查询设备能力请求 ——IRP_MN_QUERY_CAPABILITIES 请求,该请求的参数是一个 DEVICE_CAPABILITIES 结构,该结构体将描述系统的电源状态信息, DEVICE_CAPABILITIES 结构体描述。
typedef struct _DEVICE_CAPABILITIES {
USHORT Size;
USHORT Version;
ULONG DeviceD1:1;
ULONG DeviceD2:1;
ULONG LockSupported:1;
ULONG EjectSupported:1;
ULONG Removable:1;
ULONG DockDevice:1;
ULONG UniqueID:1;
ULONG SilentInstall:1;
ULONG RawDeviceOK:1;
ULONG SurpriseRemovalOK:1;
ULONG WakeFromD0:1;
ULONG WakeFromD1:1;
ULONG WakeFromD2:1;
ULONG WakeFromD3:1;
ULONG HardwareDisabled:1;
ULONG NonDynamic:1;
ULONG WarmEjectSupported:1;
ULONG NoDisplayInUI:1;
ULONG Reserved:14;
ULONG Address;
ULONG UINumber;
DEVICE_POWER_STATE DeviceState[PowerSystemMaximum];
SYSTEM_POWER_STATE SystemWake;
DEVICE_POWER_STATE DeviceWake;
ULONG D1Latency;
ULONG D2Latency;
ULONG D3Latency;
} DEVICE_CAPABILITIES, *PDEVICE_CAPABILITIES;
Ø DeviceState 域为与每个系统状态对应的可能存在的设备最高级状态数组。
Ø SystemWake 域为最低级的系统电源状态,在该状态中设备可以生成唤醒信号。 PowerSystemUnspecified 域指出设备不能唤醒系统。
Ø DeviceWake 域为最低级的设备电源状态,在该状态中设备可以生成唤醒信号。 PowerDeviceUnspecified 指出设备不能生成唤醒信号
Ø D1Latency 域为设备从 D1 切换到 D0 状态所需的最长时间 (100 微秒单位 ) 。
Ø D2Latency 域为设备从 D2 切换到 D0 状态所需的最长时间 (100 微秒单位 ) 。
Ø D3Latency 域为设备从 D3 切换到 D0 状态所需的最长时间 (100 微秒单位 ) 。
Ø WakeFromD0 、 WakeFromD1 、 WakeFromD2 和 WakeFromD3 标志,指出当设备处于指定的状态中时,设备的系统唤醒特征是否是可操作的
在 WDM 驱动当中,驱动程序响应 IRP_MJ_POWER 例程表示支持电源管理。电源状态描述保存在 IO_STACK_LOCATION 的 Parameters 联合中。该联合提供了响应的四种电源状态,如下表 3-3-2 所示。
表 3-3-2 IO_STACK_LOCATION的 Parameters.Power 子结构中的各个域
域名
描述
SystemContext
电源管理器内部使用的上下文值
Type
DevicePowerState 或 SystemPowerState (POWER_STATE_TYPE 类型的枚举值 )
State
电源状态,可为 DEVICE_POWER_STATE 或 SYSTEM_POWER_STATE
ShutdownType
指出转换到 PowerSystemShutdown 状态的原因代码
在管理器和驱动沟通使用了 IRP_MJ_POWER 类型的 I/O 请求包,该功能附带了四个辅助码。 IRP_MN_QUERY_POWER 确定预期的电源状态改变是否安全; IRP_MN_SET_POWER 命令驱动程序改变电源状态; IRP_MN_WAIT_WAKE 命令总线驱动程序使用唤醒特征、使功能驱动程序能了解唤醒信号何时发生; IRP_MN_POWER_SEQUENCE 为上下文保存和恢复提供优化。
对于整个操作系统而言,所有驱动程序,包括过滤器驱动程序和功能驱动程序通常都向其下面的驱动程序传递电源管理请求,除 IRP_MN_QUERY_POWER 请求外。在功能驱动 ( 例如、 USB 驱动 ) ,系统控制程序如何把电源管理请求传递到低级驱动程序有特殊的规则。图 3-3-2 -2 显示了三种可能的处理过程。第一,在释放一个电源管理请求的控制之前,你必须调用 PoStartNextPowerIrp ,即使你以错误状态完成该 IRP ,也要这样做。做这个调用的原因是,电源管理器自己需要维持一个电源管理请求队列,所以必须通知它确实可以出队一个请求,以及向设备发送下一个请求。除了调用 PoStartNextPowerIrp ,你还必须调用专用例程 PoCallDriver( 代替 IoCallDriver) 来向下层驱动程序发送请求。
图 3-3-2 -2 系统电源状态
当 PNP 管理器发出 IRP_MN_SET_POWER 后,例程函数可以设置系统和设备电源状态。在 DDK 中 SystemPowerState 和 DevicePowerState 为系统和设备电源状态。在 PNP 中是如何区分那种类型的电源状态呢? DDK 中 I/O 参数 Parameters.Power.Type 标识电源状态类型。如果 Parameters.Power.Type 为 SystemPowerState 则重新设置的是系统电源状态,反之亦然。
IRP_MN_QUERY_POWER 请求发生在 IRP_MN_SET_POWER 之前,主要用于确定是否具备修改电源状态的能力。
当 PNP 管理器发出 IRP_MN_POWER_SEQUENCE 请求,主要用于指出设备是否已经进入制定设电源状态。
IRP_MN_POWER_SEQUENCE 请求制定三种电源状态, SequenceD1 、 SequenceD2 、 SequenceD2 。该请求后电源状态保存在 Parameters.PowerSequence 当中。
当 PNP 管理器发出 IRP_MN_WAIT_WAKE 请求,指定设备是否具有硬件唤醒特征,这允许它们在外部事件发生时可以唤醒沉睡中的系统。当系统产生唤醒请求时,会调用 PoRequestPowerIrp 函数发出 IRP_MN_WAIT_WAKE 请求,其实其他 IRP_MN_SET_POWER 、 IRP_MN_QUERY_POWER 等请求也都是由该函数发起,但 IRP_MN_POWER_SEQUENCE 除外,请求后由 USB 总线处理。 PoRequestPowerIrp 函数描述如下;
NTSTATUS
PoRequestPowerIrp(
IN PDEVICE_OBJECT DeviceObject,
IN UCHAR MinorFunction,
IN POWER_STATE PowerState,
IN PREQUEST_POWER_COMPLETE CompletionFunction,
IN PVOID Context,
OUT PIRP *Irp OPTIONAL);
Ø MinorFunction 于指定为 IRP_MN_QUERY_POWER , IRP_MN_SET_POWER 和 IRP_MN_WAIT_WAKE 之一。
Ø PowerState 域特地为 IRP_MN_SET_POWER 和 IRP_MN_QUERY_POWER 准备,指出需要请求的电源状态。
Ø CompletionFunction 域为完成用例函数。
Ø Context 域为需要向用例函数传递的参数。
设备对象中的两个标志位,见表 3-3-3 ,控制电源管理的各个方面。当你在 AddDevice 函数中调用了 IoCreateDevice 例程之后,这两个位都将被置 0 ,你可以根据具体环境设置这两个位。
表 3-3-3 . DEVICE_OBJECT 中的电源管理标志
标志
简要描述
DO_POWER_PAGABLE
驱动程序的 IRP_MJ_POWER 派遣例程必须运行在 PASSIVE_LEVEL 级
DO_POWER_INRUSH
该设备在上电时需要大电流
如果 IRP_MJ_POWER 请求的派遣函数必须运行在 PASSIVE_LEVEL 级别,你需要设置 DO_POWER_PAGABLE 标志。该标志的名称含有相关的含义,因为只有在 PASSIVE_LEVEL 级上才允许分页操作。如果你把该标志设置为 0 ,则电源管理器可以在 DISPATCH_LEVEL 级上向你发送电源管理请求。实际上,在当前发行的 Windows 2000 中总应该这样做。
如果你的设备在上电时需要大电流,设置 DO_POWER_INRUSH 标志,这可以使多个有这样要求的设备不同时上电。另外,电源管理器会在 DISPATCH_LEVEL 级上向 inrush 设备发送电源管理请求,这意味着你不能同时设置 DO_POWER_PAGABLE 标志。
如果设备的 ASL 描述中这样指定,那么系统的 ACPI 过滤器驱动程序将在 PDO 中自动设置 INRUSH 标志,这样系统会正确地顺序化有 INRUSH 标志设备的电源供应,因此你并不需要在自己的设备对象中设置这个标志。但是,如果系统不能自动确定你的设备是否需要 inrush 对待,你需要自己设置这个标志。
设备的所有设备对象中的 PAGABLE 和 INRUSH 标志设置都要一致。如果 PDO 设置了 PAGABLE 标志,则其它设备对象也应该设置 PAGABLE 标志。否则将发生代码为 DRIVER_POWER_STATE_FAILURE 的 bug check( 一个 PAGABLE 设备在一个非 PAGABLE 设备之上是合法的,但相反则不行 ) 。如果设备对象设置了 INRUSH 标志,那么它自己和任何它下面的低级设备对象都不应该设置 PAGABLE 标志,否则将发生代码为 INTERNAL_POWER_ERROR 的 bug check 。如果你正写一个磁盘驱动程序,不要忘了为响应设备分页文件的 PnP 通知,你可以随时在分页和非分页状态间改变。
设备电源管理代码编写相当复杂,如果处理不当将会带来很严重的错误。在 DDK 环境下,支持 NT 和 WDM 两种驱动模式,电源管理代码需要自己编写进行处理。在微软新推出的 WDK 环境下,新推出的 WDF 驱动模式很好的解决了这个问题。微软认为电源管理应该模块化,处理方式基本一致,而且技术不过关的工程师很难将其处理完美,因为微软提供了 WDK 选择支持电源管理的功能,帮助工程师完成电源管理。但是、我们的 USB 驱动采用的是 WDM 模式,如下流程图(图 3-3-2 -3 )将说明设备电源管理的流程。
“ 空闲 ” 处理同样是 USB 驱动非常重要的地方,当长时间未时候使用 USB 的时候,因该让设备进入 “ 空闲 ” 状态。在 USB 驱动中可以通过 KeSeTimer 设置定时器进行适时检测设备 “ 空闲 ” 状态。
在 USBD.SYS 处理 USB“ 空闲 ” 状态设置的时候, IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION 可以提交 “ 空闲 ” 通知。 USB_IDLE_CALLBACK_INFO 用于设置 IDLE 的回调函数信息,其实也是一种完成用例的体现。
IDLE 处理是一个独立而且频繁的过程,在处理 IDLE 的时候, IO_WORKITEM 相关操作不得不将其引入,该结构体的意识是定义一个 I/O 工作项, Windows 中将 IDLE 处理放入一个工作项当中进行,该工作项可以随时唤醒。工作项相关的几个函数有 IoAllocateWorkItem 、 IoQueueWorkItem 和 IoFreeWorkItem 。这些函数的功能参照 DDK 描述所示,如表 3-3-4 . 。
表 3-3-4 . 函数描述
标志
简要描述
IoAllocateWorkItem
allocates a work item.
IoQueueWorkItem
打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮