WDF开发详解

原文地址:https://blog.csdn.net/lang_eva/article/details/109676808

WDF开发详解

添加设备:hdwwiz

KMDF驱动程序框架

KMDF 驱动程序框架由对象和事件回调例程构成。KMDF 框架中所有的事物都由对象表示,各种事件处理都由事件回调例程来完成。

学习KMDF编程,主要是学习 KMDF 的各种对象、对象函数和时间回调函数的编程。

一、

1.KMDF 对像

为了实现基于对象的技术,微软精心设计了对象模型并进行了封装,提供了属性方法和事件。无论是内核模式的驱动程序还是用

户模式的驱动程序,都采用同一套对象模型构建,采用同一个基础承载。

1.1对象概念

对象是 KMDF 的基础,KMDF 框架中所有的事物都由对象来表示,如Driver、Device、Request等。

下面介绍有关 KMDF 对象的基本知识。

(1).对象包含属性、方法和事件

属性(properties):可以获取和设置的一个数值。

例如:IRP 的对象是 WDFFREQUEST 。IRP 的Information 就是 WDFFREQUEST 的一个属性,可以获取或设置:

ULONG_PTR WdfRequestGetInformation

IN WDFREQUEST Request;

};

方法(method):对对象操作的函数。

比如:status = WdfDriverCreate(); //创建驱动对象

事件(event):当特定事件发生时,KMDF 调用事件例程。

比如:EvtDeviceControl 是队列对象的一个事件,当相关的应用程序调用 DeviceIoControl 函数时,KMDF 就调用 EvtIoDeviceControl例程。

(2).可以定义一个属于对象的环境变量结构,这样当对象创建时,KMDF 框架相应的分配一个属于该对象的环境变量结构内存单元

这一点非常有用,驱动程序中参数的定义均可在各种对象的环境变量结构中进行。

对象的环境变量结构定义如下:

typedef struct _ABC_CONTEXT {

...

}ABC_CONTEXT,*PABC_CONTEXT;

下面定义表示获取该结构地址指针的函数是 GetAbcContext 。

WDF_DECLARE_CONTEXT_TYPE_WITHNAME( ABC_CONTEXT, GetAbcContext)

若使用 WDF_DECLARE_CONTEXT_TYPE( ABC_CONTEXT ),则获取该结构地址指针的函数是 WdfObjectGet_ABC_CONTEXT  。

(3). 创建对象

创建对象时,输入参数包含一个 WDF_OBJECT_ATTRIBUTES 结构,其定义如下:

typedef struct _WDF_OBJECT_ATTRIBUTES {

    // Size in bytes of this structure

    ULONG Size;

    // Function to call when the object is deleted

   PFN_WDF_OBJECT_CONTEXT_CLEANUP EvtCleanupCallback;

    // Function to call when the objects memory is destroyed when the

    // the last reference count goes to zero

   PFN_WDF_OBJECT_CONTEXT_DESTROY EvtDestroyCallback;

    // Execution level constraints for Object

    WDF_EXECUTION_LEVEL ExecutionLevel;

    // Synchronization level constraint for Object

   WDF_SYNCHRONIZATION_SCOPE SynchronizationScope;

    //

    // Optional Parent Object

    //

    WDFOBJECT ParentObject;

    //

    // Overrides the size of the context allocated as specified by

    // ContextTypeInfo->ContextSize

    //

    size_t ContextSizeOverride;

    //

    // Pointer to the type information to be associated with the object

    //

   PCWDF_OBJECT_CONTEXT_TYPE_INFO ContextTypeInfo;

} WDF_OBJECT_ATTRIBUTES, *PWDF_OBJECT_ATTRIBUTES;

Size:结构的字节长度。

ContextSizeOverride:对象的环境变量结构字节长度。

ContextTypeInfo:对象的环境变量结构信息。

上述三项不用关心,初始化和设置函数会自动赋值。

EvtCleanupCallback:对象快删除时的清除例程。

EvtDestroyCallback:对象删除时的删除例程。

上述二项通常用一个就可以了。

ExecutionLevel:KMDF 调用事件例程的中断运行级别。有三种选择:

WdfExecutionLevelInheritFromParent:继承父对象中断运行级别,但有些除外,如,DPC对象、此为默认值。

WdfExecutionLevelPassive:IRQL = PASSIVE_LEVEL

WdfExecutionLevelDispatch:IRQL <= DISPATCH_LEVEL

SynchronizationScope:KMDF 调用事件例程的同步标示。同步只同一时刻只能有一个回调例程运行,这样可有效对硬件

 或数据实现保护性访问,有以下4中选择:

WdfSynchronizationScopeInheritFromParent:继承父对象同步特性。此为默认值。

WdfSynchronizationScopeDevice: 设备对象范围下的同步,设备对象下的队列和文件对象的回调例程执行将同步。如果设置同一设备对象

下的中断延迟过程调用、工作项、定时器配置项的 AutomaticSerialzation 为 TRUE ,则这些对象的

回调例程执行也将同步调用。系统获取设备对象的同步锁之后,调用回调例程,保证同一时刻之恩能

有一个回调例程在运行。

WdfSynchronizationScopeQueue:队列对象范围下的同步。需要设置队列对象的夫对象(设备对象)的同步标示为WdfSynchronizationScopeQueue

 队列对象的同步标示为WdfSynchronizationScopeInheritFromParent。如果设置同一队列对象下的中断延迟

 过程调用、工作项、定时器配置项的 AutomaticSerialzation 为 TRUE,则这些对象的回调例程执行也将同步

 调用。

WdfSynchronizationScopeNone:不同步。

ParentObject:父对象。

对象属性(attributes)的初始化语句如下:

WDF_OBJECT_ATTRIBUTES_INIT(&Attributes);

若有对象环境变量结构,则设置语句为:

WDF_OBJECT_ATTRIBUTES_SET_CONTEXT_TYPE( &Attributes, ABC_CONTEXT );

上面的两条语句,可以由下面的一条语句来实现:

WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE( &Attributes, ABC_CONTEXT );

如果对象环境变量结构有地址指针,且申请分配了内存,则当对象删除时,就需要释放内存。可以设置对象的清楚例程,在清除例程中释放内存。比如:

Attributes.EvtCleanupCallback = AbcEvtCleanup;

对象的创建函数和相应的对象有关系,没有一个统一的格式。对于一些对象,因为包含配置结构,因此需要先初始化配置结构,然后在调用创建函数,比如:

WDF_ABC_CONFIG_INIT( &Config );//初始化配置,没有统一格式

WdfAbcCreate( &Attributes, &Config, &Handel );//创建对象,没有统一格式

(4).对象生命期和删除

每个对象都有一个父对象,还可以有一个或多个子对象。

KMDF 框架控制下面对象的生命周期:

驱动对象(WDFDRIVER);

设备对象(WDFDEVICE) for FDO,filter DO,and FDO;

文件对象(WDFFILEOBJECT);

请求对象(WDFFREQUEST),程序自创建的除外。

但通常情况下,不需要删除所创建的对象,KMDF框架会删除对象及其子对象。

2.KMDF 程序结构

一个即插即用的 KMDF 设备驱动程序包括:

(1).一个 DriverEntry 例程;

(2).一个 EvtDriverDeviceAdd 例程,类似于 WDM 的 AddDevice 例程;

(3).一个或多个I/O 队列;

(4).一个或多个I/O事件回调例程,类似于 WDM 的 DispatchXxx 例程。

(5).支持的即插即用和电源管理回调例程;

(6).支持的 WMI 回调例程,用于管理计算机系统(关于 WMI 例程,本书不做讨论);

(7).其他回调例程,如对象的清除例程,中断处理例程、DMA例程等。

2.1 DriverEntry 例程

DriverEntry 例程负责驱动程序和框架的初始化,所有的驱动程序都必须包含 DriverEntry 例程。当第一次装在驱动程序时,调用 DriverEntry 例程。

在 DriverEntry 例程中主要是创建驱动对象和设置 EvtDriverDeviceAdd 例程地址。

首先介绍一下驱动对象 (WDFDRIVER)。

驱动对象的配置结构是 WDF_DRIVER_CONFIG ,其定义如下:

typedef struct _WDF_DRIVER_CONFIG {

    //

    // Size of this structure in bytes

    //

    ULONG Size;

    //

    // Event callbacks

    //

   PFN_WDF_DRIVER_DEVICE_ADD EvtDriverDeviceAdd;

    PFN_WDF_DRIVER_UNLOAD    EvtDriverUnload;

    //

    // Combination of WDF_DRIVER_INIT_FLAGS values

    //

    ULONG DriverInitFlags;

    //

    // Pool tag to use for all allocations made by the framework on behalf of

    // the client driver.

    //

    ULONG DriverPoolTag;

} WDF_DRIVER_CONFIG, *PWDF_DRIVER_CONFIG;

EvtDriverDeviceAdd:设备对象添加例程。

EvtDriverUnload:卸载例程。负责删除由 DriverEntry 例程所分配的驱动程序范围内的资源,如内存。

DriverInitFlags:初始化标识位。 WdfDriverInitNonPnpDriver 表示不是即插即用设备驱动程序; WdfDriverInitNoDispatchOverride 表示没有 I/O 处理例程。

DriverPoolTag:Debuggers显示的内存标识,4字节。

驱动对象的配置结构初始化函数如下:

VOID

WDF_DRIVER_CONFIG_INIT{

__out PWDF_DRIVER_CONFIG Config,

__in_opt PFN_WDF_DRIVER_DEVICE_ADD EvtDriverDeviceAdd

};

驱动对象的创建函数如下:

NTSTATUS

WdfDriverCreate{

__in PDRIVER_OBJECT DriverObject,

__in PCUNICODE_STRING RegistryPath,

__in_opt PWDF_OBJECT_ATTRIBUTES DriverAttributes,

__in PWDF_DRIVER_CONFIG DriverConfig,

__out_opt WDFDRIVER* Driver

};

参数说明:

DriverObject 和 RegistryPath 是 DriverEntry 例程的入口参数。

DriverAttributes: 对象属性。 WDF_NO_OBJECT_ATTRIBUTES ,表示无属性说明。

DriverConfig:驱动对象的配置结构。

Driver:驱动对象创建句柄。 WDF_NO_HANDLE ,表示不需要。

下面是一个 KMDF 范例中一个基本的 DriverEntry 例程。

NTSTATUS

DriverEntry( IN PDRIVER_OBJECT DriverObject, 

IN PUNICODE_STRING RegistryPath )

{

WDF_DRIVER_CONFIG config;

NTSTATUS status;

//初始化驱动对象的配置结构,设置 DeviceAdd 例程入口地址

WDF_DRIVER_CONFIG_INIT(&config, CharSample_EvtDeviceAdd);

//创建驱动对象,没有对象属性和驱动对象环境变量结构

status = WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE);

return status;

}

下面是一个KMDF 范例中一个复杂的 DriverEntry 例程。

NTSTATUS

DriverEntry( IN PDRIVER_OBJECT DriverObject, 

IN PUNICODE_STRING RegistryPath )

{

WDF_DRIVER_CONFIG config;

NTSTATUS status;

WDFDRIVER driver;

WDF_OBJECT_ATTRIBUTES driverAttributes;

//初始化驱动对象的环境变量结构

WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE (&driverAttributes, DRIVER_CONTEXT);

WDF_DRIVER_CONFIG_INIT(&config, RegSample_EvtDeviceAdd);

status = WdfDriverCreate(

       DriverObject,

       RegistryPath,

       &driverAttributes, // Driver Attributes

       &config, // Driver Config Info

       &driver // hDriver

       );

if (NT_SUCCESS (status)) {

LoadRegistryParameters(driver);

}

return status;

}

2.2 EvtDriverDeviceAdd 例程

在驱动初始化后,PnP 管理器调用驱动程序的 DeviceAdd 例程来初始化由该驱动程序所控制的设备。在 DeviceAdd 例程中,驱动程序创建一个设备对象作为 

目标 I/O  设备,并将设备对象附着到设备堆栈中。

在设备被首次枚举时, DeviceAdd 例程在系统初始化时被调用。当系统运行时,任何时候一个新设备被枚举,系统都将调用 DeviceAdd 例程。 

在 KMDF 中, DeviceAdd 例程的基本职责是:创建设备对象、一个或多个 I/O 队列和设备 GUID 接口,设置各种事件的回调例程,如即插即用例程、电源管理

例程、电源管理例程、I/O 处理例程等。

设备对象 (WDFDEVICE)的创建函数如下:

NTSTATUS

WdfDeviceCreate(

__inout PWDFDEVICE_INIT* DeviceInit,

__in_opt PWDF_OBJECT_ATTRIBUTES DeviceAttributes,

__out WDFDEVICE* Device

);

DeviceInit: EvtDriverDeviceAdd 例程的入口函数。

DeviceAttributes: 对象属性。

Device: 设备对象创建句柄。

设备 GUID 接口的创建函数如下:

NTSTATUS

WdfDeviceCreateDeviceInterface(

__in WDFDEVICE Device,

__in CONST GUID* InterfaceClassGUID,

__in_opt PCUNICODE_STRING ReferenceString

);

参数说明:

Device:设备对象创建句柄。

InterfaceClassGUID:设备接口 GUID。

ReferenceString: 描述设备接口的参考字符串。若没有,则设置 NULL 。

下面是 KMDF 范例中一个基本的 DeviceAdd 例程。

#ifdef ALLOC_PRAGMA

#pragma alloc_text(PAGE, CharSample_EvtDeviceAdd)

#endif

NTSTATUS

CharSample_EvtDeviceAdd(

    IN WDFDRIVER      Driver,

    IN PWDFDEVICE_INIT DeviceInit

    )

{

    NTSTATUS status;

    WDFDEVICE device;

    WDF_IO_QUEUE_CONFIG ioQueueConfig;

//例程的首句PAGED_CODE,表示该例程的代码占用分页内存。

//只能在PASSIVE_LEVEL中断级别调用该例程,否则会蓝屏。

//如不说明,则占用系统的非分页内存,要珍惜使用。

    PAGED_CODE();

//创建设备,没有对象属性和设备对象环境变量结构

    status = WdfDeviceCreate(&DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &device);

    if (!NT_SUCCESS(status)) {

       return status;

    }

//初始化缺省队列配置,设置I/O请求分发处理方式为串行。

//对这个实例而言,选择串行或并行都可以,但不能选手工。

   WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&ioQueueConfig, WdfIoQueueDispatchSequential);

//设置EvtIoDeviceControl例程,处理应用程序的DeviceIoControl()函数调用

   ioQueueConfig.EvtIoDeviceControl  = CharSample_EvtIoDeviceControl;

//创建队列

    status = WdfIoQueueCreate(device, &ioQueueConfig, WDF_NO_OBJECT_ATTRIBUTES, NULL);

    if (!NT_SUCCESS(status)) {

       return status;

    }

//创建设备GUID接口

    status = WdfDeviceCreateDeviceInterface(device, (LPGUID) &CharSample_DEVINTERFACE_GUID, NULL);

    if (!NT_SUCCESS(status)) {

    }

    return status;

}

在 DeviceAdd 例程中,还有一些设置函数,应该掌握,如下所示。

(1) VOID

WdfDeviceInitSetExclusive(

__in PWDFDEVICE_INIT DeviceInit,

__in BOOLEAN IsExclusive

);

设置设备是否独占, IsExclusive 为 TRUE ,表示独占,只能是一个程序打开设备。

(2) VOID

WdfDeviceInitSetIoType(

__in PWDFDEVICE_INIT DeviceInit,

__in WDF_DEVICE_IO_TYPE IoType

);

设置 I/O 类型,有 WdfDeviceIoBuffered 和 WdfDeviceIoDirect 两种,与 Write 和 Read 读写时所访问的地址有关;默认值为 WdfDeviceIoBuffered。

注意:这些函数必须在调用 WdfDeviceCreate 之前调用;否则,系统会蓝屏,重新启动。

2.3 I/O 处理例程

I/O 处理例程处理应用程序与驱动程序之间的通信,包括 Create、Close、Cleanup、Read、Write 和 DeviceControl。

Create、Close、Cleanup例程的设置和 Read、Write 和 DeviceControl 例程的设置不同,后者是由队列来管理的。另外,Create 例程也可以设置由队

列来管理,但 Close 和 Cleanup 例程却不能。

对于 Read、Write 和 DeviceControl 的 I/O 请求,是由队列来管理的,可以是默认队列,也可以由自己创建的队列来管理;可以是一个,也可以由多

个队列来分别管理。这些请求可以串行处理,也可以并行处理

1. Create、Close和Cleanup例程

Create 例程处理应用程序的 CreateFile 函数调用, Close和Cleanup例程处理应用程序的 CloseHandle 函数调用。

设置 Create、Close和Cleanup例程的配置结构是 WDF_FILEOBJECT_CONFIG 。其定义如下:

typedef struct _WDF_FILEOBJECT_CONFIG {

    //

    // Size of this structure in bytes

    //

    ULONG Size;

    //

    // Event callback for create requests

    //

   PFN_WDF_DEVICE_FILE_CREATE  EvtDeviceFileCreate;

    //

    // Event callback for close requests

    //

    PFN_WDF_FILE_CLOSE   EvtFileClose;

    //

    // Event callback for cleanup requests

    //

    PFN_WDF_FILE_CLEANUP EvtFileCleanup;

    //

    // If WdfTrue, create/cleanup/close file object related requests will be

    //     sent down the stack.

    //

    // If WdfFalse, create/cleanup/close will be completed at this location in

    //     the device stack.

    //

    // If WdfDefault, behavior depends on device type

    //     FDO, PDO, Control:  use the WdfFalse behavior

    //     Filter:  use the WdfTrue behavior

    //

    WDF_TRI_STATE AutoForwardCleanupClose;

    //

    // Specify whether framework should create WDFFILEOBJECT and also

    // whether it can FsContexts fields in the WDM fileobject to store

    // WDFFILEOBJECT so that it can avoid table look up and improve perf.

    //

    WDF_FILEOBJECT_CLASS FileObjectClass;

} WDF_FILEOBJECT_CONFIG, *PWDF_FILEOBJECT_CONFIG;

EvtDeviceFileCreate: Create 例程。

EvtFileClose: Close 例程。

EvtFileCleanup: Cleanup 例程。

AutoForwardCleanupClose: 是否将 Create、Close和Cleanup 请求自动传递给下一层驱动程序, WdfFalse ,表示不传递,由驱动程序完成请求;WdfTrue,

 表示将请求传递给下一层驱动程序; WdfUseDefault ,则视功能驱动程序还是过滤驱动程序而定, 如是功能驱动程序,则不传递,

 若是过滤驱动程序,则传递。默认值为WdfUseDefault。

