Wince下usb驅動的思路

首先,USB分爲主設備和從設備,在CE上分別較USB Host和USB Function。而在這兩端,又都分別有兩種驅動,一種是Controller驅動,就是硬件的控制器驅動程序,例如OHCI,UHCI啥的。另外一種是Client驅動,就是上層的具體功能驅動,例如USB鼠標,USB攝像頭。所以,其實在CE上跟USB相關的驅動至少有四類:Host Controller, Host Client, Function Controller, Function Client。

 

 

 

隨着USB設備的不斷增加,我們這些開發人員也就多了對USB設備進行驅動程序開發的工作。但是對於很多初學者來說,存在以下三個困難:
        一是對WinCE的驅動程序結構瞭解得太少,沒辦法得心應手的專注於驅動程序的開發工作;
        二是對WinCE自帶的USB驅動程序的例子沒有弄懂,看到一大堆文件夾結構和源程序思維混亂;
        三是幾乎沒有什麼中文的參考資料,不知如何下手。

        第三條是很多開發人員都遇到的,我也一樣,很多朋友問我有沒有什麼資料,我也只能說抱歉,因爲我也同樣有這個問題,一切都靠自己的黑暗中摸索,因此本文不談第三條。
        第一條是可以找到資料的,如《Windows CE .NET系統分析及實驗教程》,因此本文也不打算在此花費大量筆墨。
        這樣,本文的着重點就在第二條上面了,通過本文,我希望能讓更多的朋友理解Windows CE下對USB設備的驅動模型及樣例程序中的實現過程,以樣例代碼爲基礎理順USB設備驅動程序的開發思路。同樣,本文的讀者對象預期是入門者和準備着手 USB驅動開發的人員,驅動開發高手自然就當一笑吧。同時寫本文的目的也是履行我半年前答應很多朋友的諾言,並向我的慵懶致歉。

        好了,在看樣例程序之前,我們還有些東西需要了解,我們就先來看下圖:

 

         在此圖中,我們可以非常清晰的看到主機和物理外設之間的結構方式,在主機端,通過USBD模塊和HCD模塊使用默認的PIPE訪問一個通用的邏輯設備,實際上就是說USBD和HCD是一組抽象出來的訪問所有USB設備的邏輯接口,它們負責管理所有USB設備的連接、加載、移除、數據傳輸和通用的配置。其中 HCD是主機控制驅動,是爲USBD提供底層的功能訪問服務,USBD是USB總線驅動,位於HCD的上層,利用HCD的服務提供較高層次抽象的功能。

        由於HCD和USBD都是面向的一致的邏輯設備接口,那麼對於各種各樣的物理設備,就需要有唯一對應的設備驅動程序,這就是上圖中最上層的特殊的PIPE所連接的物理設備和USB設備驅動程序。

        有了對這個結構的認識,我們可以明確的是我們要寫的就是最上端的USB設備驅動程序,在WINCE的樣例程序中也稱爲USB Client Driver,它是工作於USBD之上,所以實際上我們的工作就變成了利用USBD提供的接口針對特定的物理設備來完成USB設備驅動程序,而暫時與其他的部分無關。

        好了,先到這,接下來就準備看一些具體的東西吧!

