WDF開發USB設備驅動教程(2)

 

3.2 獲取描述符

上一小節認識了USB 的描述符後,這一節就來說如何從 USB 設備獲取它們。我列出了具體的代碼,包括獲取設備描述符、配置描述符和 String 描述符。看過代碼後,你們會以爲在 WDF 中作這些操做,動做很是簡潔,堪稱舒心。程序員

首先看獲取設備描述符,一行代碼足矣。編程

 

USB_DEVICE_DESCRIPTOR   UsbDeviceDescriptor;數組

WdfUsbTargetDeviceGetDeviceDescriptor(app

IN  pContext->UsbDevice, // WDF設備對象框架

                                     OUT & UsbDeviceDescriptor // 返回的設備描述符ide

);函數

 

接下來看獲取配置描述符。配置描述符囊括了USB 配 置所要用到的所有信息:設備描述(區別於設備描述符)、類描述、接口描述、端點描述。和設備描述符的定長不一樣的是,因爲不一樣的設備其配置佈局,包含的接口 與端點數不盡相同,故而配置描述符的長度是不定的。應該先取得配置描述符的長度,根據長度分配內存緩衝,而後二次獲取設備描述符內容。工具

 

// 首先得到配置描述符的長度。它是變 長 的,包含了所用接口描述符、端點描述符。佈局

status = 測試

WdfUsbTargetDeviceRetrieveConfigDescriptor(pContext->UsbDevice, NULL, &size);

 

if(!NT_SUCCESS(status) && status != STATUS_BUFFER_TOO_SMALL)

break;

 

// 輸出緩衝區不夠長

if(OutputBufferLength < size)

break;

 

// 再次調用,正式取得配置描述符。

status = 

WdfUsbTargetDeviceRetrieveConfigDescriptor(pContext->UsbDevice, pBufferOutput, &size);

 

 

最後咱們看String 描述符的狀況。 USB 設備的字符串描述符也是由設備固件定義,數量不限,甚至能夠沒有。它用來表述設備廠商( Vendor )對本設備的描述,這包括設備的製造商名稱,產品名稱,產品序列號,甚至包括接口的描述(不過 Windows 系統彷佛不支持這個特性)。不一樣的字符串經過從 0 開始遞增的 String ID 來區分。另外值得一提的是, USB 協議容許字符串描述符支持多國語言,這樣同一個 String ID 能夠對應於一個以上的描述符。 String ID 爲 0 的字符串描述符專門用來描述 USB 設備說支持的語言(用 Language ID 表示,好比英語的 ID 爲 0x0904 )。這樣主機能夠經過獲取設備的 0 號字符串來分析它所支持的語言種類,並獲取相應語言版本的字符串描述符。

和配置描述符同樣,字符串描述符的長度不肯定。咱們也是分兩次調用,第一次調用獲取描述符長度,而後分配內存緩衝區,再二次調用獲取描述符內容。下面是CY001_WDF 工程中 GetStringDes 函數的實現,咱們能夠看到語言 ID 是怎麼在這裏起到做用的(惋惜 CY001 的固件代碼目前還只支持英語一種語言,呵呵):

 

NTSTATUS GetStringDes(

USHORT shIndex, // String ID

 USHORT shLanID, // 語言 ID

 VOID* pBufferOutput,

 ULONG OutputBufferLength, ULONG* pulRetLen, PDEVICE_CONTEXT pContext)

{

NTSTATUS status;

 

USHORT  numCharacters;

PUSHORT  stringBuf;

WDFMEMORY  memoryHandle;

 

KDBG(DPFLTR_INFO_LEVEL, "[GetStringDes] index:%d", shIndex);

ASSERT(pulRetLen);

ASSERT(pContext);

*pulRetLen = 0;

 

// 因爲 String 描述符是一個變長字符數組,故首先取得其長度

status = WdfUsbTargetDeviceQueryString(

pContext->UsbDevice,

NULL,

NULL,

NULL, // 傳入空字符串

&numCharacters,

shIndex,

shLanID

);

if(!NT_SUCCESS(status))

return status;

 

// 判讀緩衝區的長度

if(OutputBufferLength < numCharacters){

status = STATUS_BUFFER_TOO_SMALL;

return status;

}

 

// 再次正式地取得 String 描述符

status = WdfUsbTargetDeviceQueryString(pContext->UsbDevice,

NULL,

NULL,

(PUSHORT)pBufferOutput,// Unicode字符串

&numCharacters,

shIndex,

shLanID

);

 

// 完成操做

if(NT_SUCCESS(status)){

((PUSHORT)pBufferOutput)[numCharacters] = L'/0';// 手動在字符串末尾添加 NULL

*pulRetLen = numCharacters+1;

}

return status;

}