FileObjectClass:是否要求存储文件对象。有以下4种选择:

WdfFileObjectNotRequired:不要求

   WdfFileObjectWdfCanUseFsContext:要求,使用 IRP中的 FsContext 存放。

   WdfFileObjectWdfCanUseFsContext2:要求,使用 IRP中的 FsContext2 存放。

   WdfFileObjectWdfCannotUseFsContexts:要求,但不能使用 FsContext 或 FsContext2 存放, 由 KMDF 框架内部存放。此为默认值。

配置结构初始化函数如下:

VOID 

WDF_FILEOBJECT_CONFIG_INIT(

__out PWDF_FILEOBJECT_CONFIG FileEventCallbacks,

__in_opt PFN_WDF_DEVICE_FILE_CREATE EvtDeviceFileCreate,

__in_opt PFN_WDF_FILE_CLOSE EvtFileClose,

__in_opt PFN_WDF_FILE_CLEANUP EvtFileCleanup

)

设置函数如下:

VOID 

WdfDeviceInitSetFileObjectConfig {

__in PWDFDEVICE_INIT DeviceInit,

__in PWDF_FILEOBJECT_CONFIG FileObjectConfig,

__in_opt PWDF_OBJECT_ATTRIBUTES FileObjectAttributes

};

在 DeviceAdd 例程中设置 Create、Close和Cleanup例程, 如 EventSample 范例中所示。

//设置Create、Close处理例程

   WDF_FILEOBJECT_CONFIG_INIT(

       &fileConfig,

       EventSample_EvtDeviceFileCreate,

       EventSample_EvtFileClose,

       WDF_NO_EVENT_CALLBACK

       );

   WdfDeviceInitSetFileObjectConfig(DeviceInit,

                               &fileConfig,

                               WDF_NO_OBJECT_ATTRIBUTES);

例程如下:

VOID

EventSample_EvtDeviceFileCreate(

    IN WDFDEVICE     Device,

    IN WDFREQUEST    Request,

    IN WDFFILEOBJECT FileObject

    )

{ //无操作,仅仅演示FileCreate编程处理

    NTSTATUS status = STATUS_SUCCESS;

   WdfRequestComplete(Request, status);

    return;

}

VOID

EventSample_EvtFileClose(

    IN WDFFILEOBJECT FileObject

    )

{ //当应用程序退出时,应释放内核事件

    WDFDEVICE device;

PQUEUE_CONTEXT pQueueContext;

    device = WdfFileObjectGetDevice(FileObject);

    pQueueContext = GetQueueContext(WdfDeviceGetDefaultQueue(device));

    if( pQueueContext->Event != NULL ) {

//释放内核事件,与ObReferenceObjectByHandle对应

ObDereferenceObject(pQueueContext->Event);

pQueueContext->Event = NULL;

    }

    return;

}

2. Read、Write 和 DeviceControl 例程

Read、Write 和 DeviceControl 例程分别处理应用程序的 ReadFile、WriteFile、DeviceIoControl 函数调用。

下面是 CharSample 实例中的 EvtIoDeviceControl 例程。

VOID

CharSample_EvtIoDeviceControl(

    IN WDFQUEUE   Queue,

    IN WDFREQUEST Request,

    IN size_t     OutputBufferLength,

    IN size_t     InputBufferLength,

    IN ULONG     IoControlCode

    )

{

    NTSTATUS  status;

    PVOID  buffer;

CHAR  n,c[]="零一二三四五六七八九";

    PAGED_CODE();

    switch(IoControlCode) {

    case CharSample_IOCTL_800:

if (InputBufferLength  == 0 || OutputBufferLength < 2)

{ //检查输入、输出参数有效性

WdfRequestComplete(Request, STATUS_INVALID_PARAMETER);

}

else

{

//输入缓冲区地址可通过调用WdfRequestRetrieveInputBuffer函数获得

//输出缓冲区地址可通过调用WdfRequestRetrieveOutputBuffer函数获得

//获取输入缓冲区地址buffer

//要求1字节空间

status = WdfRequestRetrieveInputBuffer(Request, 1, &buffer, NULL);

if (!NT_SUCCESS(status)) {

WdfRequestComplete(Request, STATUS_UNSUCCESSFUL);

      break;

}

//这里buffer表示输入缓冲区地址

//输入n=应用程序传给驱动程序的数字ASCII码

n = *(CHAR *)buffer;

if ((n>='0') && (n<='9'))

{ //若为数字,则处理

n-='0'; //n=数字(0-9)

//获取输出缓冲区地址buffer

status = WdfRequestRetrieveOutputBuffer(Request, 2, &buffer, NULL);

if (!NT_SUCCESS(status)) {

WdfRequestComplete(Request, STATUS_UNSUCCESSFUL);

break;

}

//这里buffer表示输出缓冲区地址

//输出:从中文数组c[]中取出对应的数字的中文码,拷贝到输出缓冲区

strncpy((PCHAR)buffer,&c[n*2],2);

//完成I/O请求,驱动程序传给应用程序的数据长度为2字节(一个中文)

WdfRequestCompleteWithInformation(Request, STATUS_SUCCESS, 2);

}

else //否则返回无效参数

WdfRequestComplete(Request, STATUS_INVALID_PARAMETER);

}

       break;

    default :

       status = STATUS_INVALID_DEVICE_REQUEST;

WdfRequestCompleteWithInformation(Request, status, 0);

       break;

    }

    return;

}

4. 即插即用和电源管理例程

对于大多数过滤和软件功能驱动程序,WDF 的默认配置已经可以满足他们对于即插即用和电源管理的处理,这些程序不需要关于即插即用和电源管理

的编程处理。

二、基本对象

函数运行的中断级别,如无说明,表示其 IRQL <= DISPATCH_LEVEL。

1. WDFREQUEST 对象

Windows 操作系统使用 IRP 与内核模式驱动程序通信, IRP 在 Windows 驱动程序中占有非常重要的作用。

IRP 是 DDK 定义的一个数据结构。KMDF 对 IRP 进行了封装,推出了一个 WDFREQUEST 对象,提供了许多有关

IRP 操作的函数。本书中,将 WDFREQUEST 称为 I/O 请求,他出现在处理程序(或其他驱动程序)调用的 Create、Read、

Write 和 DeviceControl 例程中。

1.1 WDFREQUEST 对象函数

(1)

VOID 

WdfRequestComplete(

__in WDFREQUEST Request,

    __in NTSTATUS Status

);

完成I/O请求。

Request: 等待完成的 I/O 请求。

Status: 完成的状态标示,如 STATUS_SUCCESS(成功)、STATUS_CANCELLED(取消)、STATUS_UNSUCCESSFUL(失败)等。

(2)

VOID

WdfRequestCompleteWithInformation(

__in WDFREQUEST Request,

    __in NTSTATUS Status,

    __in  ULONG_PTR Information

);

完成 I/O 请求。

Information: I/O 请求完成时,成功传输的字节数。

在通常情况下,如果  I/O 请求完成失败(即完成的结果是某种错误状态),则Information置0。如果成功地完成了一个

数据传输,则应该把 Information 设置成传输的字节数。

(3).

VOID 

WdfRequestSetInformation(

__in WDFREQUEST Request,

    __in ULONG_PTR Information

);

设置I/O请求完成的传输字节数。

(4).

NTSTATUS 

WdfRequestRetrieveInputBuffer(

__in WDFREQUEST Request,

    __in size_t MinimumRequiredLength,

   __deref_out_bcount(*Length) PVOID* Buffer,

    __out_opt size_t* Length

);

获取 I/O 请求输入缓冲区地址,形式为 PVOID。

MinimumRequiredLength: 要求的最小字节数。

Buffer: 存放获取的输入缓冲区地址指针。

Length: 存放获取的输入缓冲区字节数。可以为NULL。

(5).

NTSTATUS

WdfRequestRetrieveInputMemory(

__in WDFREQUEST Request,

    __out  WDFMEMORY* Memory

);

获取 I/O 请求输入缓冲区地址,形式为 WDFMEMORY。

Memory: 存放获取的输入缓冲区地址指针。

(6).

NTSTATUS

WdfRequestRetrieveInputWdmMdl(

__in WDFREQUEST Request,

__deref_out PMDL* Mdl

);

获取 I/O 请求输入缓冲区地址,形式为 Mdl。

Mdl: 存放获取的输入缓冲区地址指针。

(7).

NTSTATUS

WdfRequestRetrieveOutputBuffer(

__in WDFREQUEST Request,

    __in size_t MinimumRequiredSize,

    __deref_out_bcount(*Length)  PVOID* Buffer,

    __out_opt  size_t* Length

);

获取 I/O 请求输入缓冲区地址,形式为 PVOID。

MinimumRequiredLength: 要求的最小字节数。

Buffer: 存放获取的输入缓冲区地址指针。

Length: 存放获取的输入缓冲区字节数。可以为NULL。

(8).

NTSTATUS

WdfRequestRetrieveOutputMemory(

__in WDFREQUEST Request,

    __out  WDFMEMORY* Memory

);

获取 I/O 请求输入缓冲区地址,形式为 WDFMEMORY。

Memory: 存放获取的输入缓冲区地址指针。

(9).

NTSTATUS

WdfRequestRetrieveOutputWdmMdl(

__in

    WDFREQUEST Request,

    __deref_out

    PMDL* Mdl

);

获取 I/O 请求输入缓冲区地址,形式为 Mdl。

Mdl: 存放获取的输入缓冲区地址指针。

(10).

WDFQUEUE

WdfRequestGetIoQueue(

__in

    WDFREQUEST Request

);

返回 I/O 请求队列对象。

(11).

WDFFILEOBJECT

WdfRequestGetFileObject(

__in

    WDFREQUEST Request

);

返回 I/O 请求文件对象。

(12).

WDFFILEOBJECT

WdfRequestGetParameters(

__in

    WDFREQUEST Request,

__out

    PWDF_REQUEST_PARAMETERS Parameters

);

返回 I/O 请求参数。

(13).

KPROCESSOR_MODE

WdfRequestGetRequestorMode(

__in

    WDFREQUEST Request

);

返回 I/O 请求者的模式: KernelMode或UserMode。

(14).

VOID

WdfRequestMarkCancelable(

__in

    WDFREQUEST Request,

__in

    PFN_WDF_REQUEST_CANCEL EvtRequestCancel

);

设置 I/O 请求取消例程。

EvtRequestCancel: 取消例程地址。

(15).

NTSTATUS

WdfRequestUnMarkCancelable(

__in

    WDFREQUEST Request

);

取消取消例程。

(16).

BOOLEAN

WdfRequestIsCanceled(

__in

    WDFREQUEST Request

);

判断 I/O 请求是否已取消。若已取消,则返回TRUE。

(17).

VOID 

WdfRequestFormatRequestUsingCurrentType(

__in

    WDFREQUEST Request

);

将 I/O 请求发送到下层设备前,做相应的数据化格式处理。

(18).

VOID 

WdfRequestSetCompletionRoutine(

__in

    WDFREQUEST Request,

    __in_opt

   PFN_WDF_REQUEST_COMPLETION_ROUTINE CompletionRoutine,

    __in_opt __drv_aliasesMem

    WDFCONTEXT CompletionContext

);

设置 I/O 请求完成例程。

CompletionRoutine: 完成例程地址。

CompletionContext: 传递给完成例程的无类型定义的数据信息指针。

WDF_NO_CONTEXT表示没有数据。

(19).

BOOLEAN

WdfRequestSend(

__in

    WDFREQUEST Request,

    __in

    WDFIOTARGET Target,

    __in_opt

   PWDF_REQUEST_SEND_OPTIONS Options

);

发送 I/O 请求完成状态。

(20).

NTSTATUS 

WdfRequestGetStatus(

__in

    WDFREQUEST Request

);

返回 I/O 请求完成状态。

(21).

ULONG_PTR

WdfRequestGetInformation(

__in

    WDFREQUEST Request

);

返回 I/O 请求完成的传输的字节数。

(22).

NTSTATUS

WdfRequestCreate(

__in_opt

    PWDF_OBJECT_ATTRIBUTES RequestAttributes,

    __in_opt

    WDFIOTARGET IoTarget,

    __out

    WDFREQUEST* Request

);

创建 I/O 请求。

(23).

NTSTATUS

WdfRequestForwardToIoQueue(

__in

    WDFREQUEST Request,

    __in

    WDFQUEUE DestinationQueue

);

将 I/O 请求发送到其他队列。

(24)

BOOLEAN

WdfRequestCancelSentRequest(

__in

    WDFREQUEST Request

);

取消已发送的 I/O 请求。

(25).

NTSTATUS

WdfRequestStopAcknowledge(

__in

    WDFREQUEST Request,

    __in

    BOOLEAN Requeue

);

通知框架已停止处理此 I/O 请求。

Request: TRUE 表示重新入队列。

1.2 I/O 请求基本操作

1.2.1 完成 I/O 请求

I/O 请求的完成指的是实现该 I/O 请求所要求的操作,返回结果可能成功也可能失败,但都表示已完成 I/O 请求。

WdfRequestComplete( Request,STATUS_INVALID_PARAMETER);

WdfRequestCompleteWithInformation(Request, STATUS_SUCCESS,2);

对于 WdfRequestComplete() 函数,如果之前没有设置 Information,则Information为0,通常用 WdfRequestComplete() 函数来

完成一个失败的操作。

1.2.2 取消 I/O 请求

当一个 I/O 请求不能立即被完成时,它就存在被取消的可能。 I/O 请求的取消分两种情况:一是已出队列的 I/O 请求的取消;

二是队列中的 I/O 请求的取消。

下面介绍已出队列的 I/O 请求的取消。

(1)可以在设备环境变量结构中定义一个请求对象存放 I/O 请求。

typedef struct _DEVICE_CONTEXT{

WDFREQUEST CurrentRequest;//存放待完成的 I/O请求

}DEVICE_CONTEXT,*PDEVICE_CONTEXT;

(2)I/O请求由其他例程来完成时,应将该 I/O请求保存起来,并设置取消例程。

pDeviceContext=GetDeviceContext(device);

//将该 I/O请求保存起来

pDeviceContext->CurrentRequest = Request;

//设置该 I/O请求可取消,取消例程为 CancelSample_EvtRequestCancel

WdfRequestMarkCancelable(Request, CancelSample_EvtRequestCancel);

(3).当特定事件发生时,在 I/O请求完成前,应取消取消例程。

pDeviceContext = GetDeviceContext(device);

request = pDeviceContext->CurrentRequest;

if(request!=NULL){

pDeviceContext->CurrentRequest = NULL;

//取消该 I/O请求的取消例程

status =  WdfRequestUnMarkCancelable(request);

if(status != STATUS_CANCELLED){

WdfRequestCompleteWithInformation(request, STATUS_SUCCESS, 2);

}

}

1.2.3 向下传递 I/O请求

WDM 驱动程序使用分层设备对象结构目的,就是使 I/O 请求能方便的从一层驱动程序传递到下一层驱动程序。如果驱动程序不能处理一个 I/O请求,就应当向下传递,由下一层驱动程序来完成。

向下传递的函数是:

BOOLEAN 

WdfRequestSend(

__in

    WDFREQUEST Request,

    __in

    WDFIOTARGET Target,

    __in_opt

   PWDF_REQUEST_SEND_OPTIONS Options

);

Target: 目标层对象。如果是普通 I/O目标,则由 WdfDeviceGetIoTarget 获取目标层对象;如果是 USB 目标,则由 WdfUsbDeviceGetIoTarget或 WdfUsbTargetPipeGetIoTarget 获取目标层对象。

Options: 发送方式, WDF_NOSEND_OPTIONS 表示没有选择方式。

WDF_REQUEST_SEND_OPTIONS结构如下:

typedef struct _WDF_REQUEST_SEND_OPTIONS {

    //

    // Size of the structure in bytes

    //

    ULONG Size;

    //

    // Bit field combination of values from the WDF_REQUEST_SEND_OPTIONS_FLAGS

    // enumeration

    //

    ULONG Flags;

    //

    // Valid when WDF_REQUEST_SEND_OPTION_TIMEOUT is set

    //

    LONGLONG Timeout;

} WDF_REQUEST_SEND_OPTIONS, *PWDF_REQUEST_SEND_OPTIONS;

Flags: 操作表示。含义如下:

WDF_REQUEST_SEND_OPTION_TIMEOUT 定时参数有效

WDF_REQUEST_SEND_OPTION_SYNCHRONOUS 同步操作

   WDF_REQUEST_SEND_OPTION_IGNORE_TARGET_STATE 忽略目标层状态

   WDF_REQUEST_SEND_OPTION_SEND_AND_FORGET 没有完成例程

Timeout: 定时参数。正数表示绝对时间;负责表示相对时间,从当前时间算起。单位是 0.1us ,一般采用的是相对时间。

操作标示初始化函数如下:

VOID 

WDF_REQUEST_SEND_OPTIONS_SET_TIMEOUT(

__out PWDF_REQUEST_SEND_OPTIONS Options,

    __in LONGLONG Timeout

);

KMDF 提供的相对时间的设置函数有秒、毫秒、微妙,如下:

LONGLONG WDF_REL_TIMEOUT_IN_SEC(__in ULONGLONG Time);

LONGLONG WDF_REL_TIMEOUT_IN_MS(__in ULONGLONG Time);

LONGLONG WDF_REL_TIMEOUT_IN_US(__in ULONGLONG Time);

当 I/O 请求由下一层驱动程序来完成时,驱动程序有两种选择:

(1)不设置完成例程

如果驱动不用关心 I/O 请求传递到下层驱动程序之后的事情,则编程如下:

WdfRequestFormatRequestUsingCurrentType(Request);

//不设置完成例程,发送操作标识设置如下

WDF_REQUEST_SEND_OPTIONS_INIT(

&options,

WDF_REQUEST_SEND_OPTIONS_AND_FORGET

);

ret = WdfRequestSend(Request,

WdfDeviceGetIoTarget(WdfIoQueueGetDevice(Queue)),

&options

);

if(!ret){

status = WdfRequestGetStatus(Request);

WdfRequestComplete(Request,status);

}

(2)设置完成例程

如果驱动程序关心 I/O 请求传递到下层驱动层序之后的事情,则应该设置一个完成例程。当下一层驱动程序完成该 I/O 请求时,系统将调用

完成例程,在完成例程中完成 I/O请求。

WdfRequestFormatRequestUsingCurrentType(Request);

