(江西財經大學 現代經濟管理學院,南昌330013)
Linux內核支持兩種主要類型的USB驅動程序:宿主(host)系統上的驅動程序和設備(device)上的驅動程序。從宿主的觀點來看(一個普通的USB宿主是一個桌面計算機),宿主系統的USB驅動層次控制插入其中的USB設備,而USB設備的驅動程序控制該設備如何作為一個USB設備和主機通信。設備驅動程序一般存放在內核的drivers/usb/gadget目錄中,針對某個具體設備的USB驅動程序一般都是指宿主設備驅動,本文的USB讀卡器驅動就是這一類。在Linux內核中,USB設備驅動是分很多層的,驅動層只是整個框架中的一小部分,要所有層的配合才能將設備驅動起來。USB驅動程序存在于不同的內核子系統(塊設備、網絡設備、字符設備等)和USB硬件控制器中。
USB核心為USB驅動程序提供了一個用于訪問和控制USB硬件的接口,而不必考慮系統當前存在的各種不同類型的USB硬件控制器,在USB 核心中對USB需要使用的各種資源進行分配及初始化,如注冊USB總線usb_bus_type、初始化USB主控制器、初始化USB HUB等;同時還提供了一些接口給其它層使用,如usb_alloc_dev函數(當有USB設備插入時,用于分配并初始化usb_device)。USB控制器主要負責處理各個USB設備的通信,解析驅動發送給每個設備的請求,并按照請求通知USB設備進行相應的處理,同時發送處理結果給驅動程序,并調用驅動傳入的complete函數。
USB設備是一個非常復雜的東西。如圖1所示,USB設備由配置、接口、端點組成,而USB驅動是綁定到接口上的,每個接口對應于一個設備驅動,對于某些多接口的設備(如帶音頻接口的USB鍵盤),此設備就同時需要USB鍵盤和USB音頻這2個驅動。

圖1 USB設備框架
如圖1所示,一個USB設備通常包含一個或多個配置,一個配置通常包含一個或多個接口,一個接口通常包含0個或多個端點。層次結構如圖2所示。