得到了這些描述符以後,咱們就能夠經過對它們的分析,獲得USB 設備的詳細信息了。好比設備的版本( 1.1仍是 2.0 ),有幾個接口,接口中的端點數,端點的類型(控制、批量、中斷或等時)。

運行CY001 開發板的 UsbKitApp.exe ,點擊最上面的三個按鈕,能夠得到並打印出這些描述符的信息。以下圖所示:

描述符按鈕: 

打印信息:

連接地址 4. 設備初始化

作慣了WDM 驅動的人都知道,驅動初始化在入口函數,設備初始化在 AddDevice 函數,這確是不刊之論。WDF 框架中,驅動初始化咱們已經講了它的入口函數。然則設備初始化,到底怎麼作呢?它是否仍是對應到AddDevice 函數?回答是 NO 。

WdfDriverCreate 調用已經指明瞭,設備初始化在自定義的PnpAdd函數中完成。你們稍微上翻一兩頁,就能看到定義PnpAdd函數的地方,不妨再寫出來:

WDF_DRIVER_CONFIG_INIT(&config, PnpAdd);

 

調用 WDF_DRIVER_CONFIG_INIT 宏,並不強制你必定要傳入一個有效的函數指針,若是傳入NULL指針也是能過去的,只是設備就沒有地方能夠初始化了。

回過頭來討論PnpAdd函數,你們確定腦子裏已經在想,它和AddDevice是什麼關係呢?看看它的函數申明先:

typedef  NTSTATUS 
  (*PFN_WDF_DRIVER_DEVICE_ADD)( 
    IN   WDFDRIVER    Driver , 
    IN   PWDFDEVICE_INIT    DeviceInit 
    );

 

第一個參數是驅動對象,就是DriverEntry 函數中被初始化的那個。

第二個參數,是WDFDEVICE_INIT 結構體。這個結構體頗爲複雜, WDF 未能給出它的具體定義,只是暴露出了一系列 API 用來初始化這個結構體。具體來講,它涉及到了設備初始化的方方面面,甚至更多。好比定義設備名、設備緩衝方式的定義,屬於正常的設備對象屬性;而註冊 PNP 和 Power 回調函數,則已經超出了傳統的設備對象屬性範圍,越界到驅動對象裏去了(這些回調函數,更像是驅動對象的分發函數或者分發函數的變體。WDF 框架對 PNP 和 Power 管理有很是大的變更,內部機理,誰也不曉得,咱們順其天然罷了)。

WDFDEVICE_INIT結構體的初始化 API 頗爲豐富。分紅了三個系列。對應於普通設備對象(簡稱 Devcice )的初始化,專門針對功能設備對象(簡稱 FDO )的初始化,和專門針對物理設備對象(簡稱 PDO )的初始化。總共加起來大概有 30 來個。對 USB 設備驅動而言,要用到的只是前二者系列 API 。物理設備對象的初始化 API通常由總線驅動或更底層的驅動使用,生成的物理設備對象,將被上層功能驅動所掛載。

一一弄明白這些API 接口,非常一件煩心事。好在這些 API 的定義到時很 Readable ,有時候看看名稱到也可以猜到一二。我下面儘可能多分析幾個。

PnpAdd函數所收到的這個 WDFDEVICE_INIT 結構體,是已經被初始化過的。最明顯的一個理由是,經過它,能夠調用 FDO 初始化 API 得到許多設備信息。好比:獲取物理設備對象、獲取註冊表中的硬鍵、軟鍵(也就是 Hardware 鍵和 Software 鍵)、獲取物理設備對象的屬性(設備 ID 、兼容 ID 等)。這些 API 列於下:

WdfFdoInitAllocAndQueryProperty

WdfFdoInitOpenRegistryKey

WdfFdoInitQueryProperty

WdfFdoInitWdmGetPhysicalDevice

這些API 的具體的使用方法很簡單,確實起到了簡化操做的目的。 WDF 文檔中都有示例代碼的。注意,這些API 必須在 WdfDeviceCreate 被調用以前調用。由於一旦 WdfDeviceCreate 被調用後, WDFDEVICE_INIT 結構的內容可能就已經變了甚至不存在了。

FDO初始化 API 中剩下的三個,兩個是爲過濾驅動準備的( WdfFdoInitSetEventCallbacks 和WdfFdoInitSetFilter ),一個爲總線驅動準備( WdfFdoInitSetDefaultChildListConfig ),咱們就不用管它們了。

 

回頭來看Devcice 初始化系列 API 。這裏面涉及最多的是設置 Pnp 和 Power 屬性、回調的 API ,由此也可見這二者的複雜程度。 CY001 中用到了一個 WdfDeviceInitSetPowerPolicyEventCallbacks ,下面會講到 。

另外一類是類型註冊,用來在註冊表中修改物理設備安裝屬性的(包括Type 、 GUID 、特性等)。它們使得設備即便在被安裝後,也能改變它的 class ID 、 device type 這些安裝時設定的設備屬性。這確實是一件很實惠的事情。拿 CY001 爲例,用 inf 文件安裝好後,它的給定類 ID 是: {9048DC75-B91C-4392-925A-44A7269D6BD4} ,類名稱是: CY001 Sample 。打開設備管理器,正以下圖所能看到的:

 

但若是我在PnpAdd 函數中,調用 WdfDeviceInitSetDeviceType 函數並傳入參數FILE_DEVICE_SERIAL_PORT ,那下次再看到CY001 的時候,它的位置就會列於串口設備下面去了。

說一說這些API 的內部機理吧。 Windows 的安裝( Setup )模塊是一套挺複雜的東西,我就很少嘴多舌了。對於已經在系統中安裝好的設備,它們的信息是統一被列在註冊表中 Enum 和 Class 下的,也就是你們所說的硬件鍵和軟件鍵。系統的 Setup 系統,正是從這些地方保存並查找設備的。而咱們如今所講到的這一系列的 API ,其工做就是修改設備對象這兩個鍵的位置與值,這樣 Setup 系統下次就會把它當成另一我的看了。

這些API 列於下:

 

WdfDeviceInitSetCharacteristics // 好比軟盤設備: FILE_FLOPPY_DISKETTE

WdfDeviceInitSetDeviceClass // 好比系統設備類: GUID_DEVCLASS_SYSTEM

WdfDeviceInitSetDeviceType // 好比改爲串口類型 FILE_DEVICE_SERIAL_PORT

WdfDeviceInitSetExclusive // 獨佔打開,即一次只能建立設備對象的一個實例

//(對應於應用程序的Handle)

 

下面具體講,如何進行USB 設備初始化、配置。

 

連接地址 4.1  初始化過程

 

之前寫USB 驅動,程序員大倒苦水,緣由之一是 USB 設備的配置太麻煩了。這不由讓我想起了寫文件過濾驅動的時候,裏面有一個卷設備掛載操做,反反覆覆,這般那般,簡直沒完沒了。代碼還沒開始寫呢,腦子先被他轉暈了。還好 USB 的設備配置任務雖然重(我指的是代碼多),但總算都是些基本概念,不用太難爲本身的腦細胞。

從USB 設備插入 PC 主機開始,到它能被操做系統識別,要通過一些特定的過程,枚舉以下:

a)  設備插入主機後,USB 設備進行復位操做,將物理地址置 0 。

b)  主機檢測到有物理設備接入,便經過地址查找的方式,查找地址爲0 的 USB 設備;找到後,向 USB 設備發送請求,獲取它的設備描述符。

c )  主機分析設備描述符,並根據實際狀況,爲新插入設備從新分配一個物理地址(非0 );並把這個新地址,經過 Set Address 命令發送給設備。

