上一小節認識了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 ,點擊最上面的三個按鈕,能夠得到並打印出這些描述符的信息。以下圖所示:
描述符按鈕:
打印信息:
作慣了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 代碼示例,教你走,領着看。
提到設備對象,讓人一會兒就想到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本身參透吧。
這一小節乃是從《建立設備》節中分出來的,爲了醒目的緣故。
和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 環境下如何爲設備建立符號連接或設備接口。(省略)