//设置 I/O 请求完成例程。

WdfRequestSetCompletionRoutine(

Request,

FilterSample_EvtRequestIoctlCompletionRoutine,

NULL

);

//将 I/O 请求发送到下层设备

ret = WdfRequestSend(

Request,

WdfDeviceGetIoTarget(WdfIoQueueGetDevice(Queue)),

WDF_NO_SEND_OPTIONS

);

if(!ret){

status = WdfRequestGetStatus(Request);

WdfRequestComplete(Request,status);

}

VOID FilterSample_EvtRequestIoctlCompletionRoutine(

IN WDFREQUEST Request,

IN WDFIOTARGET Target,

PWDF_REQUEST_COMPLETION_PARAMETERS CompletionParams,

IN WDFCONTEXT Context)

{

......

}

1.2.4 设备队列 I/O 请求环境变量结构属性设置

这里指的是由系统所创建的 I/O 请求,如 Read、Write和DeviceControl 例程的 I/O请求。如 FilterSample 范例中所示:

//初始化 I/O 请求对象环境变量结构

WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&requestAttributes,REQUEST_CONTEXT);

//设置 I/O 请求对象属性

WdfDeviceInitSetRequestAttributes(DeviceInit, &requestAttributes);

2.WDFQUEUE 对象

在 KMDF 驱动程序中,队列具有非常重要的地位。如果要处理应用程序的 I/O 请求,就需要队列。对于应用程序  Read、Write和DeviceControl 

的 I/O请求,均是由队列来管理的。框架有一个默认队列。也可以自己创建队列。如果分别对这些 I/O 请求进行排队,则可以由多个队列来分别管理。

这些例程调用可以串行处理,也可以并行处理。

队列的配置结构是 WDF_IO_QUEUE_CONFIG,其定义如下:

typedef struct _WDF_IO_QUEUE_CONFIG {

    ULONG                                Size;

   WDF_IO_QUEUE_DISPATCH_TYPE               DispatchType;

    WDF_TRI_STATE                          PowerManaged;

    BOOLEAN                               AllowZeroLengthRequests;

    BOOLEAN                               DefaultQueue;

   PFN_WDF_IO_QUEUE_IO_DEFAULT              EvtIoDefault;

    PFN_WDF_IO_QUEUE_IO_READ                 EvtIoRead;

   PFN_WDF_IO_QUEUE_IO_WRITE                EvtIoWrite;

   PFN_WDF_IO_QUEUE_IO_DEVICE_CONTROL        EvtIoDeviceControl;

   PFN_WDF_IO_QUEUE_IO_INTERNAL_DEVICE_CONTROL EvtIoInternalDeviceControl;

    PFN_WDF_IO_QUEUE_IO_STOP                 EvtIoStop;

   PFN_WDF_IO_QUEUE_IO_RESUME               EvtIoResume;

   PFN_WDF_IO_QUEUE_IO_CANCELED_ON_QUEUE      EvtIoCanceledOnQueue;

    union {

       struct {

           ULONG NumberOfPresentedRequests;

       } Parallel;

    } Settings;

} WDF_IO_QUEUE_CONFIG, *PWDF_IO_QUEUE_CONFIG;

DispatchType: I/O 请求分发处理方式。WdfIoQueueDispatchSequential 表示一次只能有一个 I/O请求在处理;

WdfIoQueueDispatchParallel 表示多个 I/O 请求可并行处理

WdfIoQueueDispatchManual 表示这是一个手工队列,它不能用于 I/O 请求例程的处理,只能作为一个备用队列。

PowerManaged: 是否允许框架对队列实施电源管理;WdfUseDefault 表示如果没有调用 WdfFdoInitSetFilter 函数,则框架对队

 列实施电源管理;WdfFalse表示驱动程序对队列实施电源管理;WdfTrue 表示由框架对队列实施电源管理。默认值

 是 WdfUseDefault。

AllowZeroLengthRequests: TRUE 表示允许读/写例程的数据传输的字节数为0。

DefaultQueue: TRUE 表示选择默认队列;FALSE 表示创建一个新的队列。

EvtIoDefault: 其他没有注册的操作处理例程,可以处理 Create 请求。

EvtIoRead: 读操作的处理例程。

EvtIoWrite: 写操作的处理例程。

EvtIoDeviceControl: 设备 I/O调用操作的处理例程。

EvtIoInternalDeviceControl: 内部设备 I/O 调用操作的处理例程。

EvtIoStop: 当设备电源脱离 D0 工作状态时的调用例程。

EvtIoResume: 当设备电源进入 D0 工作状态时的调用例程。

EvtIoCanceledOnQueue: 队列中的 I/O请求取消例程。

队列配置初始化函数有两个,分别如下:

(1)

VOID

WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE{

__out PWDF_IO_QUEUE_CONFIG     Config,

    __in WDF_IO_QUEUE_DISPATCH_TYPE DispatchType

};

初始化默认队列设置

(2)

VOID 

WDF_IO_QUEUE_CONFIG_INIT(

__out PWDF_IO_QUEUE_CONFIG     Config,

    __in WDF_IO_QUEUE_DISPATCH_TYPE DispatchType

);

初始化非默认队列配置。

如果非默认队列用于分发 I/O 请求,则必须指定要分发的 I/O 请求类型,函数如下:

NTSTATUS

WdfDeviceConfigureRequestDispatching(

__in

    WDFDEVICE Device,

    __in

    WDFQUEUE Queue,

    __in

   __drv_strictTypeMatch(__drv_typeCond)

    WDF_REQUEST_TYPE RequestType

);

RequestType: I/O 请求类型,包括  WdfRequestTypeCreate  、 WdfRequestTypeRead 、 WdfRequestTypeWrite 、 WdfRequestTypeDeviceControl 和WdfRequestTypeDeviceControlInternal。

2.1 WDFQUEUE 对象函数

(1).

NTSTATUS

WdfIoQueueCreate(

__in

    WDFDEVICE Device,

    __in

    PWDF_IO_QUEUE_CONFIG Config,

    __in_opt

    PWDF_OBJECT_ATTRIBUTES QueueAttributes,

    __out_opt

    WDFQUEUE* Queue

);

创建队列对象。

(2)

WDFDEVICE

WdfIoQueueGetDevice( IN WDFQUEUE Queue);

返回队列的设备对象。

(3)

VOID

WdfIoQueueStart(

__in

    WDFQUEUE Queue

);

(4)

VOID

WdfIoQueueStop(

__in

    WDFQUEUE Queue,

    __drv_when(Context != 0, __in)

    __drv_when(Context == 0, __in_opt)

    PFN_WDF_IO_QUEUE_STATE StopComplete,

    __drv_when(StopComplete != 0, __in)

    __drv_when(StopComplete == 0, __in_opt)

    WDFCONTEXT Context

);

停止分发 I/O请求,但仍接收新的 I/O请求。异步操作函数。

StopComplete: EvtIoQueueState 回调函数历程地址,所有已分发的 I/O 请求处理完成或被取消时,系统将调用该例程。可以为NULL。

Context: 传递给 EvtIoQueueState 回调例程的无类型定义的数据结构指针。可以为 NULL 。

(5)

VOID 

WdfIoQueueStopSynchronously(

__in

    WDFQUEUE Queue 

);

停止分发 I/O请求,但仍接收新的 I/O请求。直到所有已分发的 I/O请求处理完毕或被取消,才返回。

(6)

VOID 

WdfIoQueueDrain(

__in

    WDFQUEUE Queue,

    __drv_when(Context != 0, __in)

    __drv_when(Context == 0, __in_opt)

    PFN_WDF_IO_QUEUE_STATE DrainComplete,

    __drv_when(DrainComplete != 0, __in)

    __drv_when(DrainComplete == 0, __in_opt)

    WDFCONTEXT Context

);

停止接收新的 I/O请求,但队列中未处理的 I/O请求仍将分发处理。异步操作函数。

DrainComplete: EvtIoQueueState 回调例程地址,所有已分发的 I/O 请求处理完成或被取消时,系统将调用该例程。可以为NULL。

(7)

VOID 

WdfIoQueueDrainSynchronously(

__in

    WDFQUEUE Queue

);

停止接收新的 I/O 请求,但队列中未处理的 I/O 请求仍将分发处理。直到所有 I/O 请求处理完毕或被取消,才能返回。

(8)

VOID

WdfIoQueuePurge(

__in

    WDFQUEUE Queue,

    __drv_when(Context != 0, __in)

    __drv_when(Context == 0, __in_opt)

    PFN_WDF_IO_QUEUE_STATE PurgeComplete,

    __drv_when(PurgeComplete != 0, __in)

    __drv_when(PurgeComplete == 0, __in_opt)

    WDFCONTEXT Context

);

停止接收新的 I/O请求,并取消队列中待处理的 I/O请求。异步操作函数。

PurgeComplete: EvtIoQueueState 回调例程地址,所有已分发的 I/O 请求处理完成或被取消时,系统将调用该例程。可以为NULL。

(9)

VOID 

WdfIoQueuePurgeSynchronously(

__in

    WDFQUEUE Queue

);

停止接收新的 I/O请求,并取消队列中待处理的 I/O请求。直到多有已分发的 I/O请求处理完毕或被取消,才返回。

(10)

NTSTATUS

WdfIoQueueRetrieveNextRequest(

__in

    WDFQUEUE Queue,

    __out

    WDFREQUEST* OutRequest

);

从队列中取出下一个要处理的 I/O请求。

2.2 队列编程

2.2.1 队列初始化设置

用于分发例程的队列,即管理 I/O处理例程请求的队列,其队列可以是默认队列,也可以是非默认队列;处理方式可以是串行,也可以是并行,但不能是手工方式。

(1)默认队列

//初始化默认队列配置,设置 I/O请求分发方式为串行

WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&ioQueueConfig,WdfIoQueueDispatchSequential);

//设置EvtIoDeviceControl回调例程

ioQueueConfig.EvtIoDeviceControl = XXXSample_EvtIoDeviceControl;

//创建队列

status = WdfIoQueueCreate(device, &ioQueueConfig, WDF_OBJECT_ATTRIBUTES, NULL);

(2)非默认队列

//初始化非默认队列配置,设置 I/O 请求分发方式为串行

WDF_IO_QUEUE_CONFIG_INIT(&ioQueueConfig, WdfIoQueueDispatchSequential);

ioQueueConfig.EvtIoDeviceControl =  XXXSample_EvtIoDeviceControl;

status = WdfIoQueueCreate(device, &ioQueueConfig, WDF_OBJECT_ATTRIBUTES, &queue);

if(!NT_SUCCESS(status)){

return status;

}

//对于非默认队列,必须指定要分发的 I/O 请求类型

status = WdfDeviceConfigureRequestDispatching(device, queue, WdfRequestTypeDeviceControl);

2.2.2 队列中的 I/O 请求取消

对于取消仍在队列中驱动程序尚未处理的 I/O 请求,如果注册了队列中的 I/O请求取消例程,则框架调用其队列取消例程;如果未注册,则框架直接将其从队列中取消。

3 WDFTIMER 对象

定时器分为一次性定时器和周期性定时器:一次行定时器是启动一次,当定时时间到后,定时器回调例程执行一次;周期性定时器只启动一次,当定时时间到后,定时器

回调例程执行一次;当定时周期到后,定时时间又重新计时,这样定时器回调例程周期执行。

定时器对象的配置结构是 WDF_TIMER_CONFIG ,其定义如下:

typedef struct _WDF_TIMER_CONFIG{

ULONG Size;

    PFN_WDF_TIMER EvtTimerFunc;

    ULONG Period;

    //

    // If this is TRUE, the Timer will automatically serialize

    // with the event callback handlers of its Parent Object.

    //

    // Parent Object's callback constraints should be compatible

    // with the Timer DPC (DISPATCH_LEVEL), or the request will fail.

    //

    BOOLEAN AutomaticSerialization;

    //

    // Optional tolerance for the timer in milliseconds.

    //

    ULONG TolerableDelay;

} WDF_TIMER_CONFIG, *PWDF_TIMER_CONFIG;

EvtTimerFunc: 定时器回调例程

Period: 循环调用周期,单位是毫秒。

AutomaticSerialization: 若为 TRUE,则回调例程的执行与父对象的其他例程同步,即串行执行。默认值为TRUE。

配置结构初始化函数有两个,如下所示:

(1).

VOID 

WDF_TIMER_CONFIG_INIT(

__out PWDF_TIMER_CONFIG Config,

    __in  PFN_WDF_TIMER    EvtTimerFunc

);

初始化一次性定时器 WDF_TIMER_CONFIG 结构。

(2).

VOID 

WDF_TIMER_CONFIG_INIT_PERIODIC(

__out PWDF_TIMER_CONFIG Config,

    __in  PFN_WDF_TIMER    EvtTimerFunc,

    __in  LONG           Period

);

初始化周期性定时器 WDF_TIMER_CONFIG 结构。

相应函数:

(1)

NTSTATUS

WdfTimerCreate(

__in

    PWDF_TIMER_CONFIG Config,

    __in

    PWDF_OBJECT_ATTRIBUTES Attributes,

    __out

    WDFTIMER* Timer

);

创建定时器对象。

(2)

BOOLEAN

WdfTimerStart(

__in

    WDFTIMER Timer,

    __in

    LONGLONG DueTime

);

启动定时器运行。

DueTime: 定时器参数。正数表示绝对时间;负数表示相对时间,从当前时间算起。单位是 0.1us

(3)

BOOLEAN

WdfTimerStop(

__in

    WDFTIMER Timer,

    __in

    BOOLEAN Wait

);

停止定时器运行。

Wait: 若为 TRUE,则等待系统所有排队中的延迟过程调用(包括定时器回调例程)已被执行完毕,PASSIVE_LEVEL 中断级别调用;若为FALSE,则立即返回,IRQL <= DISPATCH_LEVEL

中断级别调用。

(4)

WDFOBJECT

WdfTimerGetParentObject(

__in

    WDFTIMER Timer

);

返回定时器父对象。

4. WDFDPC 对象

在中断服务例程 (ISR) 中通常需要执行这样一些操作:这些操作不适合在 ISR 中执行,或者考虑到在 ISR 中执行会对操作系统性能造成影响。为了解决这个问题,Windows

提供了延迟过程调用机制(DPC)。DPC是一个通用机制,但通常都用在中断处理中。当 ISR 正在执行时,由于处理器运行在设备 IRQL 级别上,因此不能调用大部分的系统服务。通过

请求一个DPC,在中断结束后,内核在 DISPATCH_LEVEL 级别上调用这个 DPC 例程,然后就可以在 DPC 中调用这些系统服务了。再多处理器系统中,系统可以立刻在另一个处理器上

运行DPC。

延迟过程调用对象的配置结构是 WDF_DPC_EONFIG, 其定义如下:

typedef struct _WDF_DPC_CONFIG{

ULONG      Size;

    PFN_WDF_DPC EvtDpcFunc;

    //

    // If this is TRUE, the DPC will automatically serialize

    // with the event callback handlers of its Parent Object.

    //

    // Parent Object's callback constraints should be compatible

    // with the DPC (DISPATCH_LEVEL), or the request will fail.

    //

    BOOLEAN     AutomaticSerialization;

}WDF_DPC_CONFIG, *PWDF_DPC_CONFIG;

EvtDpcFunc: 延迟过程调用例程。

AutomaticSerialization: 若为 TRUE , 则回调例程的执行与父对象的其他例程同步。默认值为 TRUE。

配置结构初始化函数如下:

VOID 

WDF_DPC_CONFIG_INIT(

__out PWDF_DPC_CONFIG Config,

    __in  PFN_WDF_DPC    EvtDpcFunc

);

(1)

NTSTATUS

WdfDpcCreate(

__in

    PWDF_DPC_CONFIG Config,

    __in

    PWDF_OBJECT_ATTRIBUTES Attributes,

    __out

    WDFDPC* Dpc

);

创建工作项对象。

(2)

BOOLEAN

WdfDpcEnqueue(__in WDFDPC Dpc);

排队一个延迟过程调用,任意 IRQL 中断级别调用。

(3)

BOOLEAN

WdfDpcCancel(

__in

    WDFDPC Dpc,

    __in

    BOOLEAN Wait

);

取消延迟过程调用。

Wait: 若为 TRUE,则等待延迟过程调用已被取消或执行完毕,PASSIVE_LEVEL 中断级别调用;若为FALSE,则立即返回,任意 IRQL 中断级别调用。

(4)

WDFOBJECT

WdfDpcGetParentObject(

__in

    WDFDPC Dpc

);

返回延迟过程调用父对象,任意 IRQL 中断级别调用。

5.WDFWORKITEM 对象

许多函数必须在 PASSIVE_LEVEL 中断级别上执行,但有时候,所在的例程的运行级别大于 PASSIVE_LEVEL ,怎样解决这个问题?降低 IRQL 显然是不行的,不过如果运行在 IRQL <= DISPATCH_LEVEL

级别上,在有操作系统所拥有的一个工作项线程的环境中运行。

工作项对象的配置结构是 WDF_WORKITEM_CONFIG,其定义如下:

typedef struct _WDF_WORKITEM_CONFIG{

ULONG Size;

    PFN_WDF_WORKITEM EvtWorkItemFunc;

    //

    // If this is TRUE, the workitem will automatically serialize

    // with the event callback handlers of its Parent Object.

    //

    // Parent Object's callback constraints should be compatible

    // with the work item (PASSIVE_LEVEL), or the request will fail.

    //

    BOOLEAN AutomaticSerialization;

} WDF_WORKITEM_CONFIG, *PWDF_WORKITEM_CONFIG;

EvtWorkItemFunc: 工作项回调例程。

AutomaticSerialization: 若为 TRUE ,则回调例程的执行与父对象的其他例程同步。默认值为 TRUE。

配置结构初始化函数如下:

VOID

WDF_WORKITEM_CONFIG_INIT(

__out PWDF_WORKITEM_CONFIG Config,

    __in PFN_WDF_WORKITEM     EvtWorkItemFunc

);

(1)

NTSTATUS

WdfWorkItemCreate(

__in

    PWDF_WORKITEM_CONFIG Config,

    __in

    PWDF_OBJECT_ATTRIBUTES Attributes,

    __out

    WDFWORKITEM* WorkItem

);

创建工作项对象。

(2)

VOID 

WdfWorkItemEnqueue(__in WDFWORKITEM WorkItem);

排队一个工作项。

(3)

VOID 

WdfWorkItemFlush(

__in

    WDFWORKITEM WorkItem

);

等待工作项结束, PASSIVE_LEVEL 中断级别调用。

