基于WDF框架的PCIE驱动设计

1.    概述
Windows平台下的设备驱动程序从Windows 2000开始都是以WDM ( Windows Driver Model) 框架为平台进行开发。以此模型开发,开发者需要一方面实现驱动程序与硬件的交互,另一方面要对操作系统内核进行操作,难度大。驱动程序容易出现问题,这也是Windows2000以来操作系统容易蓝屏的原因。
为了改善这种局面,降低驱动程序开发者的开发难度,提高系统稳定性,微软推出了新的驱动程序开发模型WDF。WDF对WDM进行了封装,将驱动程序中与操作系统交互的细节由框架实现。这样驱动程序就从内核中分离出来,开发者只需要专注处理与硬件的交互,简化了驱动程序的设计,提高了整个系统的可靠性和稳定性。
WDF是UMDF(User Mode Driver Framework,用户模式驱动程序框架)和KMDF(Kernel Mode Driver Framework,内核模式驱动程序框架)的总和。由于本项目基于PCIe硬件设备进行驱动开发,涉及到内存读写等内核操作,所以使用KMDF框架来编写驱动程序。
2.     Windows驱动程序
Windows是一个分层的操作系统,它的运行依赖于上层组件向下层组件的调用。驱动层提供设备驱动的基本功能函数,包括但不限于设备打开(OpenFile)、设备关闭(CloseFile)、数据读写(ReadFile/WriteFile)以及DeviceIoControl等。一个简化的I/O流模型如下图所示:
 


3.    WDF框架
WDF抽象的框架如下图所示:
 

WDF已经把驱动程序开发做了很好的封装,开发者只需要定义框架对象和编写事件回调函数。当我们注册好编写好的回调例程之后,当事件发生(设备插入、设备打开、I/O操作等)时,WDF会自动帮我们调用相关的例程。
4.    开发环境搭建
Windows 驱动程序开发工具包 (WDK) 与 Microsoft Visual Studio 和用于 Windows 驱动程序的调试工具相集成。该集成环境给开发者提供了开发、构建、打包、部署、测试和调试驱动程序时所需的工具。
本项目采用微软的驱动程序工具包为WDK8.1。WDK8.1 更新与 Microsoft Visual Studio2013 集成。开发者需要首先在微软的官方网站上下载并安装 Visual Studio 2013,然后安装WDK 8.1 更新。
5.    基于WDF框架开发PCIE驱动的一般流程
 


6.    设备通信接口
WDF驱动模型主要通过KMDF回调例程与WIN32函数的一一对应,实现驱动程序与应用程序的通信。数据传输任务主要在EvtIoRead、EvtIoWrite和EvtIoDeviceControl例程中完成,对于支持DMA传输的硬件,在EvtIoRead和EvtIoWrite中采用DMA方式进行传输。
1) DMA传输
DMA传输编程首先通过函数WdfDmaTransactionInitialize或WdfDmaTransactionInitializeUsingRequest初始化DMA传输,再调用WdfDmaTransactionExecute启动DMA编程;在EvtProgramDMA函数中根据数据手册完成对DMA相关寄存器的配置;最后当DMA传输中断时,在中断延迟处理例程EvtInterrupt例程中判断DMA传输是否结束,没有则调用WdfDmaTransactionExecute函数,继续启动DMA编程,若DMA传输结束,则完成I/O请求。
2) EvtIoDeviceControl例程设计
该例程主要通过应用程序向驱动传送的控制码区分功能,根据需求创建如下控制码:
PCIE_IOCTRL_CONFIGREG_READ:配置空间寄存器读
PCIE_IOCTRL_CONFIGREG_WRITE:配置空间寄存器写
PCIE_IOCTRL_MEMORY_READ:存储空间寄存器读
PCIE_IOCTRL_ MEMORY _WRITE:存储空间寄存器写
PCIE_IOCTRL_ OFFSETADDR _SET:偏移地址设置
通过调用WIN32函数DeviceIoControl传入指定的控制码即可实现对应功能。
7.中断处理:
在驱动中创建中断对象和中断回调例程,当有中断产生时首先判断该中断是否来自本设备,若是则读取中断标志位分析中断类别,然后执行相应操作。中断类型有DMA读写中断控制,在一次DMA传输完成产生;数据传输中断,在FPGA主动给上位机发送数据时产生。前者已在DMA部分论述,此处主要介绍后者的中断处理。
内核同步听起来比上面两者高级一些,原因是使用的人不太多。原理却很简单:创建一个事件对象传递给内核,用户层一直等待它的完成。这和用户层的两个线程之间的同步,并无差别。但在内核那边,有一些技巧。
问题在于,用户创建的事件,是一个Win32 句柄,并且这个句柄还是进程相关的(即放到别一个进程中去,这个句柄就是非法的了)。内核所在的地址却是全局性的(亦即一个地址需在此进程中正确,亦在所有其他的进程中也正确),要让全局性的内核可以安全使用局部性的用户句柄,唯一的办法是将用户句柄转换成全局对象地址。


