从零开始学习Windows WDF驱动程序开发

摘要:本文通过简单的例子和精简的说明,带你快速了解和掌握基于WDF架构的Windows驱动程序的开发过程。WDF自带的文档非常详细而且冗长,读者需要一定的编程基础和英语水平,再加上一定的努力,才能看明白。而在网络上,写得既简单又清楚还具有操作性的资料几乎没有。本文就是一篇简洁明了并且有实际操作的教程,节约你宝贵的时间和脑细胞。即便你是从来没有接触过Window下面驱动程序开发的新手,也能让你快速建立起WDF开发的概念,顺利进入实际的Windows驱动程序项目开发中。

声明:本文是电子达人的原创文章,版权归电子达人所有。您可以转载,但是不能修改文章的内容。
电子达人网址:
http://blog.csdn.net/sjsj
QQ   : 1759120331
Email: 1759120331@qq.com

目录:
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和我讨论。

[]
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页