目录:
1. 第一部分 编译安装测试一个简单的WDF驱动程序
1.1 编译安装测试
1.2 Windows驱动开发就是要开发出INF文件和SYS文件
1.3 简单介绍下编译脚本
1.4 Windows应用程序如何访问一个设备
2. 第二部分 了解WDF框架并且开发WDF驱动程序
2.1 驱动程序的加载
2.2 系统检测到新硬件的时候干些什么?
2.3 WDF中读写和控制设备
3. 第三部分 后记
第一部分 编译安装并且测试一个简单的WDF驱动程序。
对于初学者,我们需要一个简单的例子,就和C语言里面的HelloWorld一样,编译运行,接着打印出"Hello world!"。我们要先建立起对WDF驱动的一个初步而强烈的感性认识,然后再对照着例子来学习WDF的概念,看它的代码是怎么实现的,这样就会有深刻的认识。这就是教育学上所谓的循序渐进。按照这个思路,我们就先要编译安装运行一个简单驱动程序例子。我浏览了下WDF的例子之后,发现Echo这个例子比较适合我们的这个思路。下面就开始编译、安装和运行Echo这个例子。我是在XP下面做的实验,如果在其他操作系统下,也类似。在开始试验之前,读者可以从微软的网站下载WDK开发包,大小约700Mbytes,需要耐心地下才能下完。下文中,如果读者不知道我说的文件或程序在哪个目录下面,请搜索下,我尽量说详细些。
1.1 编译安装测试
(1) 编译Echo这个WDF例子。
在开始菜单中选择X86 Free Build Environment。
命令行界面出现之后,用cd切换到/WinDDK/7600.16385.1/src/general/echo/kmdf目录下。
命令行里面运行,"build -ceZ"。
可以看到生成了echo.inf, echo.sys这两个文件。后面安装驱动程序的时候要用到它俩。
编译完毕。
(2) 安装。
拷贝devcon.exe, WdfCoInstaller01009.dll, echo.inf, echo.sys四个文件到同一个目录下。
在命令行下进入该目录并运行,devcon.exe install echo.inf "root/echo"
命令行中出现"Driver installed successfully"。
安装完毕。
(3) 察看。
在"我的电脑"->右键"属性"->"硬件"->"设备管理器"里面,可以看见Sample Device已经安装好了。如图,因为我安装了2次,所有这里有两个Sample WDF ECHO Driver。如果安装3次,就有3个。
(4) 试验。
在开始菜单中选择X86 Free Build Environment。
在命令行中用cd切换到D:/WinDDK/7600.16385.1/src/general/echo/exe目录下面。
在命令行中运行, "build -ceZ"。
再cd到D:/WinDDK/7600.16385.1/src/general/echo/exe/objfre_wxp_x86/i386下面。
在命令行中运行, "echoapp.exe"。出现下面的图就说明,程序正确工作了。
使用上述的步骤,可以编译和安装大部分的WDF驱动例子。WDK中提供的这些WDF例子可以作为我们开发驱动程序的原始代码。
1.2 Windows驱动开发就是要开发出INF文件和SYS文件
实验之后,读者可能会问几个问题devcon.exe, WdfCoInstaller01009.dll, echo.inf, echo.sys是分别用来做什么用的?答案如下。
echo.sys文件是真正的驱动程序,是一个DLL文件,但是以sys作为文件后缀名。
echo.inf文件是一个存放着安装信息的文本文件。由Windows的SetupAPI调用echo.inf里面的内建命令和配置信息,完成安装过程。过程有点类似于shell程序调用脚本运行。
WdfCoInstaller01009.dll是Co-Installer, 用于协助安装的程序,以dll的形式提供。
devcon.exe是一个命令行工具,可以用来显示设备信息、寻找设备、安装和改动设备、重启电脑等。
devcon.exe和WdfCoInstaller01009.dll是WDK自带的,而echo.inf和echo.sys是需要驱动程序开发者提供的。你要为某个设备开发一个驱动程序,那你最终提供的就是inf和sys文件。所以掌握了inf文件和sys文件的写法,就掌握了驱动程序的开发方法。sys文件的开发实际上就是驱动程序本身的开发,这是本文重点,所以在后面详述。而inf文件,很多应用程序安装的时候也会用到,有兴趣的读者可以在网上google下,有很多中文资料;WDF文档中也有描述和很多例子,所以我在这里不对其展开介绍。(后面如果有时间,我可能会写一篇关于inf的介绍性文章)。读者要知道,安装驱动程序一定是需要inf文件的。
1.3 简单介绍下编译脚本
既然要编译程序,就要涉及到编译器和其相关的工程文件。WDK使用Build程序来编译。Build程序要使用makefile和sources文件。用户没有必要从自己写一个makefile和sources,因为WDF例子里面就有很多现成脚本,可以用作参考,改改就能用。
从KMDF的echo例子可以看到,makefile如果不是特别需要的话,就不需要改了。而Sources文件里面的几个地方需要改下。
TARGETNAME=echo <---改为自己想要的驱动程序名称
INF_NAME=echo <---改为自己想要的inf文件名称
SOURCES=driver.c / <---改为需要编译的c文件名
device.c /
queue.c
1.4 Windows应用程序如何访问一个设备
先了解windows设备树的概念。
当系统启动起来的时候,系统(主要是PnP管理器)会搜集各个硬件的信息,然后生成一个设备树。在Windows的"设备管理器"里面可以看到整个设备树的组织情况。设备树立面的每一个节点都代表了一个设备,叫做设备节点或者devnode。WDF的device对象正是对应着这样的一个节点。每当有硬件加载或者卸载的时候,设备树上相应的节点就会添加或者删除。为了访问一个设备,可以用枚举的方法顺藤摸瓜,在设备树上找到该设备。也可以根据设备注册在系统中的ID直接找到设备。echoapp.exe使用的是后面的方法,代码如下。
DevicePath = GetDevicePath((LPGUID)&GUID_DEVINTERFACE_ECHO);
printf("DevicePath: %s/n", DevicePath);
找到了设备在设备树上的路径,也就找到了设备。Windows下面对设备的操作被抽象为对文件的操作,所以在对设备进行操作之前,先要根据设备路径来创建设备文件对象。以后对设备文件的读写和控制,就代表了对设备的读写和控制。创建设备文件对象的代码如下。
hDevice = CreateFile(DevicePath,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
0,
NULL );
读写和控制,分别是用这三个API函数。ReadFile(), WriteFile(), DeviceIoControl()。当然,最后还要用CloseHandle()关闭设备文件对象。
好奇的读者,还会进一步问,系统是怎么把这些读写命令传递给目标设备的?这正是WDF框架做的事情,我们下面正式开始讨论WDF框架和WDF驱动程序的开发。
第二部分 了解WDF框架并且开发WDF驱动程序
2.1 驱动程序的加载
驱动程序什么时候被加载?加载时会做些什么?
通常Windows启动的时候会加载驱动程序,这个时候会调用驱动程序里面的DriverEntry()函数。这是整个驱动程序内部最早运行的函数,它主要是做一些驱动程序运行的准备工作。其中最主要是调用了WdfDriverCreate()函数初始化了一个driver对象。
WDF使用WDF_DRIVER_CONFIG结构来初始化driver对象。于是初始化好之后,driver对象就有了WDF_DRIVER_CONFIG预定义好的事件响应函数。WDF_DRIVER_CONFIG结构的定义如下:
typedef struct _WDF_DRIVER_CONFIG {
ULONG Size;
PFN_WDF_DRIVER_DEVICE_ADD EvtDriverDeviceAdd;
PFN_WDF_DRIVER_UNLOAD EvtDriverUnload;
ULONG DriverInitFlags;
ULONG DriverPoolTag;
} WDF_DRIVER_CONFIG, *PWDF_DRIVER_CONFIG;
注意第二个成员变量是一个EvtDriverDeviceAdd事件回调函数指针。当系统监测到有新硬件存在的时候,就会调用该硬件的驱动程序的WDF_DRIVER_CONFIG变量里面的EvtDriverDeviceAdd指针指向的函数。WdfDriverCreate()只初始化driver对象。而在EvtDriverDeviceAdd指向的事件回调函数里面,才是驱动程序的资源(队列、中断等)初始化的地方。这说明系统中有硬件了,才会分配资源;没有硬件,就不会分配相关资源。
下面是示意代码。
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
WDF_DRIVER_CONFIG config;
NTSTATUS status = STATUS_SUCCESS;
WDF_DRIVER_CONFIG_INIT(
&config,
MyEvtDeviceAdd //发现新硬件的事件回调函数
);
config.EvtDriverUnload = MyEvtDriverUnload;
status = WdfDriverCreate(
DriverObject,
RegistryPath,
WDF_NO_OBJECT_ATTRIBUTES,
&config, //这是WDF_DRIVER_CONFIG类型的变量
WDF_NO_HANDLE
);
if (!NT_SUCCESS(status)) {
TraceEvents(
TRACE_LEVEL_ERROR,
DBG_PNP,
"WdfDriverCreate failed with status %!STATUS!",
status
);
}
return status;
}
注意WdfDriverCreate()的第四个参数是一个WDF_DRIVER_CONFIG类型的变量config。config的第二个参数是一个EvtDriverDeviceAdd回调函数指针,具体指向的函数是MyEvtDeviceAdd()函数。一旦系统监测到相关硬件的时候,就会调用MyEvtDeviceAdd()函数。并且在MyEvtDeviceAdd()里面做各种具体初始化操作。
2.2 系统检测到新硬件的时候干些什么?
在EvtDriverDeviceAdd指向的MyEvtDeviceAdd()具体要做哪些初始化工作呢?
让我们简单回想下驱动程序的作用。驱动程序在系统中起到的是承上启下的部分,主要作用有两点。一是承上,为系统中上层的软件(驱动程序或者应用程序)提供API接口。上层软件调用API访问驱动和下面的硬件提供的功能。二是启下,管理底层的硬件,包括初始化、读写和控制等。为了提高访问效率,还会支持中断、DMA等方式。这个观点适用在所有操作系统中,不管是PC上的Linux、Windows, 还是嵌入式的uCos、FreeRTOS、VxWorks等的内部都这样。在有些不使用操作系统的嵌入式系统上也适用。与不使用操作系统的裸奔程序相比,操作系统会给用户提供很多机制和框架,比如内存管理、中断、与用户程序的API接口等。只要使用了操作系统提供的这些框架相关的代码,写出来的驱动程序就有很强的可移植性并且更可靠规范。而开发者付出的代价就是需要去学习操作系统的这些框架。
综上,在操作系统上运行的驱动程序由三部分的内容组成:承上、启下、操作系统的框架代码。
回到WDF来,可以看到MyEvtDeviceAdd()函数的初始化工作是利用了操作系统提供的框架代码,为承上启下这两个任务做准备的。主要任务有:
- 创建一个代表设备的对象。 [注:框架代码]
- 创建I/O队列,用于设备的读写和控制。 [注:启下的框架代码]
- 创建应用程序或者其他驱动访问本驱动程序的接口。[注:承上的框架代码]
- 为支持Windows Management Instrumentation (WMI)做一些初始化工作。[注:承上的框架代码]
- 如果用到了中断,就创建中端对象。 [注:启下的框架代码]
- 如果用到了DMA,就创建DMA对象。 [注:启下的框架代码]
简单总结下,就是创建设备对象,对下提供I/O队列和中断和DMA用于读写和控制硬件,对上提供应用程序和WMI接口。对于一个初学者,只要先掌握创建设备对象,创建I/O队列,就可以完成一个简单的驱动程序了。一旦建立起这些概念,可以举一返三地掌握其余的内容。
2.3 WDF中读写和控制设备
按照前一节的说明,首先要创建设备对象。示意代码如下:
NTSTATUS
MyEvtDeviceAdd(
IN WDFDRIVER Driver,
IN PWDFDEVICE_INIT DeviceInit
)
{
WDF_PNPPOWER_EVENT_CALLBACKS pnpPowerCallbacks;
WDF_OBJECT_ATTRIBUTES attributes;
NTSTATUS status;
WDFDEVICE device;
// Initialize the WDF_PNPPOWER_EVENT_CALLBACKS structure.
WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);
pnpPowerCallbacks.EvtDevicePrepareHardware = MyEvtDevicePrepareHardware;
pnpPowerCallbacks.EvtDeviceD0Entry = MyEvtDeviceD0Entry;
pnpPowerCallbacks.EvtDeviceD0Exit = MyEvtDeviceD0Exit;
WdfDeviceInitSetPnpPowerEventCallbacks(
DeviceInit,
&pnpPowerCallbacks
);
// This driver uses buffered I/O.
WdfDeviceInitSetIoType(
DeviceInit,
WdfDeviceIoBuffered
);
// Specify the device object's context space by
// using a driver-defined DEVICE_CONTEXT structure.
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(
&attributes,
DEVICE_CONTEXT
);
// Create the device object.
status = WdfDeviceCreate(
&DeviceInit,
&attributes,
&device
);
if (!NT_SUCCESS(status)) {
return status;
}
// ... 下面的代码是创建队列,此处省略,下面讲。
}
上面的代码先是设置好设备对象的各项初始化属性,然后调用WdfDeviceCreate()创建设备对象。其中的MyEvtDevicePrepareHardware()是设备硬件初始化函数,设备的硬件初始化的工作可以放在这里做。MyEvtDeviceD0Entry()和MyEvtDeviceD0Exit()分别是进入和退出D0状态时候的回调函数。
当一个Windows应用程序使用ReadFile()试图读取来自设备的数据的时候,Windows系统的I/O管理器和WDF的机制相配合,为这一次读操作创建出一个reqeust对象,并且把这个request对象放到设备的驱动程序的I/O queue里面。很快,WDF框架又会调用queue的EvtIoRead()函数,把数据读给应用程序。对于设备的写操作和控制操作,即EvtIoWrite()和EvtIoDeviceControl()回调函数,其过程也是类似的。
从ReadFile()到EvtIoRead()的这个流程实际上很简单,但是有时候驱动程序会创建多个queue,那选择哪个queue里面的EvtIoRead()呢?
如果只创建了一个queue,那么用WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE把这个queue设为默认的queue, 所有来在上层的request对象都被会被WDF框架放到这个默认queue中,等待处理。
如果创建了多个queue, 可以通过WdfDeviceConfigureRequestDispatching()函数来指定某个类型的queue处理某个类型的request。通过这个办法,WDF框架在接收到上层应用程序的request的时候,知道把这个request放到哪个queue里面去。
下面看一下创建队列的示意代码。注意这段代码还是在MyEvtDeviceAdd()函数里面的,位于创建设备对象的代码的后面。
NTSTATUS
MyEvtDeviceAdd(
IN WDFDRIVER Driver,
IN PWDFDEVICE_INIT DeviceInit
)
{
// ... 前面是创建设备对象的代码。
// 这是一个默认queue
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(
&queueConfig,
WdfIoQueueDispatchSequential
);
// 自定义了队列的读操作和写操作
queueConfig.EvtIoRead = EchoEvtIoRead;
queueConfig.EvtIoWrite = EchoEvtIoWrite;
// Fill in a callback for destroy, and our QUEUE_CONTEXT size
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, QUEUE_CONTEXT);
attributes.EvtDestroyCallback = EchoEvtIoQueueContextDestroy;
// 创建队列
status = WdfIoQueueCreate(
Device,
&queueConfig,
&attributes,
&queue
);
}
最后来看下具体的读写操作,通常就是对内存或者寄存器的读写。以Echo这个例子里面的EvtIoRead()函数中的代码为例。
// 得到request对象的memory
Status = WdfRequestRetrieveOutputMemory(Request, &memory);
if( !NT_SUCCESS(Status) ) {
KdPrint(("EchoEvtIoRead Could not get request memory buffer 0x%x/n",Status));
WdfVerifierDbgBreakPoint();
WdfRequestCompleteWithInformation(Request, Status, 0L);
return;
}
// 复制到输出memory
Status = WdfMemoryCopyFromBuffer( memory, // destination
0, // offset into the destination memory
queueContext->Buffer,
Length );
if( !NT_SUCCESS(Status) ) {
KdPrint(("EchoEvtIoRead: WdfMemoryCopyFromBuffer failed 0x%x/n", Status));
WdfRequestComplete(Request, Status);
return;
// 指明传输已经结束
WdfRequestSetInformation(Request, (ULONG_PTR)Length)
上面的代码就是把设备里面的数据queueContext->Buffer拷贝给输出内存(这个输出是指输出给应用程序的)memory中。最后通知系统传输已经结束了。EvtIoWrite()和EvtIoDeviceControl()之类的回调函数还是类似的,读者可以再次举一返三。比如,USB bulk驱动的话,就可以在EvtIoRead()和EvtIoWrite()中间作bulk read和bulk write的操作。
我们可以发现,WDF驱动的结构并不复杂。最开始只要关注WdfDriverCreate(),WdfDeviceCreate(), WdfIoQueueCreate(), EvtIoRead(), EvtIoWrite(), EvtIoDeviceControl()就可以了。后面有了实际的需要再往里面添加各个功能,比如为了加快速度采用中断/DMA, 为了同步而采用同步对象。
第三部分 后记
这篇文章本来是为我的USB设备开发驱动程序的时候整理的一些思路。WDK自带的WDF文档其实是最好的资料,不过太长了,不好好静下心来看上几天,估计是看不出门道的。希望我的这篇文章能过对WDF的初学者有帮助,能快速地让他们掌握WDF的全貌,快速地进入实际的项目开发中,而不是在工具的学习上花费太多时间。欢迎加我的QQ和我讨论。