8.驱动程序具体实现
8.1驱动程序入口地址
当系统加载驱动程序的时候,DriverEntry例程会被调用。该例程是WDF驱动程序的主入口地址,驱动程序会通过该例程创建驱动对象并且设置EvtDriverDeviceAdd例程地址。值得注意的是,在调用WdfDriverCreate函数前,必须先调用WDF_DRIVER_CONFIG_INIT函数来初始化WDF_DRIVER_CONFIG结构,回调函数PCIDriver_EvtDeviceAdd寄存在这个结构内。初始化程序如下。
NTSTATUS DriverEntry(
    IN PDRIVER_OBJECT DriverObject,
    IN PUNICODE_STRING RegistryPath
)
{
    WDF_DRIVER_CONFIG  config;
    NTSTATUS             status;
    WDF_DRIVER_CONFIG_INIT(&config, PCIDriver_Evt_DeviceAdd);
    status = WdfDriverCreate(
DriverObject,
RegistryPath,
WDF_NO_OBJECT_ATTRIBUTES,
&config,
WDF_NO_HANDLE
);
    return status;
}

8.2驱动程序初始化
    当有新的PCIE硬件设备插入。PNP管理器首先根据驱动程序的inf文件找到相应设备驱动,然后调用PCIDriver_Evt_DeviceAdd回调函数,从而初始化驱动程序。在本驱动中,该例程的基本职责是创建设备对象和设备GUID接口,设置各种事件的回调例程,获取设备内存空间、初始化I/O队列、初始化中断处理以及初始化DMA操作等工作。
8.3创建设备对象和设备GUID接口
    在PCIDriver_Evt_DeviceAdd例程中,驱动程序将创建一个设备对象作为目标I/O设备,并将设备对象附着到设备堆栈中。创建设备对象的具体程序如下,其中DeviceInit是例程的入口参数,deviceAttributes是对象属性,device是设备对象创建的句柄。
    status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
    if(!NT_SUCCESS(status))  return status;
    创建设备GUID接口是为了使上层应用程序可以调用底层驱动程序,创建成功后,应用程序就能够打开设备的句柄,从而完成向驱动程序IRP的发送。应用程序可以使用标准的CreateFile API打开设备句柄,然后用ReadFile、WriteFile和DeviceIoContorl向驱动程序发出请求。创建设备接口的具体代码如下。
    status = WdfDeviceCreateDeviceInterface(device, (LPGUID)&PCIDriver_DEVINTERFACE_GUID, NULL);
    if(!NT_SUCCESS(status))  return status;
    其中device是设备对象创建句柄,PCIDriver_DEVINTERFACE_GUID是设备接口GUID,其定义方式如下:
    DEFINE_GUID(PCIDriver_DEVINTERFACE_GUID, 0x1b77aab3, 0x6ab0, 0x4883, 0xa2, 0xd6, 0x8e, 0x28, 0xa1, 0x08, 0x75, 0xe6 )

8.4初始化I/O队列
    在KMDF驱动程序中,队列具有非常重要的地位。如果要处理应用程序的I/O请求,就需要队列。驱动程序存在3种IO队列:Read、Write、IoControl。系统根据应用程序以及上层驱动发送过来的IRP包的主请求码在相应的队列中排队,Read队列用来处理读请求,Write队列处理写请求,IoControl队列主要处理IO控制命令请求。框架有个默认队列,也可以自己创建队列。本驱动采用框架默认队列。
8.5获取设备内存空间
    当PNP管理器为设备分配完硬件资源后WDF框架自动调用EvtDevicePrepareHardware函数来获取设备的内存空间。首先,驱动程序利用WDF框架封装好的函数得到设备内存的物理地址与长度,如果函数返回成功,将物理地址映射成驱动程序可以识别的虚拟地址,并将此虚拟地址保存到内部变量中。驱动程序根据EvtDevicePrepareHardware函数提供的ResourcesTranslated参数来寻址硬件设备的内存空间。本系统中,BAR0和BAR1被配置成Memory空间,用来传输数据;BAR2配置成IO空间,用来对应DMA的各个寄存器。