d )  設備收到並保存新地址,此後當主機查詢設備的時候,USB 設備即當以此新地址來回應查詢請求。

e )  Set Address成功後,主機向剛分配地址的 USB 設備再次發送請求,獲取設備描述符。

f )  獲取設備描述符成功後,主機發送請求獲取配置和報告描述符。

g )  根據獲取的描述符,主機配置此USB 設備。

h )  配置完成,設備正常工做。

 

上面的這個過程,凡是講PNP 管理器的書籍,大抵都會講。我這裏僅僅簡單列一下,詳細透徹的說明,你們去找書看,《 Windows Internal 》就講得很是詳細。 a->d 這四個步驟,是設備被系統識別的過程,是由系統(總線驅動或其餘的系統模塊)和 USB 設備交互完成的。 e->g 這三個步驟由功能驅動負責來作。

我上面也說過了,之前用WDM 來完成這五個步驟,是比較煩難的。弄弄就是一大堆代碼,雖然沒有什麼靈活機變的地方,但很容易一不當心就搞錯了。在這篇文檔中,我爲了比較可能會舉一些 WDM 的示例代碼。但我主要想指給你們的路,是一條用 WDF 鋪出的林中碎石密徑,輕快、乾淨還漂亮。因此會有好多 WDF 代碼示例,教你走,領着看。

連接地址 4.2   建立WDF 設備

提到設備對象,讓人一會兒就想到DEVICE_OBJCET 結構體。更有些人還會馬上想到《Undocument Windows 2k 》 裏面列出的關於這個結構體每一個成員的詳細解釋。設備對象是最基本的內核對象之一。設備對象未必都對應到一個物理設備。好多「設備」都是存在於邏輯上的,比 如「卷」設備;還有一些設備對象,則連邏輯設備也不是,好比每一個驅動均可能會有一個控制設備對象,它們純粹只是一個「結構體」而已。

但對於表明物理設備的物理設備對象而言,系統經過操做這些對象,起到了實際控制物理設備自己的做用。

從結構體自己而言,DEVICE_OBJCET 夠底層,夠強大,夠 Undocument 。另外,它還夠難理解,夠難使用,夠易出錯。用好它的人夠厲害,用壞它的人,嗯,夠不幸。處於對無數不幸人士的體貼, WDF 提供了封裝對象 WDFDVICE 。對於 WDFDEVICE ,它徹底 undocument (別沮喪),但無比易用,幾乎不會出錯。

哦,不要忘了,WDF 除了 WDFDVICE 外,還進一步又封裝了一個 WDFUSBDEVICE 對象。從從屬關係來講, WDFUSBDEVICE 已是 DEVICE_OBJCET 的孫子輩了。對於 USB 驅動,這個對象真是太好用了!

WDF對象封裝得過於嚴實。到目前爲止,我還不曉得有誰破譯出它們內部的定義。這種狀況下,***們大概是不太歡喜的。

調用WDF 驅動初始化函數後,框架就爲驅動對象生成一個 WDFDEVICE 對象。這個對象句柄在 XXX 函數中做爲參數傳入。能夠不保存這個句柄,由於咱們須要根據這個對象句柄,生成 WDFUSBDEVICE 對象,只要保存後者就能夠了。

要找一個可用來保存自有數據的地方。WDF 爲每一個框架對象都設計了一個特殊的「環境變量」——不只僅是這裏講到的設備對象,而是全部框架對象——正可用來保存這些數據。這個「環境變量」,用起來有點像 WDM設備對象中的設備擴展。但用起來要麻煩不少。

首先要申明「環境變量」的類型和大小。根據大小,框架爲設備對象申請一塊內存。

其次定義一個函數指針,經過這個函數能夠獲取「環境變量」。這可真麻煩。但這也是沒有辦法,由於框架對象是徹底密封的,沒有辦法像設備擴展指針同樣直接獲取。這項技術提及來仍是挺有趣的,我非要給你們說個明白不可。

注:咱們使用KMDF 框架進行編程,通常不直接使用原始的 WDM 對象。在這裏,咱們把 WDM 對象稱做原始對象( RAW ),而把 KMDF 對象稱做封裝對象( Wrapped )。只要願意,能夠對 RAW 對象進行各類形式的封裝。你們初學的時候遇到這些東西會感受比較麻煩,但熟悉以後卻能帶來編程上的便利,它們都帶有定義良好的接口。