圖2 USB設備層次結構
(1)端點
USB通信的最基本形式是通過端點來實現的,驅動和接口的通信都是通過端點,一個接口可以有0個或多個端點,端點根據傳輸方式的不同分為以下4種:控制端點、中斷端點、等時端點和批量端點,分別對應于4種不同傳輸方式。
控制端點主要通過向USB設備發送請求來設置或獲取USB設備的配置、狀態等信息,每個USB設備都有一個名為“端點0”的控制端點,USB核心使用該端點在插入時進行設備的配置;中斷端點主要實現以一個固定的速率來傳輸少量的數據,中斷端點是USB鍵盤、鼠標等設備所使用的主要傳輸方式;批量端點用于傳輸大量數據,這些端點通常比中斷端點大得多,常用于需要確保沒有數據丟失的設備(如打印機、存儲設備、網絡設備);等時端點同樣可以傳輸大批量數據,當數據是否到達沒有保證,等時端點用于可以應付數據丟失的設備,這類設備更注重于保持一個恒定的數據流(如USB音頻、視頻)。
(2)接口
USB端點被捆綁為接口,USB接口對應于一個設備驅動,對應于一種功能的設備,對于多接口的復合設備就需要多個驅動。
(3)配置
USB接口被捆綁為配置,一個USB設備可以有多個配置,而且可以在配置之間切換已改變設備的狀態。例如一些允許下載固件到其上的設備包含多個配置以完成這個工作,而某一個時刻只能激活一個配置。
Linux內核中的USB代碼通過一個稱為urb(USB請求塊)的東西和所有的USB設備通信。這個請求塊使用struct urb結構體來描述,可以從include/linux/usb.h文件中找到。urb被用來以一種異步的方式向/從特定的USB 設備上的特定USB端點發送/接收數據。USB設備驅動程序可能會為單個端點分配許多urb,也可能對許多不同的端點重用單個的urb,這取決于驅動程序的需要。設備中的每個端點都可以處理一個urb隊列,所以多個urb可以在隊列為空之前發送到同一個端點。一個urb的典型生命周期如下:
①由USB設備驅動程序創建;②分配給一個特定USB 設備的特定端點;③由USB設備驅動程序遞交到USB 核心;④由USB核心遞交到特定設備的特定USB 主控制器驅動程序;⑤由USB主控制器驅動程序處理,從設備進行USB 傳送;⑥當urb結束之后,USB主控制器驅動程序通知USB設備驅動程序。
urb可以在任何時刻被遞交該urb的驅動程序取消掉,或者被USB核心取消,如果該設備已從系統中移除。urb被動態地創建,它包含一個內部引用計數,使得它們可以在最后一個使用者釋放它們時自動地銷毀。
struct urb結構體不能在驅動程序中或者另一個結構體中靜態地創建,因為這樣會破壞USB核心對urb所使用的引用計數機制。它必須使用usb_alloc_urb函數來創建。該函數原型如下:
struct urb *usb_alloc_urb(int iso_packets, int mem_flags);
第一個參數iso_packets是該urb應該包含的等時數據包的數量。如果不打算創建等時urb,該值應該設置為0。第二個參數mem_flags和傳遞給用于從內核分配內存的kmalloc函數的標志有相同的類型。如果該函數成功地為urb分配了足夠的內存空間,指向該urb的指針將被返回給調用函數。如果返回值為NULL,說明USB核心內發生了錯誤,驅動程序需要進行適當的清理。
當一個urb被創建之后,在它可以被USB核心使用之前必須被正確地初始化。
驅動程序必須調用usb_free_urb函數來告訴USB核心驅動程序已經使用完urb。該函數只有一個參數:
void usb_free_urb(struct urb *urb);
這個參數指向所需釋放的struct urb的指針。在該函數被調用之后,urb結構體就消失了,驅動程序不能再訪問它。
一旦urb被USB驅動程序正確地創建和初始化之后,就可以提交到USB核心以發送到USB設備了。這是通過調用usb_submit_urb函數來完成的:
int usb_submit_urb(struct urb *urb, int mem_flags);
urb參數是指向即將被發送到設備的urb的指針。mem_flags參數等同于傳遞給kmalloc調用的同一個參數,用于通知USB核心如何在此時及時地分配內存緩沖區。
當一個urb被成功地提交到USB核心之后,在接收函數被調用之前不能訪問該urb結構體中的任何字段。因為usb_submit_urb函數可以在任何時刻調用(包括從一個中斷上下文中),mem_flags變量的內容必須是正確的。其實只有三個有效的值可以被使用,取決于usb_submit_urb何時被調用:
(1)GFP_ATOMIC
只要下列條件成立就應該使用該值:
①調用者是在一個urb結束處理例程、中斷處理例程、底半部、tasklet或者定時器回調函數中。
②調用者正持有一個自旋鎖或讀寫鎖,注意如果持有了信號量,該值就不需要了。
③current->state不是TASK_RUNNING,該狀態永遠是TASK_RUNNING,除非驅動程序自己改變了當前的狀態。
(2)GFP_NOIO
如果驅動程序處于塊I/O路徑中應該使用該值,在所有存儲類型的設備的錯誤處理路徑中也應該使用它。
(3)GFP_KERNEL
該值應該在前述類別之外的所有情況中使用。
在寫USB驅動前,首先需要確定當前設備的一些基本信息,例如當前設備屬于哪類設備,有哪些接口,每個接口有哪些端點,設備、接口、端點、配置描述符的信息是什么,這些信息都可以通過軟件來獲取,常用軟件有windriver、USBtrace、bushound等,其中windriver不但可以看到設備的相關信息,還可以向設備發送標準請求,也可以直接向端點傳輸數據,通過此工具也可以驗證寫好的驅動通信是否正確。
U盤一般提供4個端點:一個控制端點、一個中斷端點、兩個批量端點,進入每個端點后就可以對各個端點進行操作了,對于控制端點可以對其發送各種USB標準請求,這里就可以發送獲取描述符的請求了,對于兩個bulk端點可以直接對其進行讀寫。
剛剛拿到讀卡器,首先當然是要獲取USB讀卡器的相關信息,通過工具USBtrace獲取各描述符信息如表1所列。

表1 USBtrace獲取各描述符信息

