1. 程式人生 > >基於WDF框架的PCIE驅動設計

基於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級別上執行的延遲過程呼叫。

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);

}