咱們要找到一個保存WDFUSBDEVICE 對象句柄的地方。 WDF 設備的「環境變量」,至關於 WDM 驅動中的設備擴展,是一個理想的地方。

 

// 建立WDFUSB 設備

status = WdfUsbTargetDeviceCreate(Device, WDF_NO_OBJECT_ATTRIBUTES, &DeviceContext->UsbDevice);

if(!NT_SUCCESS(status))

{

   KDBG(DPFLTR_INFO_LEVEL, "WdfUsbTargetDeviceCreate failed with status 0x%08x/n", status);

return status;

}

    

上例中調用 WdfUsbTargetDeviceCreate 時候的Device 句柄,是初始化的時候由系統建立的。這個句柄表明了一個WDFDEVICE 對象,也就是說系統其實已經爲咱們建立了一個 WDF 設備對象了,咱們如今在它的基礎上再封裝出一個 WDF USB 設備對象。

建立WDF 設備對象是比較簡單的,複雜的地方在於設置初始化結構體。咱們能夠分兩個步驟來實現初始化: 1. 註冊 PNP 、 Power 回調函數; 2.  設備命名;

對於設備驅動來說,PNP 、 Power 分發是頂頂重要的,這一點和過濾驅動不一樣。如何處理好 PNP 、 Power 分發,是設備驅動開發過程當中很頭疼的事情。不只事繁,並且事艱。 WDF 框架頂好的一個優勢就是爲全部的 PNP、 Power 分發寫了默認處理方法。這樣咱們只要註冊少許感興趣的回調函數,即能將它們輕鬆處理了。

 

// 註冊PNP與Power回調函數。

WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);

pnpPowerCallbacks.EvtDevicePrepareHardware   = PnpPrepareHardware; // 在此爲設備驅動申請系統資源

pnpPowerCallbacks.EvtDeviceReleaseHardware = PnpReleaseHardware;

pnpPowerCallbacks.EvtDeviceSurpriseRemoval   = PnpSurpriseRemove;  // 異常移除

pnpPowerCallbacks.EvtDeviceRelationsQuery    = PnpRelation;

pnpPowerCallbacks.EvtDeviceD0Entry   = PwrD0Entry; // 進入D0電源狀態(工做狀態),好比初次插入、或者喚醒

pnpPowerCallbacks.EvtDeviceD0Exit    = PwrD0Exit;  // 離開D0電源狀態(工做狀態),好比休眠或設備移除

WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &pnpPowerCallbacks); // 註冊回調

 

// 讀寫請求中的緩衝區訪問方式。默認爲Buffered,還包括Direct和Neither。

WdfDeviceInitSetIoType(DeviceInit, WdfDeviceIoBuffered);

上面代碼的所有任務,就是初始化結構體對象 WDFDEVICE_INIT 。WDK文檔沒有給出這個結構體的定義。但有一系列的宏或者方法,被定義了用來對它進行設置。上面的代碼僅用到了其中的兩個。這個結構體也是至關複雜的,你們仍是結合WDK本身參透吧。

連接地址 4.3 設備命名

這一小節乃是從《建立設備》節中分出來的,爲了醒目的緣故。

和WDM 驅動同樣,設備對象是能夠選擇被命名的。就是說,設備對象能夠被命名,也能夠不命名,由程序員本身決定。命名的目的是爲了可以被識別和使用,若是無此須要則命名可沒必要進行。

功能設備對象老是須要命名的,由於功能驅動是用來被User 程序使用的。

 

>>>>>>>>>>>>>>>>>>>>>>>>>>>

附:《查看WDM 設備對象名》

若是扯得遠一點,我想和讀者交流一下怎麼在知道了一個設備對象地址後,手動查看這個設備對象名(首先要確認是否有用設備名)。 這部份內容純屬附加,不感興趣的朋友 可 繞過。

假設如今知道了某個設備對象的地址爲0xe1016b a0 ,咱們能夠經過下面的步驟手動查看它的設備名稱(僅在XP 下測試):