續表1
從這張表中可以獲取到一些有用的信息,USB設備的venderID為0x3EB,deviceID為0x6124,設備有兩個接口,第一個接口的類型為0x2,即Communications and CDC Control,這是一個通信類的接口,子類型為0x2 (Abstract Control Model),這個接口提供了1個端點,端點類型為中斷端點,方向為in。第二個接口類型為0xa(CDC DATA),這個接口一般用于傳輸數據,所以這個結構提供2個bulk端點,一個作為輸入,一個作為輸出。當然每個設備都會有控制端點,控制端點既可作為輸入又可作為輸出。每個端點都是最大允許傳輸數據大小,2個bulk端點最大可傳輸64 B,而中斷端點最大8 B。至此信息基本獲取完畢,開始寫驅動。
首先注冊usb_driver到設備模型,前面講了是通過函數usb_register實現的,下面就是實現usb_driver了,在這里只實現了4個成員:id_table、probe、disconnect、name。id_table就是利用前面獲取的venderID、deviceID來判斷設備,probe和disconnect就是相應的初始化和卸載函數。
2.2.1 初始化、卸載設備
初始化工作在probe函數中實現,主要就是針對前面獲取的端點信息,對每個端點進行初始化。首先要獲取設備的端點,從設備描述符中看到設備有2個bulk端點和1個interrupt端點。Probe函數傳入的是當前接口,即一個usb_interface結構體,如何從usb_interface中獲取端點信息呢?在usb_interface結構中有個成員cur_altsetting,表示當前設置,這是一個usb_host_interface結構體,其中又有一個成員endpoint,這是一個usb_host_endpoint結構體的數組,每一個元素代表一個端點,但這里不保存控制端點,因為控制端點是被單獨保存在設備結構體usb_device中的。
找到每個端點后開始為其創建相應管道。USB通信中有4種端點:控制、等時、中斷、批量,同時對應4種傳輸方式,也對應了4種管道,其中4種管道中又分in或out管道,內核中提供了8個宏來創建管道,定義如下:
#defineusb_sndctrlpipe(dev,endpoint)/*創建out控制管道*/
((PIPE_CONTROL << 30) | __create_pipe(dev,endpoint))
#defineusb_rcvctrlpipe(dev,endpoint)/*創建in控制管道*/
((PIPE_CONTROL << 30) | __create_pipe(dev,endpoint) | USB_DIR_IN)
#defineusb_sndisocpipe(dev,endpoint)/*創建out等時管道*/
((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev,endpoint))
#defineusb_rcvisocpipe(dev,endpoint)/*創建in等時管道*/
((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev,endpoint) |USB_DIR_IN)
#defineusb_sndbulkpipe(dev,endpoint)/*創建out批量管道*/
((PIPE_BULK << 30) | __create_pipe(dev,endpoint))
#defineusb_rcvbulkpipe(dev,endpoint)/*創建in批量管道*/
((PIPE_BULK << 30) | __create_pipe(dev,endpoint) | USB_DIR_IN)
#defineusb_sndintpipe(dev,endpoint)/*創建out中斷管道*/
((PIPE_INTERRUPT << 30) | __create_pipe(dev,endpoint))
#defineusb_rcvintpipe(dev,endpoint)/*創建in中斷管道*/
((PIPE_INTERRUPT << 30) | __create_pipe(dev,endpoint) | USB_DIR_IN)
其中__create_pipe也是一個宏,定義如下:
static inline unsigned int __create_pipe(struct usb_device *dev,
unsigned int endpoint){
return (dev->devnum << 8) | (endpoint << 15);
}
從以上幾個宏可以知道管道的組成,其實管道就是提供了一個通信地址,讓HC知道這個urb包應該發到哪個設備、哪個端點、是什么類型的包。在這里看到了4個宏,其中PIPE_ISOCHRONOUS就是標志等時通道、PIPE_INTERRUPT就是中斷通道、PIPE_CONTROL就是控制通道、PIPE_BULK 就是BULK通道。
在內核里使用一個unsigned int類型的變量來表征一個pipe,其中8~14位是設備號,即devnum,15~18位是端點號,即endpoint。宏USB_DIR_IN用來在pipe里面標志數據傳輸方向,一個管道要么只能輸入,要么只能輸出。在pipe里面,第7位(bit 7)是表征方向的。所以這里0x80也就是說讓bit 7 為1,這就表示傳輸方向是由設備向主機,也就是所謂的in,而如果這一位是0,就表示傳輸方向是由主機向設備的,也就是所謂的out。正是因為USB_DIR_OUT是0,而USB_DIR_IN是1,所以定義管道的時候只用到了USB_DIR_IN,而沒有用到USB_DIR_OUT,因為它是0,任何數和0相或都沒有意義。