(4)

WDFOBJECT

WdfWorkItemGetParentObject(

__in

    WDFWORKITEM WorkItem

);

返回工作项父对象。

6. WDFMEMORY 对象

储存对象提供安全的数据业务,再拷贝数据之前,它会检查储存对象的缓存区再接空间,避免数据溢出。

(1)

NTSTATUS

WdfMemoryCopyFromBuffer(

__in WDFMEMORY DestinationMemory,

    __in

    size_t DestinationOffset,

    __in

    PVOID Buffer,

    __in

   __drv_when(NumBytesToCopyFrom == 0, __drv_reportError(NumBytesToCopyFrom cannot be zero))

    size_t NumBytesToCopyFrom

);

将其他缓存区的数据拷贝到储存对象中。如果储存对象是在分页内存中分配的,则运行级别是 IRQL <= APC_LEVEL ,如果是在非分页内存中分配的,则运行级别是任意 IRQL。

DestinationMemory: 目标储存对象。

DestinationOffset: 目标缓存区的歧视偏移地址。

Buffer: 源缓存区地址指针。

NumBytesToCopyFrom: 要拷贝数据的字节数。

(2)

NTSTATUS

WdfMemoryCopyToBuffer(

__in

    WDFMEMORY SourceMemory,

    __in

    size_t SourceOffset,

    __out_bcount( NumBytesToCopyTo )

    PVOID Buffer,

    __in

   __drv_when(NumBytesToCopyTo == 0, __drv_reportError(NumBytesToCopyTo cannot be zero))

    size_t NumBytesToCopyTo

);

将储存对象的数据拷贝到其他缓存区。

(3)

PVOID

WdfMemoryGetBuffer(

__in

    WDFMEMORY Memory,

    __out_opt

    size_t* BufferSize

);

返回储存对象的缓存区地址指针,运行级别是任意 IRQL 。

BufferSize: 返回的缓存区字节长度。可以为 NULL 。

(4)

NTSTATUS

WdfMemoryCreate(

__in_opt

    PWDF_OBJECT_ATTRIBUTES Attributes,

    __in

   __drv_strictTypeMatch(__drv_typeCond)

    POOL_TYPE PoolType,

    __in_opt

    ULONG PoolTag,

    __in

    __drv_when(BufferSize == 0, __drv_reportError(BufferSize cannot be zero))

    size_t BufferSize,

    __out

    WDFMEMORY* Memory,

    __out_opt

    PVOID* Buffer

);

创建储存对象。如果储存对象是在分页内存中分配的,则运行级别是 IRQL <= APC_LEVEL ,如果是在非分页内存中分配的,则运行级别是任意 IRQL <= DISPATCH_LEVEL 。

PoolType: 所分配内存区标识。PagedPool,分页内存;NonPagedPool,非分页内存。

PoolTag: Debuggers显示的内存标识,4字节。

BufferSize: 缓存区字节长度。

Memory: 储存对象创建句柄。

Buffer: 储存对象的缓存区地址指针。可以为 NULL 。

7. 数据同步访问

在数据同步访问方面,相对 WDM 而言, KMDF 显然做了许多改进。 KMDF 很少用到同步锁,同步访问数据的编程代码也相对较少。KMDF 提供了设备对象范围内和队列对象范围下的同步,基本可以实现

对设备环境变量和队列环境变量的保护性访问。不过,KMDF 仍然提供了两个锁对象。

7.1 WDFSPINLOCK 对象

自旋锁用于数据的保护性访问。一旦一个线程或 DPC 获取自旋锁,在同一时间试图获取同一个自旋锁的其他线程或 DPC 将被阻塞,直到自旋锁释放。要给每块需专一访问的数据分配一个自旋锁,在访问

数据之前先获取自旋锁,访问数据之后释放自旋锁,这样就可以实现对数据的专一访问。

通过获取自旋锁防止对数据的交叉访问,是通过两种途径实现的。首先,将获取自旋锁的线程的中断级别提升到 DISPATCH_LEVEL, 这对由单个处理器构成的系统来说已经足够了。其次,它自动测试和

设置内部的控制变量状态,如果处于设置状态,就循环检测,保证了多处理器的同步。

一个驱动程序不能长时间的持有一个自旋锁,这样会阻碍其他线程的执行。持有自旋锁的程序代码运行在 DISPATCH_LEVEL 中断级别上,因此不能访问分页代码或数据。

(1)

NTSTATUS

WdfSpinLockCreate(

__in_opt

    PWDF_OBJECT_ATTRIBUTES SpinLockAttributes,

    __out

    WDFSPINLOCK* SpinLock

);

创建自旋锁。

(2)

VOID 

WdfSpinLockAcquire(

__in

    __drv_savesIRQL

   __drv_neverHold(SpinLockObj)

   __drv_acquiresResource(SpinLockObj)

    WDFSPINLOCK SpinLock

);

获取自旋锁。

(3)

VOID 

WdfSpinLockRelease(

__in

    __drv_restoresIRQL

   __drv_mustHold(SpinLockObj)

   __drv_releasesResource(SpinLockObj)

    WDFSPINLOCK SpinLock

);

释放自旋锁。

7.2 WDFWAITLOCK 对象

WDFWAITLOCK 对象主要用于 PASSIVE_LEVEL 中断级别数据的保护性访问。

(1)

NTSTATUS

WdfWaitLockCreate(

__in_opt

    PWDF_OBJECT_ATTRIBUTES LockAttributes,

    __out

    WDFWAITLOCK* Lock

);

创建同步锁。

(2)

NTSTATUS

WdfWaitLockAcquire(

__in

    WDFWAITLOCK Lock,

    __in_opt

    PLONGLONG Timeout

);

获取同步锁。如果 Timeout 为 0 ,则函数立即返回,不管获取与否,IRQL <= DISPATCH_LEVEL 中断级别调用;如果 Timeout 为 NULL 或非 0 , PASSIVE_LEVEL 中断级别调用。

Timeout: 等待时间参数。正数表示绝对时间;负数是相对时间,从当前时间算起。单位是 0.1us。

(3)

VOID 

WdfWaitLockRelease(

__in

    WDFWAITLOCK Lock

);

释放同步锁。

8. 字符串操作

8.1 字符串格式

在 Windows驱动程序中,可以使用下面4种格式的字符串。

(1)空结尾的字符串(CHAR类型)串

可以用普通的C语言表达字符串常量,例如,"Hello,world!"。

(2)空结尾的宽字符(WCHAR类型)串

用C语言也可以表达宽字符串常量,例如"Goodbye!"。宽字符使用了Windows ANSI 字符集的 ASCII 字符集的 ASCII 和 Latin1 区(0020~007F 和 00A0~00FF)中的字符

(3)ANSI串,由 ANSI_STRING 结构描述

typedef struct _STRING{

USHORT Length;

USHORT MaximumLength;

PCHAR Buffer;

}STRING *PANSI_STRING;

其中,Length 指储存在 Buffer 缓冲区中的字符串字节数,不考虑任何可能存在的空结束符。MaximumLength 指 Buffer 缓冲区的最大空间字节数。Buffer 指字符缓冲区地址指针。

(4) Unicode 串,由 UNICODE_STRING 结构描述

typedef struct _UNICODE_STRING{

USHORT Length;

USHORT MaximumLength;

PWSTR Buffer;

}UNICODE_STRING  *PUNICODE_STRING;

其中,Buffer指宽字符缓冲区地址指针。

UNICODE_STRING 和 ANSI_STRING 有相同的数据结构只是 Buffer 表示的是宽字符缓冲区地址指针。

8.2 WDFSTRING 对象

WDFSTRING 对象用于 Unicode串的编程。

(1)

NTSTATUS 

WdfStringCreate(

__in_opt

    PCUNICODE_STRING UnicodeString,

    __in_opt

    PWDF_OBJECT_ATTRIBUTES StringAttributes,

    __out

    WDFSTRING* String

);

创建字符串对象, PASSIVE_LEVEL 中断级别调用。

UnicodeString: Unicode 串常量地址指针,用于字符串对象的创建值。可以为 NULL 。

(2)

VOID 

WdfStringGetUnicodeString(

__in

    WDFSTRING String,

    __out

    PUNICODE_STRING UnicodeString

);

获取 Unicode 串, PASSIVE_LEVEL 中断级别调用。

8.3 串处理函数

DDK 提供了许多用于处理 Unicode 和 ANSIC 字符串的服务函数。一些标准C运行库函数在内核模式驱动程序中也可以被调用处理常规的C语言串。标准DDK头中

有这些函数的声明,而且链接库中也包含了他们。

   操作 ANSI串函数 Unicode串函数

Length strlen wcslen

Concatenate strcat,strncat wcscat,wcsncat,RtlAppendUnicodeStringToString,RtlAppendUnicodeToString

Copy strcpy,strncpy,RtlCopyString wcscpy,wcsncpy,RtlCopyUnicodeString

Reverse _strrev _wcsrev

Compare strcmp,strncmp,_stricmp, wcscmp,wcsncmp,wcsicmp,_wcsnicmp,RtlCompareUnicodeString,

_strnicmp,RtlCompareString RtlEqualUnicodeString,RtlPrefixUnicodeString

RtlEqualString

Initialize _strset,RtlInitAnsiString, _wcsnset,RtlInitUnicodeString

strnset,RtlInitString

Search strchr,strrchr,strspn,strstr wcschr,wcsrchr,wcsspn,wcsstr

Upper/lower _strlwr,_strupr,RtlUpperString _wcslwr,_wcsupr,RtlUpcaseUnicodeString

case

Character isdigit,islower,isprint,

isspace,isupper,isxdigit,

tolower,toupper,RtlUpperchar towlower,towupper,RtlUpcaseUnicodeChar

Format sprintf,vsprintf, swprinf,_snwprintf

_snprintf,_vsnprintf

String conversion atoi,atol,_itoa _itow,RtlIntegerToUnicodeString,RtlUnicodeStringToInteger

Type conversion RtlAnsiStringToUnicodeSize, RtlUnicodeStringToAnsiString

RtlAnsiStringToUnicodeString

Memory release RtlFreeAnsiString RtlFreeUnicodeString

三、 KMDF 驱动程序和应用程序之间的通信

驱动程序虽然是为设备的硬件层编程服务的,但它同样需要提供和应用程序通信的能力,从而最终达到应用程序控制设备的目的。

1. 应用程序度驱动程序的通信

在 Windows 中,应用程序实现与 KMDF 通信的过程是:应用程序先用 CreateFile 函数打开设备,然后可以使用 DeviceIoControl 和 KMDF 通信,包括从 KMDF 读取数据和写数据给

KMDF 两种情况。也可以使用 ReadFile 从 KMDF 读取数据或用 WriteFile 写数据给 KMDF ,当应用程序退出时用 CloseHandle 关闭设备相对应的 KMDF 回调例程,如下所示:

win32函数 KMDF 回调函数

CreateFile EvtDeviceFileCreate

ReadFile EvtIoRead

WriteFile EvtIoWrite

DeviceIoControl EvtIoDeviceControl

CloseHandle EvtFileCleanup 、 EvtFileClose

1.1 打开设备

应用程序打开设备可以通过两种方式进行: 用 GUID 接口或符号连接名。应用程序一旦获得该设备的有效句柄,就能够与 KMDF 进行通信了。

(1) GUID 接口方式

GetDevicePath( IN LPGUID InterfaceGuid ); //获取设备路径函数

PCHAR DevicePath;

HANDLE hDevice = INVALID_HANDLE_VALUE;

//获取设备路径