8.6初始化中断处理
    WDFINTERRUPT对象实现硬件中断的处理。初始化中断处理的过程中,驱动程序先初始化结构体WDF_INTERRUPT_CONFIG,再创建中断对象。初始化中断的主要任务是为每个设备中断创建一个中断对象,当PNP管理器获取了硬件资源并为设备分配完内存空间之后,将硬件设备的中断信息保存在中断对象中。初始化中断的具体代码如下。
    WDF_INTERRUPT_CONFIG_INIT (&interruptConfig, PCIDriver_EvtInterruptIsr, PCIDriver_EvtInterrruptDpc);
    status = WdfInterruptCreate(device, &interruptConfig, WDF_NO_OBJECT_ATTRIBUTES, &pDeviceContext->Interrupt);
    if(!NT_SUCCESS(status))  return status;
    值得注意的是,在创建中断对象之前,务必先用WDF_INTERRUPT_CONFIG_INIT函数初始结构体WDF_INTERRUPT_CONFIG,因为中断服务例程(ISR)和延迟过程调用(DPC)这两个重要的例程寄存在该结构体中,最后再调用WdfInterruptCreate函数创建中断对象。
8.7初始化DMA操作
    在本驱动设计中,用到了KMDF为DMA操作提供两个对象,WDFDMAENABLER、WDFCOMMONBUFFER。WDFDMAENABLER对象用于建立一个DMA适配器,它说明DMA通道的特性。WDFCOMMONBUFFER对象用于申请系统提供的共用缓冲区。首先,驱动程序创建一个WDFDMAENABLER对象,然后再创建WDFCOMMONBUFFER对象后,操作系统就会为驱动程序提供一个在物理上连续的内存,称其为公用缓冲区,作为硬件设备和驱动程序进行DMA传输的公共区域。
8.8 硬件访问
    当驱动程序收到IRP时,根据IRP中的主功能码(IRP_MJ_XXX)相应的队列中排队,然后依次取出队列中的各个IRP,调用I/O请求队列的处理函数。在本驱动中,只运用IoControl队列,应用程序只能通过发送IoControl命令的方式来调用驱动程序。

8.9处理硬件中断
    在WDF驱动程序中,用WDFINTERRUPT对象来实现硬件中断的处理。在初始化程序中,已经对该对象作了初始化,并且链接了相应的ISR函数和DPC函数。当一次DMA传输完成后,底层硬件会发起一个中断给操作系统。驱动程序收到硬件中断,调用中断服务例程(ISR)。中断服务例程的处理时间应当尽可能的短,并且由于中断服务例程在DIRQL级别上运行,很多函数不能调用。所以通常在中断服务例程中要判断中断是否由自己的设备产生。如该中断是由自己的设备产生的,则调用一个在DISPATCH_LEVEL级别上运行的延迟过程调用。

扫描二维码关注公众号,回复: 4732779 查看本文章

8.10 DMA处理
    DMA操作分为两种,即DMA写操作和DMA读操作。DMA写操作就是底层硬件传输数据到上位机;DMA读操作则是上位机传输数据给底层硬件;其实所谓的DMA操作就是运用初始化时申请的COMMON_BUFFER与PCIE卡的FIFO进行数据交互。
    在驱动程序中,这两个操作的其他步骤都是相同的,唯一不同的就是启动方式不一样。驱动程序通过调用WRITE_PORT_ULONG函数对DMA寄存器空间进行写操作;通过READ_PORT_ULONG函数进行读操作。
    在开启DMA传输前,要先对3个寄存器进行初始化,分别用来保存FIFO的首地址、COMMONBUFFER的物理地址以及DMA传输的长度。如果是DMA写操作,则往偏移地址为0x10的寄存器写入0x001启动DMA操作如果是DMA读操作,则往偏移地址为0x10的寄存器写入0x100启动DMA操作;当DMA数据传输结束时,硬件会发起一个中断,驱动程序检测到这个中断之后,进入中断处理程序,清除中断并结束DMA操作。DMA具体实现代码如下。
NTSTATUS status=STATUS_SUCCESS;
CommonBuffer_Addr = pDeviceContext->CommmonBufferBaseLA + inputBuffer->LowerAddr_Tab;
R_W_Mode = inputBuffer->Write_Read;
if(R_W_Mode &0X01) == 0X00)
{
    WRITE_PORT_ULONG(pIO+0XA0, inputBuffer->Memory_Add);
WRITE_PORT_ULONG(pIO+0X08, CommonBuffer->Memory_Add);
WRITE_PORT_ULONG(pIO+0XA4, inputBuffer ->CtlF_LenOfDma);
WRITE_PORT_ULONG(pIO+0X10,  0x01);
}
if(R_W_Mode &0X01) == 0X01)
{
    WRITE_PORT_ULONG(pIO+0XA0, inputBuffer->Memory_Add);
WRITE_PORT_ULONG(pIO+0X08, CommonBuffer->Memory_Add);
WRITE_PORT_ULONG(pIO+0XA4, inputBuffer ->CtlF_LenOfDma);
WRITE_PORT_ULONG(pIO+0X10,  0x100);

}


 

猜你喜欢

转载自blog.csdn.net/zhangkun2609/article/details/84701423
wdf