接下來,我們就來分析一下CE中的樣例程序,我用的是4.2版本的,所以下面的內容是4.2版本中的程序。這裏的程序是通過文件夾的形式組織在一起的,所以我們還是像以前學習CE的時候那樣,先來了解與此相關的文件夾結構,如下圖。

 

        在USB文件夾下,分成了CLASS,CLIENTS,COMMON,HCD,INC,USBD幾個文件夾,其中INC和COMMON裏面有一個 lock.c的程序,這個程序很明顯是將要被其他USB有關的驅動程序所使用的一個鎖,代碼很簡單,只是一個類似臨界區的封裝體,可以保護多線程對同一內存區域的讀寫訪問,可以先不去管它。CLIENTS文件夾可能最初微軟的開發人員是用來放置設備驅動程序的,但是後來沒有放,而發佈的時候也沒有刪除,所以遺留了下來,裏面是個空的文件夾,所以沒用實際用處。USBD和HCD是前述的底層驅動,裏面含有很多子文件夾和程序,由於我們只針對USB設備驅動,因此對這兩部分不做分析,不興趣的朋友可以自己去了解。

        重點就在CLASS文件夾了,展開來看,裏面又包含了COMMON、HID、PRINTER、STORAGE幾個文件夾,同樣,COMMON裏面存放的源程序是爲HID、PRINTER、STORAGE所共有的。HID是USB輸入設備如鍵盤/鼠標的樣例驅動程序,PRINTER是USB打印機的樣例驅動程序,STORAGE是USB存儲設備如U盤的樣例程序。

        我們此次以USB存儲設備爲例,所以再來展開STORAGE文件夾,其中的INC文件夾裏面是頭文件,CLASS是USB存儲設備的驅動程序,DISK是磁盤驅動程序。這裏爲什麼有兩個驅動程序呢,我來簡要解釋一下。

        驅動程序工作在硬件與操作系統之間,它有兩個功能,一個是將操作系統轉發來的操作以符合指定硬件設備的形式控制硬件設備,另一個是向操作系統提供這個訪問接口。比如說U盤,一方面驅動程序要把操作系統對U盤的識別、讀、寫等操作轉換成U盤的動作,另一方面又告訴操作系統這是個U盤,可以當成一個文件夾或文件系統來用,能夠接受標準的文件操作命令。所以此處存在兩個驅動。

        另外還有一個文件夾,WINCE420/PUBLIC/COMMON/DDK/INC,這裏面是與設備驅動有關的頭文件,對於USB設備,相關的文件有 USB100.H, USBTYPES.H, USBDI.H,這裏面前兩個裏面關於USB的定義是完全符合USB規範的,不是隨便定義出來的,而USBDI.H文件裏的內容就是USBD總線驅動程序向USB設備驅動程序提供的接口描述,在開發USB設備驅動時必須要包含此頭文件,這樣纔可以得到USBD接口的原型。

此前,我們共同瞭解了USB驅動在CE中的位置結構,也瞭解了樣例驅動程序的文件夾結構,接下來,我們就要了解一下USBD爲我們提供了哪些接口來實現設備訪問以及驅動程序管理的功能。找到USBDI.H,不要告訴我你找不到吧,不管你用什麼編輯器,記事本也好,PB也好,VC/EVC或者VS都行,打開它,我們一起來了解一下USBD爲我們提供了什麼。

        我們首先看到的一個大的結構體就是_USB_DRIVER_SETTINGS,注意這個結構體不是USB規範中的USB設備描述,而是爲了CE設備管理器加載USB設備驅動程序方便而建立的。該結構體中對供應商描述、設備描述和Interface的描述是用來匹配註冊表中對USB設備驅動的註冊表鍵,當設備管理器發現你設備的這些值與註冊表中的這些值相符時,就會加載你的驅動。也就是說它是與你的設備唯一對應的東西,是一種標識。該結構體的供應商部分的描述需要根據你的設備的供應商信息來填,設備描述的設備類、子類、協議等可以在USB規範中找到,在USB100.H頭文件中也有一部分,在後面的樣例程序中也定義了一部分。

        在接下來有三個函數是必須由USB設備驅動程序實現的,這也就是我們在MSDN裏或其他CE的文檔裏所看到的,這幾個函數就是:
         USBDeviceAttach:設備加載的時候由系統調用
         USBInstallDriver:設備第一次加載的時候由系統調用,用來安裝註冊表配置以便搜索設備
         USBUnInstallDriver:設備移除後清理由上一個函數寫入的註冊表配置

         這樣在我們的驅動程序中就一定要按照這三個函數的原型來實現,否則就不能爲設備管理器所識別。其實除了這三個,個我覺得第四個也是必須的,這就是一個函數指針所指向的函數:
         *LPDEVICE_NOTIFY_ROUTINE
這個指針所指向的函數是用來接收通知消息的,既然微軟說任何USB設備必須實現USB_CLOSE_DEVICE消息的響應,那麼這個指針所指向的函數自然也就是必須要實現的了。

        繼續向下看,是一組函數的原型,這些函數就是USBD向設備驅動程序提供的服務接口,有些函數是可以任意調用的,用來完成版本信息讀取、註冊表操作和設備驅動程序註冊,這些函數有:
GetUSBDVersion    RegisterClientDriverID    UnRegisterClientDriverID    RegisterClientSettings
UnRegisterClientSettings    OpenClientRegistryKey

        還有大量的函數是必須通過指針調用的,通常只允許在驅動程序中調用,爲了方例使用,在這裏給出一個_USB_FUNCS的結構體,每一個結構體成員對應了一個函數指針,這樣在驅動程序中要想使用USBD函數只能通過一個結構體變量來進行了,在這裏我們要記住這個結構體的名字,並且微軟還對這個結構體變量進行了以下的類型定義:
typedef struct _USB_FUNCS USB_FUNCS, * PUSB_FUNCS, * LPUSB_FUNCS;
typedef struct _USB_FUNCS const * PCUSB_FUNCS;
typedef struct _USB_FUNCS const * LPCUSB_FUNCS;

        好了,到此我們發現,大部分的USB工作都已經被USBD完成了,我們爲了實現自己的設備驅動,只需要利用這些指針或函數,來實現四個我們自己的函數,然後在其中匹配上我們自己的設備就可以了。是不是忒簡單,沒錯,總不能剛開始就把自己嚇得不行,那樣後面可就沒法做了。

在上次瞭解了所有 USBD接口函數以後,我們已經有了很多基礎知識了,回顧USB樣例的文件夾結構,我們還能記得USB/CLASS/COMMON這個文件夾下是存放所公共部分的源程序,它是微軟專門抽象出來的能爲大多數USB設備驅動程序服務的一些結構體以及函數的封裝,我們這次再來概略的瞭解一下這裏面的源程序。

        這裏麪包含了三個程序,分別是:
        remlock        usbclient        utils
        下面我們分別來了解一下這三個程序的功能和接口,很顯然,USB設備驅動程序肯定是會用到這其中的一部分函數的,因此我們不一定需要讀懂這其中的每一行,但至少要對這些函數有個印象,不至於在讀驅動程序時不知道函數的來源。

        remlock程序是一個移除設備的鎖,利用這個結構體
   typedef struct _REMOVE_LOCK
   {
       BOOL Removed;
       LONG IoCount;
       HANDLE RemoveEvent;
   } REMOVE_LOCK, *PREMOVE_LOCK
來實現在設備移除時進行的同步控制。其中Removed成員是對設備是否已經移除的標識,IoCount成員是對設備進行訪問的數量,這也是驅動程序中常用的行爲,就像此前我們看到的那個Lock程序一樣,RemoveEvent是一個內核事件,熟悉WIN32編程的應該都很清楚,它是內核通知應用程序的一種方式,也是線程這間併發控制的一種手段,如果不熟悉,還是像我在以前文章中提到的那樣,一定要找WINDOWS高級編程之類的書把它學明白,否則就很難控制驅動程序了。
        利用它實現的那幾個函數就不說了,與臨界區的用法是一樣的。另外提一句,在此程序中有類似InterlockedIncrement這樣的函數,這種函數是WIN32 API函數,專門用來提供多線程對同一變量的同步訪問的,可以通過MSDN查到詳細用法。

        usbclient程序是對USBD進行包裝以供USB設備驅動程序使用的函數接口,通過usbclient.h我們可以發現裏面是關於數據傳輸、屬性設置、狀態描述和復位的一組函數原形的定義,我們再看usbclient.c文件,這些函數大部分都擁有一個LPCUSB_FUNCS類型的參數,回顧上次我們對USBD的瞭解可知,正是通過這一參數才能訪問USBD提供的服務功能,瀏覽一下函數的實現發現,確實每個函數都是通過這個參數調用了USBD的函數,然後處理調用後的結果,所以這裏只是多了一層封裝,使得驅動程序的編寫更加清晰易於維護。

        另外,這裏我們要留意一下IssueBulkTransfer()、IssueInterruptTransfer()、 IssueVendorTransfer() 這三個函數,它們實現了通用的Bulk傳輸、中斷傳輸和自定義的傳輸方式,在驅動程序中要用得到。

        utils程序很簡單,是對註冊表操作的封裝,利用_REG_VALUE_DESCR這個結構體和GetSetKeyValues()函數可以方便的訪問註冊表,在驅動程序的安裝中會用得較多。

        又說了這麼多東西,雖然沒有看多少程序,但我們又離驅動程序近了一層,至少知道了很多函數是要在驅動程序中用到的,如果有興趣,可以具體閱讀每一個函數的實現方法,但我覺得這並不影響對驅動程序的開發。如果是我寫驅動,在沒有特別的情況下,我會把這些公用的源程序照搬過來,這可是能極大的縮短開發週期的事哦!

 正如所料,接下來我們就進入到DRIVERS/USB/CLASS/STORAGE/CLASS文件夾下,接觸USB設備驅動程序。

         我們先來了解兩個頭文件,分別是STORAGE/INC/usbmsc.h和STORAGE/CLASS/usbmscp.h,其中前者是USB存儲設備公用的頭文件,後者是需要按照自己的設備更改的頭文件。我們先來看前者。

        在usbmsc.h這個頭文件中,前邊定義了很多常量,包括子類和協議的常量,這是從哪裏來的呢?前文我們已經提到過,這些量值是依據USB設備規範得來的,在規範上都作了定義,所以此處的值必須與USB規範中的相一致。再向下的命令塊結構體和數據塊結構體是用來與USB設備通訊用的,可以通過這兩個結構體的實例與USB設備傳輸數據。 下面的函數原型就不說了,前文提到過,在這裏只記得有這幾個函數就行了。

        再來看usbmscp.h這個頭文件,這個頭文件是要按照自己的需要和USB設備來進行修改的,比如DRIVER_NAME_SZ是驅動程序的名字,RESET_TIMEOUT 是一個超時的默認值。還有很重要的一個就是USBMSC_DRIVER_SETTINGS的設置,這個設置是與USBDI.H中的 USB_DRIVER_SETTINGS結構體一一對應的,爲了符合我自己的設備,通常要把dwVendorId和dwProductId等設置成設備的對應值,比如我的U盤的VendorID是0x058F,ProductID是0x9321,那我就會把這兩個值對應的寫在相應的位置上。同時在系統註冊表中也會利用這兩個值修改註冊表的鍵以便設備管理器可以順利的找到我的設備驅動。

        下面還有一個_USBMSC_DEVICE結構體,它是用來描述你自己的USB存儲設備的,是封裝了USBD函數表指針、磁盤設備指針、管道和配置項的最重要的數據結構,在驅動程序實現上此數據結構就是重點的參數,鑑於樣例程序對每一個結構體元素都作了明確的註釋,此處我就不一一描述了,它就像C++中的類一樣,是最後把一些小類組合起來的可以最終使用的結構。

        好了,對這兩個頭文件有所瞭解以後,我們就進入最關鍵的部分,源程序。我們接下來來看usbmsc.c這個文件。爲什麼要先看這個文件而不是同一文件夾下的其他幾個文件呢?我來解釋一下。在這個文件夾中有一個usbmsc.def的文件,大家都知道它是定義了導出函數的,通常與它同名的程序文件都會含有 DllEntry的入口,既然入口在這,那我們自然就先來看這個文件了。如果用到了其他的文件,再看不遲。

        這可是一個有1000多行的源程序,但不要害怕,我們只看最主要的,別的函數的實現你可以自己去研究。首先看到文件的DllEntry入口之前有5個函數原型的定義,從函數名上就可以知道這個函數的功能了,很顯然這幾個函數是程序實現過程中被調用的,所以目前知道功能就行了,不用瞭解實現方法。忘了說一句,這個程序中包含了bot.h和cbit.h兩個頭文件,可見程序中是要用到它們的功能的,不過先不管它,繼續往下看。

        DllEntry入口函數的下面,就是USBInstallDriver()這個函數了,它的作用是進行與USB設備相關的註冊表操作,主要的語句是:
bRc = RegisterClientDriverID( wsUsbDeviceID );
bRc = RegisterClientSettings( szDriverLibFile, wsUsbDeviceID, NULL, &usbDriverSettings );
即先註冊設備類別,然後是設備細節。 同樣,USBUnInstallDriver()函數是以相反的順序解除註冊信息的。 這幾個與註冊有關的函數在前面我們提到過,是由USBD接口提供的,這裏我們可以看到USBD對設備驅動程序的重要性。
在繼續向下看,我們發現了USBDevi


         後面的程序將以此行號進行說明。

 我們來看程序的第4行,這裏有一個判斷語句,它是在判斷插入的設備是否是USBMSC_INTERFACE_CLASS類型的,這個常量是在usbmsc.h文件中定義的,也就是說如果設備不是USB存儲設備,那麼就結束這個函數,也就是此驅動只能處理USB存儲設備。

        當發現設備符合此驅動程序的要求後,就通過函數ParseUsbDescriptors()來解析這個設備,這個函數在下面的程序中將被實現,我們可以看一下該函數的函數體,很顯然,它是在爲設備進行各種配置,這就不多說它了。

        再往下,分配內存,設置標誌,從註冊表中讀取信息。注意,這裏讀取到的註冊表信息是Drivers//USB//ClientDrivers //Mass_Storage_Class和bInterfaceSubClass變量組合成的註冊表鍵下的值,具體可參閱源程序,這個註冊表鍵下放置的內容是