圖3 循環緩沖區
接下來為每一個端點分配一個緩沖區,這個緩沖區就是urb結構體中的transfer_buffer,這個緩沖區就是傳輸過程中用來保存數據的,這時使用的分配函數是usb_buffer_alloc,這個函數的作用就是先分配一段內存空間,然后對其進行dma映射,就是從dma緩沖池分配一段內存,即usb_hcd->pool[i]。
最后將接口注冊到字符設備層,通過函數usb_register_dev實現,由于客戶需要定位每個USB口,這里筆者做了一個封裝,做了一點點簡單的改動,下面會講到。
2.2.2 讀、寫數據
所謂讀、寫設備就是實現file_operations中的read、write函數。
這里的read、write就是實現對usb的讀、寫數據和USB通信,這里在一個接口函數基礎上筆者做了一個簡單的封裝,根據實際需求改進了一點小地方,函數聲明如下:
int usb_reader_bulk_msg(struct urb *urb, struct usb_reader_data *usb_reader, unsigned int pipe,void *data, int len,int *actual_length, int timeout);
函數傳入7個參數,第一個參數為需要發送的urb包,這個urb在probe函數中已分配好內存,usb_reader就是讀卡器相關信息結構體,這是為該驅動專門定義的結構體,data是傳輸數據的緩沖區,len為希望傳輸多少數據,actual_length為實際傳輸的數據大小,timeout為傳輸的超時值,若在timeout內未完成傳輸則返回傳輸失敗。
這個函數主要實現填充傳入的urb,主要填充管道、緩沖區、傳輸數據長度、dma緩沖區、complete函數等成員,最后通過usb_submit_urb將urb提交到HC層做處理,usb_submit_urb在講HC層時已經分析過了,這里只要提交過去就行了。
在實現讀寫函數時,使用了一個4 096 B大小的循環緩沖區,每次應用程序調用write時,先將應用程序傳入的數據寫到USB讀卡器,然后馬上從讀卡器上讀取返回數據,并將返回數據寫到緩沖區中。當應用程序調用read時,將循環緩沖區的數據返回給用戶,而不是直接從設備讀取數據再返回。這樣處理的好處就是允許用戶進行多次連續寫入操作,最后只要通過一個讀取操作就能把前幾次寫入操作的結果全部讀取出去。
循環緩沖區寫入時將數據存入數組的尾部,讀取時從數組的另一端開始,當寫入數據到達數組的尾部時,回到數組頭部繼續寫。因此,一個循環緩沖區需要一個數組以及兩個索引值:一個用于下一個要寫入的數據的位置,另一個用于指定下一個要從緩沖區中移走的位置。
如圖3所示,這個緩存被定義成一個空情況,由讀寫指針相同來指示, 而滿情況發生在寫指針緊跟在讀指針后面的時候(小心解決繞回!)。
2.2.3 ioctl函數
這里還提供了一個ioctl接口,主要用于實現USB的標準請求,USB標準請求都是通過控制端點來實現的,控制端點的管道創建比較特殊,最后一個參數為0,創建函數如下:
usb_rcvctrlpipe(usb_reader->dev, 0);
usb_sndctrlpipe(usb_reader->dev, 0);
標準請求和請求參數通過setup包傳送給USB設備,一個setup包由8個字節組成,在內核中用usb_ctrlrequest結構體表示,結構體定義如下:
struct usb_ctrlrequest{
__u8bRequestType;
__u8bRequest;
__le16wValue;
__le16wIndex;
__le16wLength;
} __attribute__((packed));
usb_ctrlrequest被保存在urb->setup_packet里,共8個字節,意義如下:
byte0:bmRequestType,注意在剛才代碼中數據結構struct ctrlrequest 里邊寫的是bRequestType,但是它們對應的是相同的內容。而之所以USB協議里寫成bmRequestType,是因為它實際上又是一個位圖(m表示map),也就是說,盡管它只有1個字節,但是仍然被當作8位來用。
USB協議定義了11個標準請求,各請求對應setup包的各成員的值。
每個標準請求對應的代碼見表2,這個表對應的代碼和內核中為每個請求定義的宏是一一對應的。
對于DESCRIPTOR設置或獲取請求,需要填充一個描述符種類或索引,USB設備描述符對應的索引見表3。
表中列出了USB設備的5大描述符,USB設備的所有信息都保存在這些描述符中,在內核中為這5大描述符分別定義了相應的結構體,分別是usb_device_descriptor、usb_config_descriptor、usb_string_descriptor、usb_interface_descriptor、usb_endpoint_descriptor,上面提到的一些標準請求就是圍繞這些結構體來展開的。

表2 USB標準請求對應代碼

表3 設備描述符代碼