1.  打開WinDBG ,運行在 local kernel 模式下。在控制窗口中輸入命令 : 

dt nt!_object_header 0xe1016b a0 -0x18

 

這時候提示畫面會出現內核結構體OBJECT_HEADER (未文檔的結構體)的內容。 XP 下OBJECT_HEADER 的大小爲 0x18 字節,而且其位置正好就在 DEVICE_OBJECT 上面。因此咱們經過上面的 WinDBG 命令,能夠獲得一個正確的 OBJECT_HEADER 結構體內容。

 

2.  找到結構體中成員變量NameInfoOffset 的位置,看他的值。如今咱們能夠根據這個值判斷設備對象是否有名字:若是 NameInfoOffset 值爲 0 ,說明這個對象未被命名;不然,就是擁有一個名稱的,而且保存其名稱的地方,就在 OBJECT_HEADER 上面某處( NameInfoOffset 即爲偏移)。

我假設你看到的內容和我下面的截圖是同樣的:

 

咱們能夠根據這個值,找到系統保存對象名稱的地方。在控制窗口中運行這個命令:

dd 0xe1016b a0 -0x18-0x10

獲得一串內存數值後,第三個DWORD 值,就是保存設備名稱的緩衝區地址。

上圖是我電腦中運行後的結果,第三個DWORD 內容爲 0xe1016ba0 。

 

3.  再運行db 命令,查看地址 0xe1016ba0 所指示 緩衝區 中的 內容 :

lkd>  db  0x e1016ba0

0x e1016ba0   XXXXXXXXXX CY001_0.... //找到的設備名稱

0xe1016bb0 .......................... ........................

 

成功!

>>>>>>>>>>>>>>>>>>>>>>>>>>>

 

咱們在CY001_WDF 驅動程序中,爲設備命名形如「 CY001_X 」這樣的名稱,末位 X ,是區間 [0, 8] 的整數。由於不知道某個名字是否已經在系統中存在,因此須要一個循環嘗試的過程。經過判斷 WdfDeviceCreate調用返回的錯誤值是否爲STATUS_OBJECT_NAME_COLLISION ,能夠知道當前嘗試的名稱是否在系統中引發了名字衝突;若是發生衝突,咱們就須要從新嘗試。最多嘗試到名稱「 CY001_8 」,若是 CY001_8 也已經註冊了,就讓驅動初始化失敗。這樣的話,咱們的驅動目前最多支持同時 8 個 CY001 設備鏈接到系統中。 

 

// 目前驅動支持同時 8 個實例,便可以同時有 8 個開發板連接在 PC 上,驅動對它們給予並行支持。

// 不一樣的設備,各以其名稱的尾數( 0-7 )相別,並將尾數做爲設備的 ID 。

// 下面的操做中,咱們爲當前設備尋找一個未使用的 ID 。

for(nInstance = 0; nInstance < MAX_INSTANCE_NUMBER; nInstance++){

wcsDeviceName[nLen-1] += nInstance;// 修改末尾的數字,使從 0 至 7 。

 

// 調用 WdfDeviceInitAssignName 接口,嘗試着爲當前設備命名;

// 此函數在系統中查找此名稱是否惟一,如已存在則返回失敗,不然以成功返回。

status = WdfDeviceInitAssignName(DeviceInit, &DeviceName);

 

// 建立 WDF 設備。上面所作的設置在這一步方能發揮到實質性做用。

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

if(!NT_SUCCESS(status))

{

if(status == STATUS_OBJECT_NAME_COLLISION)// 名字衝突

KDBG(DPFLTR_ERROR_LEVEL, "Invalid name: %wZ", &DeviceName);

else

{

KDBG(DPFLTR_ERROR_LEVEL, "WdfDeviceCreate failed with status 0x%08x!!!", status);

return status;

}

}else{

KdPrint(("Found valid name: %wZ", &DeviceName));

break;// 成功即退出

}

}

 

一旦命名成功,那麼對應的名稱就會出如今系統名稱空間中。使用WinOBJ 工具,就能在 Device 子目錄下看到了。

下面來看看WDF 環境下如何爲設備建立符號連接或設備接口。(省略)

相關文章
相關標籤/搜索