[HKEY_LOCAL_MACHINE/Drivers/USB/ClientDrivers/Mass_Storage_Class/6]
"DLL"="USBDISK6.DLL"
"Prefix"="DSK"
"Folder"="USB Disk"
"IOCTL"=dword:4
"IClass"="{A4E7EDDA-E575-4252-9D6B-4195D48BB865}"
由此可以看出,通過此處的註冊表讀取,驅動程序可以知道這個設備將通過哪種形式以及哪個DLL向操作系統提供接口。同時也爲後續的操作進行了準備。

        最關鍵的部分就在接下來的LoadDriver()那句,加載了另一個驅動程序的DLL文件,就是上述註冊表中的USBDISK6.DLL文件,計數器增一,取到該文件中UsbDiskAttach函數及UsbDiskDetach函數的地址,註冊事件通知處理函數,然後調用了該DLL文件中的 UsbDiskAttach函數。

        由此可見,USB設備驅動程序有兩層功能,一方面是識別出指定的設備並進行配置,另一方面按照要求調用更高層的驅動程序來向操作系統提供接口。當調用了 USBDISK6.DLL後,操作系統就會按該文件中的程序以一個磁盤的形式或文件夾的形式進行處理,通過文件系統的操作,就可以對其進行讀寫控制了。我們也可以看一下HID設備的這個函數,它也是通過這種方式讓操作系統知道把USB設備識別成鼠標設備的。

        前文我們說過還有一個通知消息的回調函數,我們在剛纔的程序體中已經發現通過:
UsbFuncs->lpRegisterNotificationRoutine( hDevice, UsbDeviceNotify, pUsbDevice );
語句已經對這個函數進行了設置。我們再向下來看一下這個函數的函數體。這個函數很簡單,只要對USB_CLOSE_DEVICE消息進行處理,既然是要關閉 USB設備,那麼調用USBDISK6.DLL中的Detach函數是必須的,讓上層的驅動程序進行釋放,然後將引用計數減一,如果不再有設備引用此驅動程序,則FreeLibrary(),僅此而已。

        其餘的函數可以再仔細研究一下,在此就不詳細描述了,接下來我們要弄明白的就是到底操作系統是如何通過抽象的DISK讀寫具體的設備呢?

帶着上次留下的疑問,我們繼續來學習操作系統如何通過USBDISK讀寫USB設備的。我們先看USB/CLASS/STORAGE/DISK /SCSI2/usbdisk6.def文件。在這個文件中可以看到,該DLL一共導出了14個函數,其中兩個是上次內容當中被設備驅動程序調用的 UsbDiskAttach和UsbDiskDetach,餘下的是一組以DSK開頭的流驅動接口,易見,USBDISK是以流驅動的形式向操作系統提供服務的。

        爲了清晰起見,以下大量的程序我們並不學習,而只關心設備讀寫,因此我們來看DISK.C這個程序文件。找到DSK_Read和DSK_Write兩個函數,令我們大失所望,因爲這兩個函數都是形如
UNREFERENCED_PARAMETER(pDevice);
UNREFERENCED_PARAMETER(pBuffer);
UNREFERENCED_PARAMETER(BufferLength);

DEBUGMSG(ZONE_ERR,(TEXT("DSK_Read/n")));
SetLastError(ERROR_INVALID_FUNCTION);
return 0;
這樣的實現,也就是說用戶無法通過常規的ReadFile和WriteFile函數使用這個設備,那怎麼辦?是否意味着這個DISK無法讀寫呢?當然不是,我們應該馬上想到DSK_IOControl()這個函數,當遇到某些設備無法用常規的文件操作函數操作時,我們有DeviceIoControl()用戶函數可以使用,而這個函數就會調用到驅動程序中的DSK_IOControl函數。

        在這個函數中,我們找到了對IOCTL_DISK_READ等命令的處理程序,其中最關鍵的一句就是ScsiRWSG(pDevice, pSgReq, pDevice->Lun, bRead),即調用了一個ScsiRWSG的函數。

        在Scsi2.c這個程序中,我們找到了這個函數,其中SG指的是一種讀寫緩衝區的數據結構,實際上就是帶有緩衝區及長度的一個結構體,是CE下磁盤設備通用的讀寫數據結構,可以在diskio.h中找到它的定義。在這個函數中我們發現它再次調用了ScsiReadWrite()這個函數進行讀寫操作,找到這個函數,裏面有我們最重要的一行調用,即調用了UsbsDataTransfer()函數,還記得這個函數在哪見過嗎?沒錯,就是在USB設備的驅動程序當中。

        通過這一過程我們發現,那些Scsi的函數都只是在準備一些緩衝區、數據結構等,並沒有對硬件進行操作,真正要操作硬件設備的還是由驅動程序來完成的,可見,設備驅動程序是有着很強層次結構的,下層是專門針對物理設備的,上層是針對操作系統的抽象設備的,下層是U盤等物理實體,上層是文件夾,二者通過一定的通信或調用機制完成了設備在操作系統下的正常工作。
        回到usbmsc.c程序中來,找到UsbsDataTransfer函數,這個函數很簡單,根據傳輸協議調用CBIT_DataTransfer()或BOT_DataTransfer() 即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章