DeviecePath = GetDevicePath( ((LPGUID)&xxxSample_DEVINITERFACE_GUID );

//打开设备

hDevice = CreateFile( DeviecePath,

GENERIC_READ|GENERIC_WRITE,

FILE_SHARE_READ | FILE_SHARE_WRITE.

NULL,

OPEN_EXITING,

FILE_ATTRIBUTE_NORMAL,

NULL

);

注意:上面的 CreateFile 函数是以同步方式打开设备的,这样应用程序就会以同步方式实现与 KMDF 的通信。若应用程序以异步方式实现与 KMDF 的通信,则必须修改 CreateFile 函数中的第6个参数,

将 FILE_ATTRIBUTE_NORMAL 改为  FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,以异步方式打开设备。

获取设备路径函数 GetDevicePath( IN LPGUID InterfaceGuid),参考 KNDF 提供的 echo 范例。注意:其有一个缺点,即只能打开一个设备。如果是多个相同的设备,则需要作一些修改。

(2). 符号连接名方式

KMDF 驱动程序创建符号链接名的函数是 WdfDeviceCreateSymbolicLink() 。

若以符号链接名方式打开设备,则符号链接名定义为 "\\\.\\xxx" ,如下所示:

char *sLinkName = "\\\.\\xxx" ;//符号链接名

//打开设备

hDevice  = CreateFile(sLinkName,

 GENERIC_READ|GENERIC_WRITE,

 FILE_SHARE_READ,

 NULL,

 OPEN_EXITING,

 FILE_ATTRIBUTE_NORMAL,

 NULL

 );

1.2 关闭设备

在退出应用程序之前,应当用 CloseHandle 函数关闭设备,系统首先调用 EvtFileCleanup 例程,然后调用 EvtFileClose 例程。事实上,当用 Ctrl_C 强行退出应用程序或应用程序出错被强行关闭时,

都将调用这两个例程。

1.3 DeviceIoControl 函数调用

如果调用 CreateFile 函数成功,应用程序就可以调用 DeviceIoControl 函数与 KMDF 进行通信了。

(1). DeviceIoControl 函数定义

BOOL DeviceIoControl (

HANDLE hDevice, //CreteFile 函数返回的设备句柄

DWORD dwIoControlCode, //应用程序调用驱动程序的控制命令

LPVOID lpInBuffer, //应用程序传递给驱动程序的数据缓冲区地址

DWORD nInBufferSize, //应用程序传递给驱动程序的数据字节数

LPVOID lpOutBuffer, //存放驱动程序返回数据的缓冲区地址

DWORD nOutBufferSize, //存放驱动程序返回数据的缓冲区字节数

LPDWORD lpBytesReturned, //存放驱动程序实际返回数据字节数的变量地址

LPOVERLAPPED lpOverlapped //一个指向 OVERLAPPED 结构的变量地址,同步时置为NULL

);

参数说明:

hDevice: CreateFile 函数返回的设备句柄。

dwIoControlCode: 应用程序调用驱动程序的控制命令

在 public.h 中存放着应用程序调用驱动程序的控制命令。定义控制命令的格式如下:

#define X1 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define X2 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_IN_DIRECT, FILE_ANY_ACCESS)

#define X3 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_OUT_DIRECT, FILE_ANY_ACCESS)

其中 X1 、 X2 和 X3 为应用程序调用驱动程序的3个控制命令。控制命令根据需要可以为一个或多个,定义格式依次类推,只需将第2个参数依次加1即可。

第三个参数决定驱动程序该如何获取应用程序的缓冲区地址。

注意:由于 CTL_CODE 、 FILE_DEVICE_UNKNOWN 等符号的定义包含在 winioctl.h 文件中,因此在用于程序的源文件中,必须包含头文件 "#include"。

这样,编译应用程序就不会出错了。

紧接着下面的4个参数分为2组:一组为应用程序传递给驱动程序的数据缓冲区地址和字节数;另一组为驱动程序的返回数据所存放的缓冲区地址和可用的缓冲空间字节数。

LPDWORD lpBytesReturned: 存放驱动程序实际返回数据字节数的变量地址。

LPOVERLAPPED lpOverlapped: 一个指向 OVERLAPPED 结构的变量地址,同步时置为NULL。

(2) DeviceIoControl 的同步和异步调用方式

采用同步方式时,应用程序调用 DeviceIoControl 函数将被阻塞,直到驱动程序完成相应的数据传输操作,才往下执行。例如:

m_hDevice  = CreateFile(......,

 FILE_ATTRIBUTE_NORMAL,

 NULL

 );

if(DeviceIoControl(m_hDevice,...,NULL))

{

...//出错处理

}

采用异步方式时,应用程序调用 DeviceIoControl 函数立即返回。

HANDLE m_hDevice,IOWaiter;

m_hDevice = CreateFile( ...,

FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,

NULL //异步方式:FILE_FLAG_OVERLAPPED 标志

);

IOWaiter = CreateEvent(NULL,TRUE,FALSE,NULL); //创建手动重置事件

...

OVERLAPPED ol; //OVERLAPPED结构对象初始化

ol.Offset = 0;

ol.OffsetHigh = 0;

ol.hEvent = IOWaiter;

KeepRunning=TRUE;

for(i=0;i<10;i++)

{

bufInput[0]=i+0x30;

bufOutput[2]=0;

ResetEvent(IOWaiter); //复位事件

if (!DeviceIoControl(

m_hDevice, 

CancelSample_IOCTL_800, 

bufInput, 

1, 

bufOutput, 

2,

&nBytesRead,

&ol)) //DeviceIoControl异步方式调用

{

if( GetLastError()!=ERROR_IO_PENDING)

{ //返回错误

msg.Format("GET_TIMESTAMP_DATA failed %x\n", GetLastError());

Print_Event::SendEvent(msg);

goto EXIT;

}

while( WaitForSingleObject( IOWaiter, 100)==WAIT_TIMEOUT)//等待IO完成

{ //正常超时

if(!KeepRunning)

{

// Cancel the pending read

CancelIo(m_hDevice); //取消IO操作

goto EXIT;

}

}

//IO完成后,获取数据

if(!GetOverlappedResult( m_hDevice, &ol, &nBytesRead, FALSE))

{ //返回错误

msg.Format("GetOverlappedResult failed %d (ol.Internal=%X)",

GetLastError(),ol.Internal);

Print_Event::SendEvent(msg);

continue;

}

}

msg.Format("%c is %s\n", bufInput[0],bufOutput);

Print_Event::SendEvent(msg); //显示信息

}

这个异步例子与前面的同步例子有两个主要的不同之处。第一,在调用 CreateFile 函数时指定了 FILE_FLAG_OVERLAPPED 标志;第二,在 DeviceIoControl 函数的调用中指定了一个

OVERLAPPED 结构的变量地址,在这个结构中有一个事件句柄,该事件被初始化成一个手动重置事件。

对 DeviceIoControl 的异步调用将产生3种结果。

第一,他可能返回 TRUE,代表设备驱动程序的派遣例程能够立刻完成请求。

第二,他可能返回 FALSE,并且用 GetLastError 会得到一个特殊的错误代码 ERROR_IO_PENDING 。这个结果指出驱动程序的派遣例程返回了 STATUS_PENDING 。这个结果指出驱动程序的派遣例程返回了 

STATUS_PENDING ,并且推迟完成这个控制器操作。注意: ERROR_IO_PENDING 并不是一个真正的错误。

第三,他可能返回 FALSE, GetLastError 得到的值不是 ERROR_IO_PENDING,这样的结果才是一个真正的错误。

接下来调用 WaitForSingleObject 函数,等待驱动程序完成数据传输在此期间有可能应用程序退出,需要调用 CancelIo 取消相应的 IRP 。最后调用 GetOverlappedResult 函数,这个函数使用起来特别方便,

因为他同时还提供传输字节量值并设置 GetLastError 结果。

(3). 驱动程序获取应用程序数据缓冲区地址的方式

对于 DeviceIoControl 函数调用驱动程序根据 I/O 控制命令来决定应该如何获取应用程序的缓冲区地址。 I/O 控制命令中数据访问方式的定义4种:

METHOD_BUFFERED 、 METHOD_IN_DIRECT 、 METHOD_OUT_DIRECT 或 METHOD_NEITHER 。

如果控制命令定义为 METHOD_BUFFERED 方式,则系统分配一个缓冲区用于输入和输出,该缓冲区的字节数为应用程序的输入和输出缓冲区的大者的字节数。驱动程序必须先拷贝输入数据,然后再复制输出数据。

若定义为 METHOD_IN_DIRECT 方式,则其输出缓冲区可以作为附加的输入缓冲区。对于 METHOD_OUT_DIRECT 方式,其输出缓冲区可否可以作为附加的输入缓冲区呢?从 IOSample 实例的结果看,他也可以,二

者似乎没有差别。

但对于 METHOD_IN_DIRECT 和 METHOD_OUT_DIRECT 方式,KMDF 处理上还是有差异的。对于 WdfDmaTransactionInitializeUsingRequest 函数,当读(WdfDmaDirectionReadFromDevice)时,只能采用 

METHOD_OUT_DIRECT 方式;当写(WdfDmaDirectionWriteToDevice)时,只能采用 METHOD_IN_DIRECT 方式。

对于 METHOD_BUFFERED 、 METHOD_IN_DIRECT 和 METHOD_OUT_DIRECT 方式,相对于过去的 WDM 驱动程序而言,KMDF 驱动程序获取应用程序数据缓冲区地址的函数统一了,这样编程非常简单。

获取的地址有3种形式: PVOID 、 WDFMEMORY 和 PMDL 。 MDL 是存储区的一种表示方式,在 WDM 驱动程序中比较重要,但在 KMDF 驱动程序中用的很少。这里用到的地方是,KMDF 关于 DMA 传输有一个函数 

 WdfDmaTransactionInitialize , 用到 MDL。

获取的地址函数:

***输入(驱动程序读取应用程序数据)地址

NTSTATUS WdfRequestRetrieveInputBuffer();

NTSTATUS WdfRequestRetrieveInputMemory();

NTSTATUS WdfRequestRetrieveInputWdmMdl();

***输出(驱动程序写入应用程序数据)地址

NTSTATUS WdfRequestRetrieveOutputBuffer();

NTSTATUS WdfRequestRetrieveOutputMemory();

NTSTATUS WdfRequestRetrieveOutputWdmMdl();

对于 METHOD_NEITHER 方式,内核仅仅提供一个虚拟地址仅在呼叫进程的环境中有效。应当谨慎采用这种方式。在 KMDF 驱动程序中,其编程也比较复杂。

首先在 DeviceAdd 例程中, 调用 WdfDeviceInitSetIoInCallerContextback()函数,注册回调例程 EvtDeviceCallerContext 。

在 EvtDeviceCallerContext 例程中,要用到如下函数:

WdfRequestRetrieveUnsafeUserInputBuffer

WdfRequestRetrieveUnsafeUserOutputBuffer

WdfRequestProbeAndLockUserBufferForRead

WdfRequestProbeAndLockUserBufferForWrite

1.4 ReadFile 和 WriteFile 函数调用

应用程序也可以用 ReadFile 函数从驱动程序读数据或用 WriteFile 函数写数据给驱动程序。

(1)eadFile 和 WriteFile 函数定义

BOOL ReadFile(

HANDLE hDevice, //CreteFile 函数返回的设备句柄

LPVOID lpBuffer, //存放驱动程序返回数据的缓冲区地址

DWORD nNumberOfBytesToRead, //存放驱动程序返回数据的缓冲区字节数

LPDWORD lpNumberOfBytesRead, //存放驱动程序实际返回数据字节数的变量地址

LPOVERLAPPED lpOverlapped //一个指向 OVERLAPPED 结构的变量地址

);

BOOL WriteFile(

HANDLE hDevice, //CreteFile 函数返回的设备句柄

LPVOID lpBuffer, //应用程序传递给驱动程序的数据缓冲区地址

DWORD nNumberOfBytesToWrite, //应用程序传递给驱动程序的数据字节数

LPDWORD lpNumberOfBytesRead, //存放驱动程序实际写入数据字节数的变量地址

LPOVERLAPPED lpOverlapped //一个指向 OVERLAPPED 结构的变量地址

);

(2). ReadFile 和 WriteFile 函数的同步及异步调用方式

应用程序也可以采用同步或异步方式调用 ReadFile 或 WriteFile函数,其编程与 DeviceIoControl 完全雷同。

(3). 驱动程序获取应用程序 ReadFile 或 WriteFile 数据缓冲区地址的两种方式

应用程序用 ReadFile 或 WriteFile 与驱动程序进行数据传输时,驱动程序根据设备对象创建的特征标志位来决定应该如何获取应用程序的缓冲区地址。

特征标志位设置函数为:

VOID 

WdfDeviceInitSetIoType(

__in PWDFDEVICE_INIT DeviceInit,

__in WDF_DEVICE_IO_TYPE IoType

);

IoType 设置类型有 WdfDeviceIoBuffered 和 WdfDeviceIoDirect 两种,默认值为 WdfDeviceIoBuffered 。 在 KMDF 驱动驱动程序中,对于 Read 和 Write 读写地址的函数也统一了,其与 DeviceIoControl 相同。

2. 驱动程序对应用程序的通信

当 KMDF 捕捉到特定事件(如中断)发生时,应当可以与应用程序进行通信。KMDF 可以采用两种方法: DeviceIoControl 异步完成和 WIN32 事件通知。

2.1 DeviceIoControl 异步完成 

当应用程序调用 DeviceIoControl 函数时,驱动程序首先将此 I/O 请求保存起来,然后代用 WdfRequestMarkCancelable 函数,设置取消例程。当一个事件发生后,获取保存起来的的 I/O 请求,驱动程序调用 

WdfRequestUnmarkCancelable 取消取消例程,然后完成这个 I/O 请求。

注意:对于 DeviceIoControl 异步完成,当应用程序退出时,若 I/O 请求仍未完成,则必须在取消例程中取消 I/O 请求。

(1). Win32 应用程序编程

应用程序中的 DeviceIoControl 调用可以是同步的,但异步调用可能更好些。

(2). 驱动程序编程

下面谈谈驱动程序使用 DeviceIoControl 异步完成对 Win32 应用程序通信的具体编程步骤。

***1. 定义一个 WDFREQUEST ,存放异步完成的 I/O 请求。

typedef struct _DEVICE_CONTEXT{

//存放异步 I/O 请求

WDFREQUEST CurrentRequest;

}DEVICE_CONTEXT,*PDEVICE_CONTEXT;

WDF_DECLARE_CONTEXT_TYPE_WITHNAME(DEVICE_CONTEXT,GetDeviceContext)

***2. DeviceControl 例程编程

//将该 I/O 请求保存起来

pDeviceContext->CurrentRequest = Request;

//设置该 I/O 请求可取消,取消例程为 CancelSample_EvtRequestCancel

WdfRequestMarkCancelable(Request,CancelSample_EvtRequestCancel);

***3. 取消例程编程

//完成取消的 I/O请求

WdfRequestCompleteWithInformation(Request,STATUS_CANCELLED,0);

pDeviceContext->CurrentRequest = NULL;

***4. 当特定事件发生后,完成 I/O 请求,实现驱动程序对应用程序的通信

request = pDeviceContext->CurrentRequest;

if(request != NULL ){

//取消 I/O 请求的取消例程

if( status != STATUS_CANCELLED){

pDeviceContext->CurrentRequest = NULL;

...

//完成I/O请求

WdfRequestCompleteWithInformation(request,...);

}

}

2.2 WIN32 事件通知

驱动程序可以使用 WIN32 事件对 WIN32 应用程序通信。 应用程序创建一个事件后, 将该事件句柄传递给驱动程序,然后等待驱动程序发送事件消息。

驱动程序通过调用 ObReferenceObjectByHandle 函数,获取这个事件的一个内核对象指针后,就可以在 IRQL <= DISPATCH_LEVEL 级别的例程中设置事件信号状态,触发应用程序。

(1). Win32 应用程序编程

***1. 创建一个通知事件

m_hEvent = CreateEvent(NULL, FLASe, FALSE, NULL); //创建自动重置事件

***2. 将创建的自动重置事件句柄传递给驱动程序

if(!DeviceIoControl(hDevice,

EventSample_RegisterEvent,

&m_hEvent,

sizeof(m_hEvent),

NULL,0,&nOutput,

NULL))

***3. 等待事件发生,当事件发生后,就读取数据。

while(WaitForSingleObject(m_hEvent, 0) != WAIT_OBJECT_0);//等读取待数据

if(!DeviceIoControl(hDevice,

EventSample_GetCount,

NULL,

0,

&n,4,&nOutput,

NULL))

2. 驱动程序编程

下面谈谈驱动程序使用 WIN32 事件对 WIN32 应用程序通信的具体编程步骤。

(1). 定义一个 KEvent 指针

例如:

typedef struct _QUEUE_CONTEXT{

PKEVENT Event;

} QUEUE_CONTEXT,*PQUEUE_CONTEXT;

WDF_DECLARE_CONTEXT_TYPE_WITHNAME(QUEUE_CONTEXT, GetQueueContext);

(2). 在 DeviceControl 例程中,接收应用程序传递给 KMDF 的事件句柄,并构造一个内核事件。

例如:

PVOID buffer;

HANDLE hEvent;

PQUEUE_CONTEXT pQueueContext =  GetQueueContext(Queue);

status = WdfRequestRetrieveInputBuffer(Request, 4, &buffer, NULL);

hEvent = *(HANDLE *)buffer;//应用程序创建的事件句柄

//构造内核事件

status = ObReferenceObjectByHandle(hEvent,

  SYNCHRONIZE,

  *ExEventObjectType,

  UserMode,

  &pQueueContext->Event,

  NULL

  );

(3).当特定事件发生后,设置事件为信号状态,实现驱动程序对应用程序的通信。

注意: 只能在 IRQL <= DISPATCH_LEVEL 级别上调用设置函数。

KeSetEvent(pQueueContext->Event, 0, FALSE);

四、即插即用例程

即插即用 (PnP) 是一个硬件和软件支持的组合, PnP需要获得硬件设备,系统软件和驱动程序的支持。这些软件支持能够使系统自动识别或适应硬件配置的一些改变而不用用户的干预。用户可从危机系统添加

或删除设备为不必做一些复杂的人工配置。

即插即用的功能有两个:

(1)自动和动态识别安装的 PnP 设备。当添加一个 PnP设备时 ,例如 USB 设备动态插入系统时,系统就能够自动识别。

(2)硬件资源分配和再分配。 PnP 管理器对每一个 PnP 设备所提出的资源需求(例如 I/O 端口、硬件终端号 IRQ 等)进行适当的资源分配。当有新的 PnP 设备被添加到正在使用的系统中时,可以重新分配资源。

1. 即插即用简介

1.1 PnP 组件

如下图所示,描述了支持 PnP 工作的软件组件。

WDF开发详解

PnP 管理器分为两个部分:用户模式 PnP 管理器和内核模式 PnP 管理器。用户模式 PnP 管理器与用户安装组件相互作用来配置和安装设备;用户模式 PnP 管理器也与一些应用程序相互作用,例如对于一个注册设备改变

通知的应用程序,当一个设备事件发生时,便会通知应用程序。内核模式 PnP 管理器与操作系统组件和驱动程序相互作用来配置、管理和维护设备。

1.2 即插即用例程

Windows 和 WDM 驱动程序提供了一个复杂的即插即用和电源管理模型,该模型依靠驱动程序来跟踪其设备与系统的状态,进而实现模型自己的非正式的状态机。

KMDF 框架实现了智能的默认行为并提供了一组特定于状态的回调,驱动程序可以实现这些回调来自定义即插即用和电源行为。 KMDF 驱动程序仅提供提供操作其设备所需要的代码。该框架跟踪设备和系统状态,并在注册的

回调位置调用驱动程序,从而执行设备特定操作。

即插即用和电源管理包括安装、配置和设备操作所涉及的活动。要正确支持即插即用和电源管理,操作系统、驱动程序、系统管理软件、设备安装软件、系统硬件和设备硬件必须系统工作。

即插即用和电源管理例程设置的配置结构是 WDF_PNPPOWER_EVENT_CALLBACKS,其定义如下:

typedef struct _WDF_PNPPOWER_EVENT_CALLBACKS {

    //

    // Size of this structure in bytes

    //

    ULONG Size;

    PFN_WDF_DEVICE_D0_ENTRY               EvtDeviceD0Entry;

   PFN_WDF_DEVICE_D0_ENTRY_POST_INTERRUPTS_ENABLED EvtDeviceD0EntryPostInterruptsEnabled;

    PFN_WDF_DEVICE_D0_EXIT               EvtDeviceD0Exit;

   PFN_WDF_DEVICE_D0_EXIT_PRE_INTERRUPTS_DISABLED EvtDeviceD0ExitPreInterruptsDisabled;

   PFN_WDF_DEVICE_PREPARE_HARDWARE       EvtDevicePrepareHardware;

   PFN_WDF_DEVICE_RELEASE_HARDWARE       EvtDeviceReleaseHardware;

   PFN_WDF_DEVICE_SELF_MANAGED_IO_CLEANUP  EvtDeviceSelfManagedIoCleanup;

   PFN_WDF_DEVICE_SELF_MANAGED_IO_FLUSH   EvtDeviceSelfManagedIoFlush;

   PFN_WDF_DEVICE_SELF_MANAGED_IO_INIT    EvtDeviceSelfManagedIoInit;

   PFN_WDF_DEVICE_SELF_MANAGED_IO_SUSPEND  EvtDeviceSelfManagedIoSuspend;

   PFN_WDF_DEVICE_SELF_MANAGED_IO_RESTART  EvtDeviceSelfManagedIoRestart;

   PFN_WDF_DEVICE_SURPRISE_REMOVAL       EvtDeviceSurpriseRemoval;

   PFN_WDF_DEVICE_QUERY_REMOVE           EvtDeviceQueryRemove;

   PFN_WDF_DEVICE_QUERY_STOP             EvtDeviceQueryStop;

   PFN_WDF_DEVICE_USAGE_NOTIFICATION     EvtDeviceUsageNotification;

   PFN_WDF_DEVICE_RELATIONS_QUERY        EvtDeviceRelationsQuery;

} WDF_PNPPOWER_EVENT_CALLBACKS, *PWDF_PNPPOWER_EVENT_CALLBACKS;

上述结构中,既包含了即插即用例程,也包含了电源管理例程。

(1)即插即用基本例程

EvtDevicePrepareHardware: 获取资源配置例程。

EvtDeviceReleaseHardware: 删除资源配置例程。

这两个是最基本的即插即用例程,在 PCI 和 USB 设备中,必须设置,以获取资源配置和释放资源。

(2)其他即插即用例程

EvtDeviceSurpriseRemoval: 设备被意外拔除时的调用例程。

EvtDeviceQueryRemove: 询问设备是否可以删除的调用例程。

EvtDeviceQueryStop: 询问设备是否可以停止的调用例程。

(3)自我管理的 I/O 例程

KMDF 提供了一组用于自我管理的 I/O 例程,这些例程与排队的 I/O 请求无关,与即插即用过程和电源发生变化有关。这些历程如下:

EvtDeviceSelfManagedIoInit: 设备初次启动时调用例程,启动操作。

EvtDeviceSelfManagedIoRestart: 设备再次启动时调用例程,启动操作。

EvtDeviceSelfManagedIoSuspend: 设备进入省电状态或删除时调用例程,停止操作。

EvtDeviceSelfManagedIoFlush: 设备删除时调用例程,做一些释放工作。

EvtDeviceSelfManagedIoCleanup: 设备删除时调用例程,做一些清除工作。

(4)电源管理例程

EvtDeviceD0Entry: 进入正常工作电源 D0 状态的调用例程

EvtDeviceD0Exit: 退出工作电源 D0 状态的调用例程

EvtDeviceD0EntryPostInterruptsEnabled: 进入正常工作电源 D0 状态后,使能中断的例程

EvtDeviceD0ExitPreInterruptsDisabled: 退出工作电源 D0 状态之前,禁止中断的例程

例程配置结构初始化函数如下:

VOID 

WDF_PNPPOWER_EVENT_CALLBACKS_INIT(

__out PWDF_PNPPOWER_EVENT_CALLBACKS Callbacks

);

设置函数如下:

VOID 

WdfDeviceInitSetPnpPowerEventCallbacks(

__in

    PWDFDEVICE_INIT DeviceInit,

    __in

   PWDF_PNPPOWER_EVENT_CALLBACKS PnpPowerEventCallbacks

);

此外,还有一些即插即用例程,其设置由下面的 WdfFdoInitSetEventCallbacks 和 WdfPdoInitSetEventCallbacks 函数来完成。

(1) FDO 的即插即用例程

结构定义如下:

typedef struct _WDF_FDO_EVENT_CALLBACKS {

    //

    // Size of this structure in bytes

    //

    ULONG Size;

   PFN_WDF_DEVICE_FILTER_RESOURCE_REQUIREMENTS EvtDeviceFilterAddResourceRequirements;

   PFN_WDF_DEVICE_FILTER_RESOURCE_REQUIREMENTS EvtDeviceFilterRemoveResourceRequirements;

   PFN_WDF_DEVICE_REMOVE_ADDED_RESOURCES EvtDeviceRemoveAddedResources;

} WDF_FDO_EVENT_CALLBACKS, *PWDF_FDO_EVENT_CALLBACKS;

初始化函数

VOID 

WDF_FDO_EVENT_CALLBACKS_INIT(

__out PWDF_FDO_EVENT_CALLBACKS Callbacks

);

设置函数

VOID 

WdfFdoInitSetEventCallbacks(

__in

    PWDFDEVICE_INIT DeviceInit,

    __in

    PWDF_FDO_EVENT_CALLBACKS FdoEventCallbacks

);

(2)PDO 的即插即用例程

结构定义如下:

typedef struct _WDF_PDO_EVENT_CALLBACKS {

    //

    // The size of this structure in bytes

    //

    ULONG Size;

    //

    // Called in response to IRP_MN_QUERY_RESOURCES

    //

   PFN_WDF_DEVICE_RESOURCES_QUERY EvtDeviceResourcesQuery;

    //

    // Called in response to IRP_MN_QUERY_RESOURCE_REQUIREMENTS

    //

   PFN_WDF_DEVICE_RESOURCE_REQUIREMENTS_QUERY EvtDeviceResourceRequirementsQuery;

    //

    // Called in response to IRP_MN_EJECT

    //

    PFN_WDF_DEVICE_EJECT EvtDeviceEject;

    //

    // Called in response to IRP_MN_SET_LOCK

    //

    PFN_WDF_DEVICE_SET_LOCK EvtDeviceSetLock;

    //

    // Called in response to the power policy owner sending a wait wake to the

    // PDO.  Bus generic arming shoulding occur here.

    //

   PFN_WDF_DEVICE_ENABLE_WAKE_AT_BUS     EvtDeviceEnableWakeAtBus;

    //

    // Called in response to the power policy owner sending a wait wake to the

    // PDO.  Bus generic disarming shoulding occur here.

    //

   PFN_WDF_DEVICE_DISABLE_WAKE_AT_BUS    EvtDeviceDisableWakeAtBus;

} WDF_PDO_EVENT_CALLBACKS, *PWDF_PDO_EVENT_CALLBACKS;

初始化函数

VOID 

WDF_PDO_EVENT_CALLBACKS_INIT(

__out PWDF_PDO_EVENT_CALLBACKS Callbacks;

);

设置函数

VOID 

WdfPdoInitSetEventCallbacks(

__in

    PWDFDEVICE_INIT DeviceInit,

    __in

    PWDF_PDO_EVENT_CALLBACKS DispatchTable

);

1.3 例程的加载和卸载顺序

在设备加载和卸载时,驱动程序中的各种例程调用有着一定的顺序。要了解其顺序,这对于编写 KMDF 驱动程序有很大的帮助。

如图:

五、电源管理

Windows 操作系统支持电源管理,计算机和其他外部设备被维持在完成当前任务所需的最低可行的电平上,驱动程序与操作系统协作来管理他们的设备电源。如果所有的驱动程序支持电源管理,操作系统就能够在

系统范围基础上管理电力消耗,这样一来,电力保存、电力切断和快速恢复及需要时再次唤醒都可被管理。

1. 电源管理简介

1.1 系统电源状态与设备电源状态

(1). 系统电源状态

系统电源状态描述整个系统的能源消耗。操作系统支持6种系统电源状态,即从 S0 到 S5 。状态 S0 是工作状态,状态 S5 是关机状态。

在驱动程序中,6种系统电源状态所对应的名称如下:

S0-PowerSystemWorking

S1-PowerSystemSleep1

S2-PowerSystemSleep2

S3-PowerSystemSleep3

S4-PowerSystemHibernate

S5-PowerSystemShutdown

状态 S1 、 S2 、 S3 和 S4 是休眠状态,能源消耗量依次递减。

其中 S4 状态是冬眠状态,是最低功率的休眠状态。为了把能源消耗降到最低,所有设备的硬件电源都被关闭。然而,操作系统环境被保存着,即系统再进入 S4 状态之前就把它写在一个冬眠文件上。

当重新启动时,装入例程文件阅读此文件,系统进入冬眠状态前的操作系统环境。

状态 S4 和 S5 之间唯一的区别是:在状态 S4 中计算机能从冬眠文件中重新启动,而在状态 S5 中重新启动需要重新引导系统。

系统不能从一种休眠状态直接进入另一种休眠状态,他必须再进入另一种休眠状态之前进入工作状态、例如,系统不能从状态 S1 转变到状态 S2 ,也不能从状态 S2 转变到状态 S1 ; 他必须首先回到工作

状态 S0 ,然后才能进入下一个休眠状态。因为处于一种中间休眠状态的系统已经失去某种操作环境,在进行其他状态转变之前,他必须回到工作状态 S0 以恢复该环境。

(2) 设备电源状态

设备电源状态描述特定设备的能源消耗。设备可以支持4种设备电源状态,即从 D0 到 D3 。状态 D0 是工作状态,状态 D3 是关闭状态,状态 D1 和 D2 是休眠状态,能源消耗量依次递减。

在驱动程序中,4种系统电源状态所对应的名称如下:

D0-PowerDeviceD0

D1-PowerDeviceD1

D2-PowerDeviceD2

D3-PowerDeviceD3

设备能从工作状态 D0 转变到任一种休眠状态 (D1 或 D2),也能从任一种休眠状态转变到工作状态。设备还能从一种休眠状态直接进入另一种休眠状态。

电源状态的确切定义是设备特定的,并非所有设备都定义所有状态,许多设备仅仅定义状态 D0 和 D3 。微软规定了不同类型设备的专用电源需求,这个规范要求每个设备至少要支持 D0 和 D3 两个状态。输入设备

(键盘、鼠标等) 还应该支持 D1 状态,Modem 设备需要另外支持 D2 状态。设备类上的这些不同规定可能来源于设备的用途和工业上的实践。

KMDF 支持的设备电源状态如下:

WdfPowerDeviceD0

WdfPowerDeviceD1

WdfPowerDeviceD2

WdfPowerDeviceD3

WdfPowerDeviceD3Final

WdfPowerDevicePrepareForHibernation

当系统关闭或设备移除时,框架传递的是 WdfPowerDeviceD3Final 。支持文件操作的设备在系统进入 S4 状态时,框架传递的是 WdfPowerDevicePrepareForHibernation 。

(3)系统电源状态与设备状态电源状态的关系

设备的电源状态不必与系统的电源状态匹配。一般来说,当系统处于休眠状态时,设备也处于休眠状态;然而,能唤醒系统的设备是个例外。

系统初始化后即进入工作状态。大部分设备也以 D0 状态启动,但某些设备的驱动程序毁在设备启动时便使设备进入地能源消耗状态。系统电源处于工作状态,而设备处于何种状态取决于设备的具体活动和自身的能力。

1.2 电源管理控制标志位

在 EvtDeviceAdd 例程中,当创建设备对象时,可以根据需要设置相应的电源管理控制标志位。标志位有两个: DO_POWER_PAGEBLE 和 DO_POWER_INRUSH 。

****如果电源管理回调例程必须运行在 PASSIVE_LEVEL 级别,则需要设置 DO_POWER_PAGEBLE 标志。如果把该标志设置为0,则电源管理器可以在 DISPATCH_LEVEL 级别上向你发送电源管理请求。

****如果设备在上电时需要大电流,则设置 DO_POWER_INRUSH 标志,这可以使多个有这样要求的设备不同时上电。

****设备的所有设备对象中的 PAGEBLE 和 INRUSH 标志设置都要一致。如果 PDO 设置了 PAGEBLE 标志,则其他设备对象也应该设置 PAGEBLE 标志。

****驱动程序不能同时设置 DO_POWER_PAGEBLE 和 DO_POWER_INRUSH 。

****大多数 KMDF 驱动程序必须设置 DO_POWER_PAGEBLE 。

KMDF 与上述相关的函数如下:

VOID WdfDeviceInitSetPowerPageable(__in PWDFDEVICE_INIT DeviceInit);

VOID WdfDeviceInitSetPowerNotPageable(__in PWDFDEVICE_INIT DeviceInit);

VOID WdfDeviceInitSetPowerInrush(__in PWDFDEVICE_INIT DeviceInit);

1.3 设备的唤醒特征和空闲检测

某些设备具有硬件唤醒特征,这允许他们在外部事件发生时可以唤醒处于休眠状态的系统。如果设备支持唤醒,其电源策略者必须能够激活或禁止设备的唤醒能力。

通常,在设备不被使用时,用户希望其不消耗或少消耗电能。你可以向电源管理器寄存这种空闲检测要求,如果你的设备空闲了指定长度的时间,则电源管理自动向设备发出降低电源消耗的请求。

2. 电源管理编程

对于许多过滤和软件功能驱动程序,KMDF 的默认配置已经可以满足他们对于即插即用和电源管理的处理,这些程序不需要关于即插即用和电源管理的编程处理。

在默认配置下:

**设备支持 D0 和 D3 状态;

**设备和驱动不支持空闲和唤醒;

**FDO 和 PDO 的 I/O 队列实行电源管理。

2.1 电源管理基本例程

KMDF 提供了 2 个基本的电源管理例程: EvtDeviceD0Entry(进入正常工作电源D0状态)和 EvtDeviceD0Exit(退出工作电源 D0 状态)。

2.2 设备唤醒

关于设备唤醒的电源管理例程有:

**EvtDeviceArmWakeFromS0: 当系统工作在 S0 状态和设备处于低能源状态时,例程允许设备触发唤醒信号。

**EvtDeviceDisarmWakeFromS0: 当系统工作在 S0 状态和设备处于低能源状态时,例程禁止设备触发唤醒信号。

**EvtDeviceArmWakeFromSx: 当系统工作处于低能源状态时且使设备处于低能源状态时,例程允许设备触发唤醒信号。

**EvtDeviceDisarmWakeFromSx: 当系统工作处于低能源状态时且使设备处于低能源状态时,例程禁止设备触发唤醒信号。

**EvtDeviceWakeFromS0Triggered: 当系统工作在 S0 状态和设备处于低能源状态时,例程通知驱动程序设备触发了唤醒信号。

**EvtDeviceWakeFromSxTriggered: 当系统工作处于低能源状态和设备处于低能源状态时,例程通知驱动程序设备触发了唤醒信号。

设备唤醒的电源管理例程配置结构式 WDF_POWER_POLICY_EVENT_CALLBACKS ,其定义如下:

typedef struct _WDF_POWER_POLICY_EVENT_CALLBACKS{

//

    // Size of this structure in bytes

    //

    ULONG Size;

   PFN_WDF_DEVICE_ARM_WAKE_FROM_S0       EvtDeviceArmWakeFromS0;

   PFN_WDF_DEVICE_DISARM_WAKE_FROM_S0    EvtDeviceDisarmWakeFromS0;

   PFN_WDF_DEVICE_WAKE_FROM_S0_TRIGGERED  EvtDeviceWakeFromS0Triggered;

   PFN_WDF_DEVICE_ARM_WAKE_FROM_SX       EvtDeviceArmWakeFromSx;

   PFN_WDF_DEVICE_DISARM_WAKE_FROM_SX    EvtDeviceDisarmWakeFromSx;

   PFN_WDF_DEVICE_WAKE_FROM_SX_TRIGGERED  EvtDeviceWakeFromSxTriggered;

   PFN_WDF_DEVICE_ARM_WAKE_FROM_SX_WITH_REASON EvtDeviceArmWakeFromSxWithReason;

} WDF_POWER_POLICY_EVENT_CALLBACKS, *PWDF_POWER_POLICY_EVENT_CALLBACKS;

设备唤醒的电源管理例程配置初始化函数如下:

VOID 

WDF_POWER_POLICY_EVENT_CALLBACKS_INIT(

__out PWDF_POWER_POLICY_EVENT_CALLBACKS Callbacks

)

设备唤醒的电源管理例程配置设置函数如下:

VOID 

WdfDeviceInitSetPowerPolicyEventCallbacks(

__in

PWDFDEVICE_INIT DeviceInit,

__in

PWDF_POWER_POLICY_EVENT_CALLBACKS PowerPolicyEventCallbacks

);

唤醒编程的配置结构是 WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS , 其定义如下:

typedef struct _WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS {

    //

    // Size of this structure in bytes

    //

    ULONG Size;

    //

    // The low power state in which the device will be placed when it is armed

    // for wake from Sx.

    //

    DEVICE_POWER_STATE DxState;

    //

    // Inidcates whether a user can control the idle policy of the device.

    // By default, a user is allowed to change the policy.

    //

   WDF_POWER_POLICY_SX_WAKE_USER_CONTROL UserControlOfWakeSettings;

    //

    // If WdfTrue, arming the device for wake while the machine is in Sx is

    // enabled.

    //

    // If WdfFalse, arming the device for wake while the machine is in Sx is

    // disabled.

    //

    // If WdfUseDefault, arming will be enabled.  If UserControlOfWakeSettings

    // is set to WakeAllowUserControl, the user's settings will override the

    // default.

    //

    WDF_TRI_STATE Enabled;

    //

    // If set to TRUE, arming the parent device can depend on whether there

    // is atleast one child device armed for wake.

    //

    // If set to FALSE, arming of the parent device will be independent of

    // whether any of the child devices are armed for wake.

    //

    BOOLEAN ArmForWakeIfChildrenAreArmedForWake;

    //

    // Indicates that whenever the parent device completes the wake irp

    // successfully, the status needs to be also propagated to the child

    // devices.  This helps in tracking which devices were responsible for

    // waking the system.

    //

    BOOLEAN IndicateChildWakeOnParentWake;

} WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS, *PWDF_DEVICE_POWER_POLICY_WAKE_SETTINGS;

DxState: 当系统进入低电源状态下时的设备电源状态。配置值为 PowerDeviceD1 、 PowerDeviceD2 、PowerDeviceD3 。

UserControlOfIdleSettings: 是否提供一个在设备管理器中的属性页供管理员操作。配置值为 WakeDoNotAllowUserControl 、WakeAllowUserControl(此为默认值)。

Enabled: 是否允许设备唤醒低电源状态下的系统。配置值为 WdfTrue(允许) 、WdfFalse(禁止) 、 WdfUseDefault(允许,除非其被管理员级用户禁止。此为默认值)。

唤醒编程的配置初始化函数如下:

VOID 

WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS_INIT{

__out PWDF_DEVICE_POWER_POLICY_WAKE_SETTINGS Settings

};

最后调用 WdfDeviceAssignSxWakeSettings 函数, 设置设备唤醒。函数如下:

NTSTATUS

FORCEINLINE

WdfDeviceAssignSxWakeSettings(

    __in

    WDFDEVICE Device,

    __in

   PWDF_DEVICE_POWER_POLICY_WAKE_SETTINGS Settings

    );

2.3 空闲检测 

空闲检测的配置结构是 WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS

typedef struct _WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS {

    //

    // Size of this structure in bytes

    //

    ULONG Size;

    //

    // Indicates whether the device can wake itself up while the machine is in

    // S0.

    //

   WDF_POWER_POLICY_S0_IDLE_CAPABILITIES IdleCaps;

    //

    // The low power state in which the device will be placed when it is idled

    // out while the machine is in S0.

    //

    DEVICE_POWER_STATE DxState;

    //

    // Amount of time the device must be idle before idling out.  Timeout is in

    // milliseconds.

    //

    ULONG IdleTimeout;

    //

    // Inidcates whether a user can control the idle policy of the device.

    // By default, a user is allowed to change the policy.

    //

   WDF_POWER_POLICY_S0_IDLE_USER_CONTROL UserControlOfIdleSettings;

    //

    // If WdfTrue, idling out while the machine is in S0 will be enabled.

    //

    // If WdfFalse, idling out will be disabled.

    //

    // If WdfUseDefault, the idling out will be enabled.  If

    // UserControlOfIdleSettings is set to IdleAllowUserControl, the user's

    // settings will override the default.

    //

    WDF_TRI_STATE Enabled;

    //

    // This field is applicable only when IdleCaps == IdleCannotWakeFromS0

    // If WdfTrue,device is powered up on System Wake even if device is idle 

    // If WdfFalse, device is not powered up on system wake if it is idle

    // If WdfUseDefault, the behavior is same as WdfFalse 

   // 

    WDF_TRI_STATE PowerUpIdleDeviceOnSystemWake;

} WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS, *PWDF_DEVICE_POWER_POLICY_IDLE_SETTINGS;

IdleCaps: 是否支持空闲和是否在 S0 的支持唤醒;对于 USB 设备,是否支持选择性暂停。配置值为 IdleCannotWakeFromS0 、 IdleCanWakeFromS0 、 IdleUsbSelectiveSuspend 。

DxState: 空闲时设备电源状态。配置值为 PowerDeviceD0 、PowerDeviceD1 、 PowerDeviceD2 、PowerDeviceD3 。若 IdleCaps 选择 IdleCannotWakeFromS0 ,则此默认值为 PowerDeviceD3 。

IdleTimeout: 空闲检测时间,单位为毫秒。默认值为5秒。

UserControlOfIdleSettings: 是否提供一个在设备管理器中的属性页供管理员操作。配置值为 IdleDoNotAllowUserControl 、 IdleAllowUserControl(此为默认值)。

Enabled: 在设备空闲时是否允许其处于省电状态。配置为 WdfTrue(允许)、WdfFalse(禁止)、WdfUseDefault(允许,除非其被管理员级用户禁止。此为默认值)。

空闲检测配置的初始化函数如下:

VOID

WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS_INIT(

__out PWDF_DEVICE_POWER_POLICY_IDLE_SETTINGS Settings,

    __in WDF_POWER_POLICY_S0_IDLE_CAPABILITIES IdleCaps

);

最后调用 WdfDeviceAssignS0IdleSettings 函数, 设置空闲检测。函数如下所示:

NTSTATUS

VOID

FORCEINLINE

WdfPdoInitSetEventCallbacks(

    __in

    PWDFDEVICE_INIT DeviceInit,

    __in

    PWDF_PDO_EVENT_CALLBACKS DispatchTable

    )

在某些情况下,如果要禁止空闲检测,则可以调用 WdfDeviceStopIdle 函数来阻止;想恢复时,调用 WdfDeviceResumeIdle 函数即可。

(1)

NTSTATUS

WdfDeviceStopIdle(

__in

    WDFDEVICE Device,

    __in

    BOOLEAN WaitForD0

);

WaitForD0: 若为 TRUE ,则只有当设备进入 D0 电源状态时,函数才返回;若为FALSE,则立即返回。

(2)

VOID

WdfDeviceResumeIdle(

    __in

    WDFDEVICE Device

)

六、 KMDF 过滤器驱动程序

WDM 模型假定硬件设备可以有多个驱动程序,每个驱动程序都有自己管理设备的方法。 WDM根据设备对象堆栈来完成驱动程序的分层。到现在为止,我们讨论的仅是管理设备主要功能的功能驱动程序。

在这一部分中,将描述如何写一个过滤器驱动程序,该驱动程序可位于功能驱动程序的上面或下面,它通过过滤流经它的 IRP 来修改设备的行为。

处于功能驱动程序之上的过滤器驱动程序称为上层过滤器;处于功能驱动程序之下的过滤器驱动程序(仍处于总线驱动程序之上)称为下层过滤器。

上层过滤器驱动程序的用途是帮助支持这样的设备,这种设备的大多数方面都像其所属类的普通设备,但有一些附加功能。你可以依靠一个通用的功能驱动程序来支持设备的普通行为。为了处理设备的附加功能,你可以写一个上层过滤器驱动程序来干预 IRP 流。

上层过滤器驱动程序的另一个用途是修正硬件或功能驱动程序中出现的错误。如果过滤器驱动程序用于这个目的,微软会请求你在这个过滤器驱动程序上加上版本标签。并且如果在以后某一天这个错误被纠正,你应该在你的控制范围内修改任何相关部件的版本号;

否则,微软将难于使系统自动更新。

下层过滤器驱动程序不能干涉功能驱动程序直接执行的正常操作,因为功能驱动程序可能通过 HAL 调用直接访问硬件来实现大部分实质的请求。而下层过滤器仅能看到经过它传递的 IRP ,它看不到 HAL 调用。

下层过滤器驱动程序的另一个用途是帮助你写一个总线无关的驱动程序。假设一种设备有三种不同总线形式的产品,如 PCI 总线产品、 USB 总线产品、 PCMCIA 总线产品。你可以写一个完全独立于总线结构的功能驱动程序,这样的驱动程序不直接与设备对话。

另外,你还要写3个下层过滤器驱动程序,每个下层过滤器对应一个总线类型。当功能驱动程序需要与硬件对话时,它就向相应的下层过滤器驱动程序发送 IRP 。

1. KMDF 过滤器驱动程序的编程

虽然过滤器驱动程序和功能驱动程序用于不同的目的,但 KMDF 创建这两种驱动程序的机制完全相同。实际上,创建过滤器驱动层序就像创建任何其他 KMDF 驱动程序一样,都有 DriverEntry 例程、 EvtDriverDeviceAdd 例程等。

在过滤器驱动程序中,需要注意以下3个问题。

(1)在 DeviceAdd 例程中,必须调用 WdfFdoInitSetFilter 函数,表示这是一个过滤器驱动程序。

NTSTATUS

FilterSample_EvtDeviceAdd(

IN WDFDRIVER Driver,

IN PWDFDEVICE_INIT DeviceInit

)

{

...

WdfFdoInitSetFilter(DeviceInit);

...

}

(2)对于要过滤处理的 I/O 请求命令,如果关心底层完成该 I/O 请求后的情况,则必须设置完成例程,在完成例程中作进一步的处理。

//将 I/O 请求发送到下层设备前,做相应的数据处理

WdfRequestFormatRequestUsingCurrentType(Request);

//设置 I/O 请求完成回调例程

WdfRequestSetCompletionRoutine(

Request,

FilterSample_EvtRequestIoctlCompletionRoutine,

NULL

);

//将 I/O 请求发送到下层设备

ret = WdfRequestSend(Request,

WdfDeviceGetIoTarget(WdfIoQueueGetDevice(Queue)),

WDF_NOSEND_OPTIONS

);

(3)如果对于某些 I/O 请求命令,不做过滤处理或不关心底层完成该 I/O 请求后的情况,则不需要设置完成例程。

WdfRequestFormatRequestUsingCurrentType(Request);

WDF_REQUEST_SEND_OPTIONS_INIT(

&options,

WDF_REQUEST_SEND_OPTIONS_AND_FORGET

);

ret = WdfRequestSend(Request,

WdfDeviceGetIoTarget(WdfIoQueueGetDevice(Queue)),

WDF_NOSEND_OPTIONS

);

2. KMDF 过滤器驱动程序安装

KMDF 过滤器驱动程序的安装方式通常有两种:一是和功能驱动程序一起安装;二是单独安装。FilterSample 实例演示了过滤器驱动程序和功能驱动程序同时安装的情况。

若过滤器驱动程序和功能驱动程序是一起安装的,那么在 CharSample.inf 的基础上,只要添加4部分的内容就可以了。

由第三方提供的过滤器驱动程序,通常需要单独安装。

七、 PCI 设备驱动程序开发

现代微机的扩展槽通常有两种接口标准:一种是过去常用但现已淘汰的 ISA 总线;另一种是支持 即插即用的 PCI 总线。PCI 总线定义了32位数据总线,可扩展成64位数据总线,使用 33MHz 时钟频率,最大数据传输率为

132~264 MB/s ,支持无线读/写触发操作,支持并发工作方式。PCI 总线以其优良性能和可适应性成为 Pentium 以上微机的主流总线。现在开发微机接口设备,通常采用 PCI 总线。

开发 PCI 设备,需要有硬件的支持,即要选择PCI总线接口控制芯片。可采用专用芯片实现通用PCI总线接口,如 AMCC 公司提供的 S5933 和 PLX 公司提供的 9045 系列芯片。专用芯片可以实现完整的 PCI 主控模块和目

标模块接口功能,将复杂的 PCI 总线接口转换为相对简单的用户接口,用户只要设计转换后的总线接口即可。还可采用可编程逻辑器件(EPLD或FPGA)实现通用 PCI 总线接口。现在有许多生产可编程逻辑器件的厂商,如 Xilinx

的 LogiCore和Altera的AMPP,都提供有经过严格测试的PCI接口功能模块,由用户进行简单的组合设计即可。

开发 PCI 设备驱动程序 KMDF ,主要有三个方面的问题:硬件访问、中断处理和DMA传输。下面就对这3方面展开讨论,并以 CY7C09449 芯片为例,给出一个经过测试的 PCI 设备驱动程序实例。另外,为了便于掌握 DMA

编程,还给出了一个不依靠硬件设备的 DMA 例程。

1. 硬件访问

x86 处理器有两种独立的地址空间,这两种地址空间分别是 I/O 地址和内存地址。 I/O地址空间只有 64KB ,而内存地址空间现在达到 4GB 。对于微机接口卡,其中的 I/O 和存储芯片可以定位于这两种独立的地址空间中。

一个芯片的地址在 I/O 地址空间的范围称为 I/O 映射,在 I/O 地址空间的设备只能通过 I/O 指令来访问,如汇编语言的 IN 、 OUT 指令。而一个芯片的地址在内存地址空间的范围称为内存映射,可以通过一些内存访问指令

来访问。

1.1 I/O 访问

对于 I/O 映射地址芯片的访问, KMDF 提供了下列函数,这些函数的运行级别是任意 IRQL 。

*** 单个字节、字、双字的写

(1) VOID WRITE_PORT_UCHAR ( IN PUCHAR Port, IN UCHAR Value );

往端口写一个字节数据。

Port: 端口地址

Value: 所写字节数据

(2) VOID WRITE_PORT_USHORT( IN PUSHORT Port, IN USHORT Value );

往端口写一个字数据。

(3)VOID WRITE_PORT_ULONG( IN PULONG Port, IN ULONG Value );

往端口写一个双字数据。

*** 连续字节、字、双字的写

(1) VOID 

WRITE_PORT_BUFFER_UCHAR(

IN PUCHAR Port,

IN PUCHAR Buffer,

IN ULONG  Count

);

往端口连续写多个字节数据。

Port: 端口地址

Buffer: 写数据缓冲区地址指针

Count: 缩写字节数

(2)VOID

WRITE_PORT_BUFFER_USHORT(

IN PUSHORT Port,

IN PUSHORT Buffer,

IN ULONG  Count

);

往端口连续写多个字数据。

(3)VOID

WRITE_PORT_BUFFER_ULONG(

IN PULONG Port,

IN PULONG Buffer,

IN ULONG  Count

);

往端口连续写多个双字数据。

***单个字节、字、双字的读

(1) VOID READ_PORT_UCHAR ( IN PUCHAR Port, IN UCHAR Value );

往端口读一个字节数据。

(2) VOID READ_PORT_USHORT( IN PUSHORT Port, IN USHORT Value );

往端口读一个字数据。

(3)VOID READ_PORT_ULONG( IN PULONG Port, IN ULONG Value );

往端口连续读双字数据。

*** 连续字节、字、双字的读

(1) VOID 

READ_PORT_BUFFER_UCHAR(

IN PUCHAR Port,

IN PUCHAR Buffer,

IN ULONG  Count

);

从端口连续读多个字节数据。

Port: 端口地址

Buffer: 读数据缓冲区地址指针

Count: 所读字节数

(2)VOID

READ_PORT_BUFFER_USHORT(

IN PUSHORT Port,

IN PUSHORT Buffer,

IN ULONG  Count

);

从端口连续读多个字数据。

(3)VOID

READ_PORT_BUFFER_ULONG(

IN PULONG Port,

IN PULONG Buffer,

IN ULONG  Count

);

从端口连续读多个双字数据。

1.2 存储器访问

对于存储器映射芯片的访问, KMDF 提供了下列函数,这些函数的运行级别是任意 IRQL。

**1** 单个字节、字、双字、四字的写

(1) VOID WRITE_REGISTER_UCHAR ( IN PUCHAR Register, IN UCHAR Value );

往存储器写一个字节数据。

Register: 存储器地址

Value: 所写字节数据

(2) VOID WRITE_REGISTER_USHORT( IN PUSHORT Register, IN USHORT Value );

往存储器写一个字数据。

(3)VOID WRITE_REGISTER_ULONG( IN PULONG Register, IN ULONG Value );

往存储器写一个双字数据。

(4)VOID WRITE_REGISTER_ULONG64( IN PULONG64 Register, IN ULONG64 Value );

往存储器写一个四字数据。

**2** 连续字节、字、双字、四字的写

(1) VOID 

WRITE_REGISTER_BUFFER_UCHAR(

IN PUCHAR Register,

IN PUCHAR Buffer,

IN ULONG  Count

);

往存储器连续写多个字节数据。

Register: 存储器地址

Buffer: 写数据缓冲区地址指针

Count: 缩写字节数

(2)VOID

WRITE_REGISTER_BUFFER_USHORT(

IN PUSHORT Register,

IN PUSHORT Buffer,

IN ULONG  Count

);

往存储器连续写多个字数据。

(3)VOID

WRITE_REGISTER_BUFFER_ULONG(

IN PULONG Register,

IN PULONG Buffer,

IN ULONG  Count

);

往存储器连续写多个双字数据。

(4)VOID

WRITE_REGISTER_BUFFER_ULONG64(

IN PULONG64 Register,

IN PULONG64 Buffer,

IN ULONG  Count

);

往存储器连续写多个四字数据。

**3**单个字节、字、双字的读

(1) VOID READ_REGISTER_UCHAR ( IN PUCHAR Register, IN UCHAR Value );

往存储器读一个字节数据。

(2) VOID READ_REGISTER_USHORT( IN PUSHORT Register, IN USHORT Value );

往存储器读一个字数据。

(3)VOID READ_REGISTER_ULONG( IN PULONG Register, IN ULONG Value );

往存储器连续读双字数据。

(4)VOID READ_REGISTER_ULONG64( IN PULONG64 Register, IN ULONG64 Value );

往存储器读一个四字数据。

**4** 连续字节、字、双字的读

(1) VOID 

READ_REGISTER_BUFFER_UCHAR(

IN PUCHAR Register,

IN PUCHAR Buffer,

IN ULONG  Count

);

从存储器连续读多个字节数据。

Register: 存储器地址

Buffer: 读数据缓冲区地址指针

Count: 所读字节数

(2)VOID

READ_REGISTER_BUFFER_USHORT(

IN PUSHORT Register,

IN PUSHORT Buffer,

IN ULONG  Count

);

从存储器连续读多个字数据。

(3)VOID

READ_REGISTER_BUFFER_ULONG(

IN PULONG Register,

IN PULONG Buffer,

IN ULONG  Count

);

从存储器连续读多个双字数据。

(4)VOID

READ_REGISTER_BUFFER_ULONG64(

IN PULONG64 Register,

IN PULONG64 Buffer,

IN ULONG  Count

);

从存储器连续读多个四字数据。

1.3 硬件访问编程

(1)根据配置定义地址指针

对于所设计的 PCI 设备,其配置空间的基地址寄存器(0~5)值决定请求的地址空间类型和大小,驱动程序根据以上情况定义地址指针。比如 PCISample 实例的设备卡,基地址寄存器 0 和基地址寄存器 1 分别

固定用于 CY7C09449 芯片的操作寄存器的内存映射地址和 I/O 映射地址。

在设备环境变量结构中声明地址指针。

typedef struct _DEVICE_CONTEXT{

PVOID MemBaseAddress; //内存映射地址

ULLONG MemLength;

PVOID IoBaseAddress;//I/O 映射地址

ULONG IoLength;

}DEVICE_CONTEXT,*PDEVICE_CONTEXT;

(2)在 EvtDevicePrepareHardware 例程中,初始化地址指针。

在次例程中,用 WdfCmResourceListGetCount 函数获取配置资源的个数,用 WdfCmResourceListGetDescriptor 函数获取该资源的描述符,其类型属性 Type 包括 CmResourceTypePort(IO 端口)、CmResourceTypePort

(存储器)、CmResourceTypeInterrupt(中断)等。对于 IO 端口,将首地址指针和空间大小值保存起来;对于存储器地址,要先用 MmMapIoSpace 函数将物理地址转换成系统内核模式地址,然后保存起来;对于中断,不作处

理。

NTSTATUS

PCISample_EvtDevicePrepareHardware(

    IN WDFDEVICE Device,

    IN WDFCMRESLIST ResourceList,

    IN WDFCMRESLIST ResourceListTranslated

    )

{

    PDEVICE_CONTEXT pDeviceContext;

ULONG i;

PCM_PARTIAL_RESOURCE_DESCRIPTOR descriptor;

    PAGED_CODE();

   DbgPrint("EvtDevicePrepareHardware - begins\n");

    pDeviceContext = GetDeviceContext(Device);

pDeviceContext->MemBaseAddress = NULL;

    //

    // Parse the resource list and save the resource information.

    //

    for (i=0; i < WdfCmResourceListGetCount(ResourceListTranslated); i++) {

       descriptor = WdfCmResourceListGetDescriptor(ResourceListTranslated, i);

       switch (descriptor->Type) {

       case CmResourceTypeMemory:

//MmMapIoSpace将物理地址转换成系统内核模式地址

          pDeviceContext->MemBaseAddress = MmMapIoSpace(

                      descriptor->u.Memory.Start,

                      descriptor->u.Memory.Length,

                       MmNonCached);

pDeviceContext->MemLength = descriptor->u.Memory.Length;

          break;

       default:

          break;

       }

    }

   DbgPrint("EvtDevicePrepareHardware - ends\n");

    return STATUS_SUCCESS;

}

(3)在 EvtDeviceReleaseHardware 例程中,对于存储器地址,要用 MmUnmapIoSpace 函数解除物理地址与系统内核模式地址的关联。

NTSTATUS

PCISample_EvtDeviceReleaseHardware(

    IN WDFDEVICE Device,

    IN WDFCMRESLIST ResourceListTranslated

    )

{

    PDEVICE_CONTEXT pDeviceContext;

    PAGED_CODE();

   DbgPrint("EvtDeviceReleaseHardware - begins\n");

    pDeviceContext = GetDeviceContext(Device);

if(pDeviceContext->MemBaseAddress) {

//MmUnmapIoSpace解除物理地址与系统内核模式地址的关联

MmUnmapIoSpace(pDeviceContext->MemBaseAddress, pDeviceContext->MemLength);

pDeviceContext->MemBaseAddress = NULL;

}

DbgPrint("EvtDeviceReleaseHardware - ends\n");

return STATUS_SUCCESS;

}

2. 中断处理

2.1 WDFINITERRUPT 处理

WDFINTERRUPT 对象实现硬件中断的处理。

中断服务例程的处理时间应该尽可能的短,并且由于中断服务例程在 DIRQL 级别上运行,很多函数不能调用。所以通常在中断服务例程中,若判断该中断是由自己的设备产生的,

则调用一个在 DISPATCH_LEVEL 级别上运行的延迟过程调用。当一个中断服务例程完成后,一旦处理器获得 DISPATCH_LEVEL 级别的运行权,内核就运行它的延迟过程调用,可以在延

迟过程调用例程中做大部分的中断处理工作。

中断服务例程是异步调用的,所以必须考虑其他例程和中断服务例程对共享数据的保护性访问。有两种方法:一种是获取硬件中断自旋锁,他提升程序的运行级别至 DIRQL ,访问

共享数据后,应释放硬件中断自旋锁;另一种是中断同步调用例程,也可实现其他例程中断服务例程对共享数据的串行访问。中断同步例程也在 DIRQL 级别上运行,执行时间应当尽可

能的短。

中断的配置结构是 WDF_INTERRUPT_CONFIG ,其定义如下:

typedef struct _WDF_INTERRUPT_CONFIG {

    ULONG            Size;

    //

    // If this interrupt is to be synchronized with other interrupt(s) assigned

    // to the same WDFDEVICE, create a WDFSPINLOCK and assign it to each of the

    // WDFINTERRUPTs config.

    //

    WDFSPINLOCK       SpinLock;

    WDF_TRI_STATE     ShareVector;

    BOOLEAN          FloatingSave;

    //

    // Automatic Serialization of the DpcForIsr

    //

    BOOLEAN          AutomaticSerialization;

    // Event Callbacks

    PFN_WDF_INTERRUPT_ISR        EvtInterruptIsr;

    PFN_WDF_INTERRUPT_DPC        EvtInterruptDpc;

    PFN_WDF_INTERRUPT_ENABLE     EvtInterruptEnable;

   PFN_WDF_INTERRUPT_DISABLE    EvtInterruptDisable;

} WDF_INTERRUPT_CONFIG, *PWDF_INTERRUPT_CONFIG;

SpinLock: 中断自旋锁。如果不设置,则框架采用一个内部自旋锁对象。获取硬件中断自旋锁,用的就是此锁。

AutomaticSerialization: 若为 TRUE ,则延迟过程调用的执行与父对象的其他例程同步。

EvtInterruptIsr: 中断服务例程。

EvtInterruptDpc: 延迟过程调用。

EvtInterruptEnable: 中断使能例程,运行级别为 DIRQL 。

EvtInterruptDisable: 中断禁止例程,运行级别为 DIRQL 。

中断配置初始化函数如下:

VOID 

WDF_INTERRUPT_CONFIG_INIT(

__out PWDF_INTERRUPT_CONFIG Configuration,

    __in PFN_WDF_INTERRUPT_ISR EvtInterruptIsr,

    __in_opt PFN_WDF_INTERRUPT_DPC EvtInterruptDpc

)

中断运行级别是任意 IRQL 。

下面介绍其函数。

(1) NTSTATUS

WdfInterruptCreate(

__in

    WDFDEVICE Device,

    __in

    PWDF_INTERRUPT_CONFIG Configuration,

    __in_opt

    PWDF_OBJECT_ATTRIBUTES Attributes,

    __out

    WDFINTERRUPT* Interrupt

);

创建中断对象。

(2)VOID 

WdfInterruptAcquireLock(IN WDFINTERRUPT Interrupt);

获取硬件中断自旋锁。

(3)VOID 

WdfInterruptReleaseLock(IN WDFINTERRUPT Interrupt);

释放硬件中断自旋锁, IRQL = DIRQL 。

(4) BOOLEAN

WdfInterruptQueueDpcForIsr(IN WDFINTERRUPT Interrupt);

排队已注册的硬件中断的延迟过程调用, IRQL <= DIRQL 。

(5)WDFDEVICE

WdfInterruptGetDevice(IN WDFINTERRUPT Interrupt);

获取中断的设备对象, IRQL <= DIRQL 。

(6)BOOLEAN

WdfInterruptSynchronize(

__in

    WDFINTERRUPT Interrupt,

    __in

   PFN_WDF_INTERRUPT_SYNCHRONIZE Callback,

    __in

    WDFCONTEXT Context

);

调用中断同步例程,返回的是中断同步例程的返回值。

Callback: 中断同步例程地址。

Context: 传递给中断同步例程的环境参数指针。

中断同步例程格式如下:

BOOLEAN

(*PFN_WDF_INTERRUPT_SYNCHRONIZE)(

__in

    WDFINTERRUPT Interrupt,

__in

    WDFCONTEXT Context

);

2.2 中断处理编程

(1)在设备环境变量结构中声明中断对象。

获取硬件中断自旋锁时需要。

typedef struct _DEVICE_CONTEXT{

WDFINTERRUPT Interrupt;

}DEVICE_CONTEXT,*PDEVICE_CONTEXT;

(2)在 EvtDeviceAdd 例程中,创建中断对象。

//设置中断服务例程和延迟过程调用

WDF_INTERRUPT_CONFIG_INIT(&interrupt,PCISample_EvtInterruptIsr,PCISample_EvtInterruptDpc);

//创建中断对象

status = WdfInterruptCreate(device,

&interruptConfig,

WDF_NO_OBJECT_ATTRIBUTES,

&pDeviceContext->Interrupt);

(3)中断服务例程编程

在中断服务例程中,首先判断该中断是否是自己的设备产生的,若不是,则返回 FALSE ;若是,则进行必要的处理,排队中断延迟过程调用,然后返回 TRUE 。

3. DMA 传输

3.1 DMA 编程对象

KMDF 提供了3个对象: WDFDMAENABLER 、 WDFDMATRANSACTION 和 WDFCOMMONBUFFER 对象,用于实现 DMA 操作。 WDFDMAENABLER 对象用于建立一个 DMA 适配器,

它说明 DMA 通道的特性; WDFDMATRANSACTION 对象用于 DMA 传输控制; WDFCOMMONBUFFER 对象用于申请系统提供的公用缓冲区。

3.1.1 WDFDMAENABLER 对象

对于 DMA 传输,驱动程序需创建一个 DMA 适配器对象,它标明一个 DMA 通道的特性和提供串行化访问的服务。

WDFDMAENABLER 对象实现对 DMA 适配器的操作。

WDFDMAENABLER 对象的配置结构是 WDF_DMA_ENABLER_CONFIG ,其定义如下:

typedef struct _WDF_DMA_ENABLER_CONFIG {

    //

    // Size of this structure in bytes

    //

    ULONG             Size;

    //

    // One of the above WDF_DMA_PROFILES

    //

    WDF_DMA_PROFILE     Profile;

    //

    // Maximum DMA Transfer handled in bytes.

    //

    size_t             MaximumLength;

    //

    // The various DMA PnP/Power event callbacks

    //

    PFN_WDF_DMA_ENABLER_FILL               EvtDmaEnablerFill;

   PFN_WDF_DMA_ENABLER_FLUSH              EvtDmaEnablerFlush;

   PFN_WDF_DMA_ENABLER_DISABLE             EvtDmaEnablerDisable;

   PFN_WDF_DMA_ENABLER_ENABLE             EvtDmaEnablerEnable;

   PFN_WDF_DMA_ENABLER_SELFMANAGED_IO_START  EvtDmaEnablerSelfManagedIoStart;

   PFN_WDF_DMA_ENABLER_SELFMANAGED_IO_STOP  EvtDmaEnablerSelfManagedIoStop;

} WDF_DMA_ENABLER_CONFIG, *PWDF_DMA_ENABLER_CONFIG;

Profile: DMA 通道的特性;如下所示:

KMDF DMA Profile 及其意义

名称 64 位地址寻址能力 支持设备硬件分散/聚集 支持同时读和写操作

WdfDmaProfilePacket NO NO NO

WdfDmaProfileScatterGather NO Yes NO

WdfDmaProfileScatterGatherDuplex NO Yes Yes

WdfDmaProfilePacket64 Yes NO NO

WdfDmaProfileScatterGather64 Yes Yes NO

WdfDmaProfileScatterGatherDuplex64 Yes Yes Yes

MaximumLength: 单个传输的最大长度,小于 65536 。

EvtDmaEnablerFill: 可用于申请 DMA 缓存区的例程。

EvtDmaEnablerFlush: 可用于释放 DMA 缓存区的例程。

EvtDmaEnablerDisable: 在设备退出工作电源 D0 之前,禁止 DMA 操作的例程。

EvtDmaEnablerEnable: 在设备进入工作电源 D0 之后,允许 DMA 操作的例程。

EvtDmaEnablerSelfManagedIoStart: DMA 设备管理的 I/O 启动操作例程。

EvtDmaEnablerSelfManagedIoStop: DMA 设备管理的 I/O 停止操作例程。

配置初始化函数如下:

VOID 

WDF_DMA_ENABLER_CONFIG_INIT(

__out PWDF_DMA_ENABLER_CONFIG Config,

    __in  WDF_DMA_PROFILE   Profile,

    __in  size_t           MaximumLength

);

PASSIVE_LEVEL 中断级别调用。

(1)NTSTATUS

WdfDmaEnablerCreate(

__in

    WDFDEVICE Device,

    __in

    PWDF_DMA_ENABLER_CONFIG Config,

    __in_opt

    PWDF_OBJECT_ATTRIBUTES Attributes,

    __out

    WDFDMAENABLER* DmaEnablerHandle

);

创建 WDFDMAENABLER 对象, PASSIVE_LEVEL 中断级别调用。

(2)VOID 

WdfDmaEnablerSetMaximumScatterGatherElements(

__in

    WDFDMAENABLER DmaEnabler,

    __in

    size_t MaximumFragments

);

设置设备支持的 DMA 分散/聚集单元数, PASSIVE_LEVEL 中断级别调用。

MaximumFragments: DMA 分散/聚集单元数。

3.1.2 WDFDMATRANSACTION 对象

WDFDMATRANSACTION 对象控制 DMA 的传输。

启动一个 DMA 传输、获取 DMA 传输数据缓冲区物理地址和传输字节数,以及 DMA 传输结束后数据处理,这些工作都是由 WDFDMATRANSACTION 对象实现的。

(1)NTSTATUS

WdfDmaTransactionCreate(

__in

    WDFDMAENABLER DmaEnabler,

    __in_opt

    PWDF_OBJECT_ATTRIBUTES Attributes,

    __out

    WDFDMATRANSACTION* DmaTransaction

);

创建WDFDMATRANSACTION 对象。

(2)NTSTATUS

WdfDmaTransactionInitializeUsingRequest(

__in

    WDFDMATRANSACTION DmaTransaction,

    __in

    WDFREQUEST Request,

    __in

    PFN_WDF_PROGRAM_DMA EvtProgramDmaFunction,

    __in

    WDF_DMA_DIRECTION DmaDirection

);

利用 I/O 请求初始化 DMA 传输,这样 I/O 请求缓冲区将作为 DMA 存储区使用。

DmaTransaction: 相关的 WDFDMATRANSACTION 对象。

Request: I/O 请求对象。

EvtProgramDmaFunction: 硬件 DMA 编程例程,执行 WdfDmaTransactionExecute函数后,系统将调用该例程。

DmaDirection: DMA 传输方向,分读操作(WdfDmaDirectionReadFromDevice)和写操作(WdfDmaDirectionWriteToDevice)

(3)NTSTATUS

WdfDmaTransactionInitialize(

__in

    WDFDMATRANSACTION DmaTransaction,

    __in

    PFN_WDF_PROGRAM_DMA EvtProgramDmaFunction,

    __in

    WDF_DMA_DIRECTION DmaDirection,

    __in

    PMDL Mdl,

    __in

    PVOID VirtualAddress,

    __in

    __drv_when(Length == 0, __drv_reportError(Length cannot be zero))

    size_t Length

);

初始化 DMA 传输,用公用缓冲区做为 DMA 存储区时,需要用该函数。

Mdl: 用作 DMA 存储区的 Mdl。

VirtualAddress: 用作 DMA 存储区的内核模式地址。

Length: 用作 DMA 存储区的字节长度。

(4)NTSTATUS

WdfDmaTransactionExecute(

__in

    WDFDMATRANSACTION DmaTransaction,

    __in_opt

    WDFCONTEXT Context

);

执行 DMA 传输。

Context: 传递给 EvtProgramDmaFunction 函数的环境变量指针。

一旦执行 DMA 传输后,系统将调用 DMA 编程例程,其格式如下:

BOOLEAN

(*PFN_WDF_PROGRAM_DMA)(

__in

WDFDMATRANSACTION DmaTransaction,

__in

WDFDEVICE Device,

__in PVOID  

Context,

__in

WDF_DMA_DIRECTION Direction,

__in

PSCATTER_GATHER_LIST SgList

);

SgList: 用作 DMA 传输的存储区列表。其结构定义如下:

typedef struct _SCATTER_GATHER_LIST{

ULONG NumberOfElements;

ULONG_PTR Reserved;

SCATTER_GATHER_ELEMENT  Elements[];

}SCATTER_GATHER_LIST,PSCATTER_GATHER_LIST;

NumberOfElements: 存储区单元的数目。若不支持分散/聚集 DMA,则存储区单元的数目为1。

Elements: 存储区单元的描述符数组。每个存储单元的描述符结构定义如下:

typedef struct _SCATTER_GATHER_ELEMENT{

PHYSICAL_ADDRESS Address; //存储区的物理地址

ULONG Length; //存储区的字节长度

ULONG_PTR Reserved;

}SCATTER_GATHER_ELEMENT,*PSCATTER_GATHER_ELEMENT;

(5)NTSTATUS

WdfDmaTransactionRelease(

__in

    WDFDMATRANSACTION DmaTransaction

);

中止 DMA 传输。

(6)WDFREQUEST

WdfDmaTransactionGetRequest(

__in

    WDFDMATRANSACTION DmaTransaction

);

返回 DMA 传输的 I/O 请求对象。

(7)BOOLEAN

WdfDmaTransactionDmaCompleted(

__in

    WDFDMATRANSACTION DmaTransaction,

    __out

    NTSTATUS* Status

);

测试 DMA 传输是否完成。返回 TRUE 表示已完成;返回 FALSE 且 Status 为 STATUS_MORE_PROCESSING_REQ ,表示 DMA 传输还没完成。

(8)BOOLEAN

WdfDmaTransactionDmaCompletedWithLength(

__in

    WDFDMATRANSACTION DmaTransaction,

    __in

    size_t TransferredLength,

    __out

    NTSTATUS* Status

);

测试 DMA 传输是否完成。返回 TRUE 表示已完成;返回 FALSE 且 Status 为 STATUS_MORE_PROCESSING_REQ ,表示 DMA 传输还没完成。

TransferredLength: 已传输的字节数。

(9)BOOLEAN

WdfDmaTransactionDmaCompletedFinal(

__in

    WDFDMATRANSACTION DmaTransaction,

    __in

    size_t FinalTransferredLength,

    __out

    NTSTATUS* Status

);

强行完成 DMA 传输。输入参数无效时,返回 FALSE 。

FinalTransferredLength: 已传输的字节数。

(10)size_t

WdfDmaTransactionGetBytesTransferred(

__in

    WDFDMATRANSACTION DmaTransaction

);

返回 DMA 传输的总字节数。

(11)size_t

WdfDmaTransactionGetCurrentDmaTransferLength(

__in

    WDFDMATRANSACTION DmaTransaction

);

返回当前 DMA 传输的字节数。

(12)WDFDEVICE

WdfDmaTransactionGetDevice(

__in

    WDFDMATRANSACTION DmaTransaction

);

返回 DMA 传输的设备对象。

(13)VOID 

WdfDmaTransactionSetMaximumLength(

__in

    WDFDMATRANSACTION DmaTransaction,

    __in

    size_t MaximumLength

);

设置 DMA 传输的最大字节数,应小于 65536 。当此值小于 WDF_DMA_ENABLER_CONFIG_INIT 中所设置的 DMA 适配器的最大字节数时,该数据有效。

3.1.3 WDFCOMMONBUFFER 对象

对于 DMA 操作,系统提供了一个特殊的内存,即物理上连续的内存,称为公用缓冲区。

公用缓冲区是稀有的系统资源,应该避免浪费使用。对于支持分散/聚集 DMA 的设备,因为其并不要求在物理没存上连续的内存,因此可不使用公用缓冲区。

WDFCOMMONBUFFER 对象实现对公用缓冲区的操作。

(1)NTSTATUS

WdfCommonBufferCreate(

__in

    WDFDMAENABLER DmaEnabler,

    __in

    __drv_when(Length == 0, __drv_reportError(Length cannot be zero))

    size_t Length,

    __in_opt

    PWDF_OBJECT_ATTRIBUTES Attributes,

    __out

    WDFCOMMONBUFFER* CommonBuffer

);

创建 WDFCOMMONBUFFER 对象, PASSIVE_LEVEL 中断级别调用。

其地址边界的设置由下面函数设定,若不调用此函数,则以字对齐。

VOID 

WdfDeviceSetAlignmentRequirement(

__in

    WDFDEVICE Device,

    __in

    ULONG AlignmentRequirement

);

地址边界有如下定义:

#define  FILE_BYTE_ALIGNMENT 0x00000000  //字节对齐

#define  FILE_WORD_ALIGNMENT 0x00000001  //字对齐

#define  FILE_LONG_ALIGNMENT 0x00000003  //4字节对齐

#define  FILE_QUAD_ALIGNMENT 0x00000007  //8字节对齐

#define  FILE_OCTA_ALIGNMENT 0x0000000F  //16字节对齐

#define  FILE_32_BYTE_ALIGNMENT 0x0000001F  //32字节对齐

#define  FILE_64_BYTE_ALIGNMENT 0x0000003F  //64字节对齐

#define  FILE_128_BYTE_ALIGNMENT 0x0000007F  //128字节对齐

#define  FILE_256_BYTE_ALIGNMENT 0x000000FF  //256字节对齐

#define  FILE_512_BYTE_ALIGNMENT 0x000001FF  //512字节对齐

(2)NTSTATUS

WdfCommonBufferCreateWithConfig(

__in

    WDFDMAENABLER DmaEnabler,

    __in

    __drv_when(Length == 0, __drv_reportError(Length cannot be zero))

    size_t Length,

    __in

   PWDF_COMMON_BUFFER_CONFIG Config,

    __in_opt

    PWDF_OBJECT_ATTRIBUTES Attributes,

    __out

    WDFCOMMONBUFFER* CommonBuffer

);

创建 WDFCOMMONBUFFER 对象,但对地址边界有请求, PASSIVE_LEVEL 中断级别调用。

WDF_COMMON_BUFFER_CONFIG 的结构如下:

typedef struct _WDF_COMMON_BUFFER_CONFIG {

    //

    // Size of this structure in bytes

    //

    ULONG  Size;

    //

    // Alignment requirement of the buffer address

    //

    ULONG  AlignmentRequirement;

} WDF_COMMON_BUFFER_CONFIG, *PWDF_COMMON_BUFFER_CONFIG;

初始化地址边界函数如下:

VOID 

WDF_COMMON_BUFFER_CONFIG_INIT(

__out PWDF_COMMON_BUFFER_CONFIG Config,

    __in  ULONG  AlignmentRequirement

);

(3)PHYSICAL_ADDRESS

WdfCommonBufferGetAlignedLogicalAddress(

__in

    WDFCOMMONBUFFER CommonBuffer

);

返回缓冲区的物理地址。

(4)PVOID 

WdfCommonBufferGetAlignedVirtualAddress(

__in

    WDFCOMMONBUFFER CommonBuffer

);

返回缓冲区的内核模式地址。

(5)size_t

WdfCommonBufferGetLength(

__in

    WDFCOMMONBUFFER CommonBuffer

);

返回缓冲区的字节长度。

3.2 DMA 传输编程

(1)在设备环境变量结构中声明 DMA 编程的3个对象。

typedef struct _DEVICE_CONTEXT{

WDFDMAENABLER DmaEnabler;

WDFCOMMONBUFFER CommonBuffer;

PVOID CommonBufferBase;

PHYSICAL_ADDRESS CommonBufferBaseLA;

WDFDMATRANSACTION DmaTransaction;

}DEVICE_CONTEXT,*PDEVICE_CONTEXT;

(2)在 EvtDeviceAdd 例程中,创建用于 DMA 编程的 3 个对象。

(3)DMA 传输编程

①当有 I/O 请求时,在 EvtIoxxx 例程中,用 WdfDmaTransactionInitializeUsingRequest 或 WdfDmaTransactionInitialize 函数初始化 DMA 传输;调用 WdfDmaTransactionExecute

函数,启动 DMA 编程。

②在 EvtProgramDmaDMA 编程中,对硬件的 DMA 操作寄存器进行编程。

③当 DMA 传输中断发生时,在 EvtInterruptDPC 例程中,测试 DMA 传输是否结束,没有则调用 WdfDmaTransactionExecute 函数,继续启动 DMA 编程;若 DMA 传输结束,则完成 I/O

请求。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值