Linux USB驅動架構分析(一)
事實上,Linux的裝置驅動都遵循一個慣例——表征驅動程式的結構體,結構體裡面應該包含了驅動程式所需要的所有資源。用術語來說,就是這個驅動器對象所擁有的屬性及成員。這個結構體的名字由驅動開發人員決定,比如說,滑鼠可能有一個叫做mouse_dev的struct,鍵盤可能由一個keyboard_dev的struct。而這次我們來分析一下Linux核心源碼中的一個usb-skeleton,自然它定義的裝置結構體就叫做usb-skel:
struct usb_skel {
struct usb_device * udev; /* the usb device for this device */
struct usb_interface * interface; /* the interface for this device */
struct semaphore limit_sem; /* limiting the number of writes in progress */
unsigned char * bulk_in_buffer; /* the buffer to receive data */
size_t bulk_in_size; /* the size of the receive buffer */
__u8 bulk_in_endpointAddr; /* the address of the bulk in endpoint */
__u8 bulk_out_endpointAddr; /* the address of the bulk out endpoint */
struct kref kref;
};
USB能夠自動監測裝置,并調用相應的驅動程式處理裝置,是以其規範實際上是相當複雜的,幸好,我們不必理會大部分細節問題,因為Linux已經提供相應的解決方案。USB的驅動分為兩塊,一塊是USB的bus驅動,這個東西Linux核心已經做好了,可以不管,但我們至少要了解它的功能。形象的說,USB的bus驅動相當于鋪出一條路來,讓所有的資訊都可以通過這條USB通道到達該到的地方,這部分工作由usb_core來完成。當USB裝置接到USB控制器接口時,usb_core就檢測該裝置的一些資訊,例如生産廠商ID和産品的ID,或者是裝置所屬的class、subclass跟protocol,以便确定應該調用哪一個驅動處理該裝置。另一塊工作是usb的裝置驅動。也就是說,我們就等着usb_core告訴我們要工作了,我們才工作。
從開發人員的角度看,每一個usb裝置有若幹個配置(configuration)組成,每個配置又可以有多個接口(interface),每個接口又有多個設定,而接口本身可能沒有端點或者多個端點(end point)。USB的資料交換通過端點來進行,主機與各個端點之間建立起單向的管道來傳輸資料。而這些接口可以分為四類:
控制(control)
用于配置裝置、擷取裝置資訊、發送指令或者擷取裝置的狀态報告。
中斷(interrupt)
當USB宿主要求裝置傳輸資料時,中斷端點會以一個固定的速率傳送少量資料,還用于發送資料到USB裝置以控制裝置,一般不用于傳送大量資料。
批量(bulk)
用于大量資料的可靠傳輸,如果總線上的空間不足以發送整個批量包,它會被分割成多個包傳輸。
等時(isochronous)
大量資料的不可靠傳輸,不保證資料的到達,但保證恒定的資料流,多用于資料采集。
Linux中用struct usb_host_endpoint來描述USB端點,每個usb_host_endpoint中包含一個struct usb_endpoint_descriptor結構體,當中包含該端點的資訊以及裝置自定義的各種資訊,這些資訊包括:
bEndpointAddress(b for byte)
8位端點位址,其位址還隐藏了端點方向的資訊(之前說過,端點是單向的),可以用掩碼USB_DIR_OUT和USB_DIR_IN來确定。
bmAttributes
端點的類型,結合USB_ENDPOINT_XFERTYPE_MASK可以确定端點是USB_ENDPOINT_XFER_ISOC(等時)、USB_ENDPOINT_XFER_BULK(批量)還是USB_ENDPOINT_XFER_INT(中斷)。
wMaxPacketSize
端點一次處理的最大位元組數。發送的BULK包可以大于這個數值,但會被分割傳送。
bInterval
如果端點是中斷類型,該值是端點的間隔設定,以毫秒為機關。
在邏輯上,一個USB裝置的功能劃分是通過接口來完成的。比如說一個USB揚聲器,可能會包括有兩個接口:一個用于鍵盤控制,另外一個用于音頻流傳輸。而事實上,這種裝置需要用到不同的兩個驅動程式來操作,一個控制鍵盤,一個控制音頻流。但也有例外,比如藍牙裝置,要求有兩個接口,第一用于ACL跟EVENT的傳輸,另外一個用于SCO鍊路,但兩者通過一個驅動控制。
在Linux上,接口使用struct usb_interface來描述,以下是該結構體中比較重要的字段:
struct usb_host_interface *altsetting(注意不是usb_interface)
其實據我了解,他應該是每個接口的設定,雖然名字上有點奇怪。該字段是一個設定的數組(一個接口可以有多個設定),每個usb_host_interface都包含一套由struct usb_host_endpoint定義的端點配置。但這些配置次序是不定的。
unsigned num_altstting
可選設定的數量,即altsetting所指數組的元素個數。
struct usb_host_interface *cur_altsetting
目前活動的設定,指向altsetting數組中的一個。
int minor
當捆綁到該接口的USB驅動程式使用USB主裝置号時,USB core配置設定的次裝置号。僅在成功調用usb_register_dev之後才有效。
Linux USB驅動架構分析(二)
事實上,Linux的裝置驅動,特别是這種hotplug的USB裝置驅動,會被編譯成子產品,然後在需要時挂在到核心。要寫一個Linux的子產品并不複雜,以一個helloworld為例:
#include
MODULE_LICENSE(“GPL”);
static int hello_init(void)
{
printk(KERN_ALERT “Hello World!\n”);
return 0;
}
static int hello_exit(void)
printk(KERN_ALERT “GOODBYE!\n”);
module_init(hello_init);
module_exit(hello_exit);
這個簡單的程式告訴大家應該怎麼寫一個子產品,MODULE_LICENSE告訴核心該子產品的版權資訊,很多情況下,用GPL或者BSD,或者兩個,因為一個私有子產品一般很難得到社群的幫助。module_init和module_exit用于向核心注冊子產品的初始化函數和子產品推出函數。如程式所示,初始化函數是hello_init,而退出函數是hello_exit。
另外,要編譯一個子產品通常還需要用到核心源碼樹中的makefile,是以子產品的Makefile可以寫成:
ifneq ($(KERNELRELEASE),)
obj-m:= hello.o#usb-dongle.o
else
KDIR:= /usr/src/linux-headers-$(shell uname -r)
BDIR:= $(shell pwd)
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
.PHONY: clean
clean:
make -C $(KDIR) M=$(BDIR) clean
endif
可以用insmod跟rmmod來驗證子產品的挂在跟解除安裝,但必須用root的身份登陸指令行,用普通使用者加su或者sudo在Ubuntu上的測試是不行的。
Linux USB驅動架構分析(三)
下面分析一下usb-skeleton的源碼。這個範例程式可以在linux-2.6.17/drivers/usb下找到,其他版本的核心程式源碼可能有所不同,但相差不大。大家可以先找到源碼看一看,先有個整體印象。
之前已經提到,子產品先要向核心注冊初始化跟銷毀函數:
static int __init usb_skel_init(void)
int result;
/* register this driver with the USB subsystem */
result = usb_register(&skel_driver);
if (result)
err("usb_register failed. Error number %d", result);
return result;
static void __exit usb_skel_exit(void)
/* deregister this driver with the USB subsystem */
usb_deregister(&skel_driver);
module_init (usb_skel_init);
module_exit (usb_skel_exit);
MODULE_LICENSE("GPL");
從代碼開來,這個init跟exit函數的作用隻是用來注冊驅動程式,這個描述驅動程式的結構體是系統定義的标準結構struct usb_driver,注冊和登出的方法很簡單,usb_register(struct *usb_driver), usb_deregister(struct *usb_driver)。該結構體要向系統提供幾個函數入口,跟驅動的名字:
static struct usb_driver skel_driver = {
.name = "skeleton",
.probe = skel_probe,
.disconnect = skel_disconnect,
.id_table = skel_table,
從代碼看來,usb_driver需要初始化四個東西:子產品的名字skeleton,probe函數skel_probe,disconnect函數skel_disconnect,以及id_table。
在解釋skel_driver各個成員之前,我們先來看看另外一個結構體。這個結構體的名字由開發人員自定義,它描述的是該驅動擁有的所有資源及狀态:
struct usb_device * udev; /* the usb device for this device */
struct usb_interface * interface; /* the interface for this device */
struct semaphore limit_sem; /* limiting the number of writes in progress */
unsigned char * bulk_in_buffer; /* the buffer to receive data */
size_t bulk_in_size; /* the size of the receive buffer */
__u8 bulk_out_endpointAddr; /* the address of the bulk out endpoint */
它擁有一個描述usb裝置的結構體udev,一個接口interface,用于并發通路控制的semaphore(信号量) limit_sem,用于接收資料的緩沖bulk_in_buffer及其尺寸bulk_in_size,然後是批量輸入輸出端口位址bulk_in_endpointAddr、bulk_out_endpointAddr,最後是一個核心使用的引用計數器。
再回過頭來看看skel_driver:
name用來告訴核心子產品的名字是什麼,這個注冊之後由系統來使用,跟我們關系不大;
id_table用來告訴核心該子產品支援的裝置;usb子系統通過裝置的production ID和vendor ID的組合或者裝置的class、subclass跟protocol的組合來識别裝置,并調用相關的驅動程式作處理。可以看看這個id_table到底是什麼東西:
/* Define these values to match your devices */
#define USB_SKEL_VENDOR_ID 0xfff0
#define USB_SKEL_PRODUCT_ID 0xfff0
/* table of devices that work with this driver */
static struct usb_device_id skel_table [] = {
{ USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) },
{ } /* Terminating entry */
MODULE_DEVICE_TABLE (usb, skel_table);
MODULE_DEVICE_TABLE的第一個參數是裝置的類型,如果是USB裝置,那自然是usb(如果是PCI裝置,那将是pci,這兩個子系統用同一個宏來注冊所支援的裝置)。後面一個參數是裝置表,這個裝置表的最後一個元素是空的,用于辨別結束。代碼定義了USB_SKEL_VENDOR_ID是0xfff0,USB_SKEL_PRODUCT_ID是0xfff0,也就是說,當有一個裝置接到集線器時,usb子系統就會檢查這個裝置的vendor ID和product ID,如果它們的值是0xfff0時,那麼子系統就會調用這個skeleton子產品作為裝置的驅動。
Linux USB驅動架構分析(四)
probe是usb子系統自動調用的一個函數,有USB裝置接到硬體集線器時,usb子系統會根據production ID和vendor ID的組合或者裝置的class、subclass跟protocol的組合來識别裝置調用相應驅動程式的probe(探測)函數,對于skeleton來說,就是skel_probe。系統會傳遞給探測函數一個usb_interface *跟一個struct usb_device_id *作為參數,分别是該USB裝置的接口描述(一般會是該裝置的第0号接口,該接口的預設設定也是第0号設定)跟它的裝置ID描述(包括Vendor ID、Production ID等)。probe函數比較長,我們分段來分析這個函數:
dev->udev = usb_get_dev(interface_to_usbdev(interface));
dev->interface = interface;
在初始化了一些資源之後,可以看到第一個關鍵的函數調用——interface_to_usbdev。本來,要得到一個usb_device隻要用interface_to_usbdev就夠了,但因為要增加對該usb_device的引用計數,我們應該在做一個usb_get_dev的操作,來增加引用計數,并在釋放裝置時用usb_put_dev來減少引用計數。該引用計數值是對該usb_device的計數,并不是對本子產品的計數,本子產品的計數要由kref來維護。是以,probe一開始就有初始化kref。事實上,kref_init操作不單隻初始化kref,還将其置設成1。是以在出錯處理代碼中有kref_put,它把kref的計數減1,如果kref計數已經為0,那麼kref會被釋放。kref_put的第二個參數是一個函數指針,指向一個清理函數。注意,該指針不能為空,或者kfree。該函數會在最後一個對kref的引用釋放時被調用。下面是核心源碼中的一段注釋及代碼:
/**
* kref_put - decrement refcount for object.
* @kref: object.
* @release: pointer to the function that will clean up the object when the
* last reference to the object is released.
* This pointer is required, and it is not acceptable to pass kfree
* in as this function.
*
* Decrement the refcount, and if 0, call release().
* Return 1 if the object was removed, otherwise return 0. Beware, if this
* function returns 0, you still can not count on the kref from remaining in
* memory. Only use the return value if you want to see if the kref is now
* gone, not present.
*/
int kref_put(struct kref *kref, void (*release)(struct kref *kref))
WARN_ON(release == NULL);
WARN_ON(release == (void (*)(struct kref *))kfree);
/*
* if current count is one, we are the last user and can release object
* right now, avoiding an atomic operation on 'refcount'
*/
if ((atomic_read(&kref->refcount) == 1) ||
(atomic_dec_and_test(&kref->refcount))) {
release(kref);
return 1;
}
當我們執行打開操作時,要增加kref的計數,可以用kref_get來完成。所有對struct kref的操作都有核心代碼確定其原子性。
得到了該usb_device之後,我們要對我們自定義的usb_skel各個狀态跟資源作初始化。這部分工作的任務主要是向usb_skel注冊該usb裝置的端點。在一個usb_host_interface結構裡面有一個usb_interface_descriptor叫做desc的成員,用于描述該interface的一些屬性,其中bNumEndpoints是一個8位(b for byte)的數字,代表了該接口的端點數。probe然後周遊所有的端點,檢查他們的類型跟方向,注冊到usb_skel中。
/* set up the endpoint information */
/* use only the first bulk-in and bulk-out endpoints */
iface_desc = interface->cur_altsetting;
for (i = 0; i desc.bNumEndpoints; ++i) {
endpoint = &iface_desc->endpoint[i].desc;
if ( !dev->bulk_in_endpointAddr &&
((endpoint->bEndpointAddress & USB_ENDPOINT_DIR_MASK) = = USB_DIR_IN) &&
((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK) = = USB_ENDPOINT_XFER_BULK)) {
/* we found a bulk in endpoint */
buffer_size = le16_to_cpu(endpoint->wMaxPacketSize);
dev->bulk_in_size = buffer_size;
dev->bulk_in_endpointAddr = endpoint->bEndpointAddress;
dev->bulk_in_buffer = kmalloc(buffer_size, GFP_KERNEL);
if (!dev->bulk_in_buffer) {
err("Could not allocate bulk_in_buffer");
goto error;
}
}
if (!dev->bulk_out_endpointAddr &&
((endpoint->bEndpointAddress & USB_ENDPOINT_DIR_MASK)= =USB_DIR_OUT) &&
((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK)= = USB_ENDPOINT_XFER_BULK)) {
/* we found a bulk out endpoint */
dev->bulk_out_endpointAddr = endpoint->bEndpointAddress;
if (!(dev->bulk_in_endpointAddr && dev->bulk_out_endpointAddr)) {
err("Could not find both bulk-in and bulk-out endpoints");
goto error;
Linux USB驅動架構分析(五)
接下來的工作是向系統注冊一些以後會用的資訊。首先我們來說明一下usb_set_intfdata(),它向核心注冊一個data,這個data的結構可以是任意的,這段程式向核心注冊了一個usb_skel結構,就是我們剛剛看到的被初始化的那個,這個data可以在以後用usb_get_intfdata來得到。
usb_set_intfdata(interface, dev);
retval = usb_register_dev(interface, &skel_class);
然後我們向這個interface注冊一個skel_class結構。這個結構又是什麼?我們就來看看這到底是個什麼東西:
static struct usb_class_driver skel_class = {
.name = "skel%d",
.fops = &skel_fops,
.minor_base = USB_SKEL_MINOR_BASE,
它其實是一個系統定義的結構,裡面包含了一名字、一個檔案操作結構體還有一個次裝置号的基準值。事實上它才是定義 真正完成對裝置IO操作的函數。是以他的核心内容應該是skel_fops。usb裝置可以有多個interface,每個interface所定義的IO操作可能不一樣,是以向系統注冊的usb_class_driver要求注冊到某一個interface,而不是device,是以,usb_register_dev的第一個參數才是interface,而第二個參數就是某一個usb_class_driver。通常情況下,linux系統用主裝置号來識别某類裝置的驅動程式,用次裝置号管理識别具體的裝置,驅動程式可以依照次裝置号來區分不同的裝置,是以,這裡的次裝置好其實是用來管理不同的interface的。
static struct file_operations skel_fops = {
.owner = THIS_MODULE,
.read = skel_read,
.write = skel_write,
.open = skel_open,
.release = skel_release,
這個檔案操作結構中定義了對裝置的讀寫、打開、釋放(USB裝置通常使用這個術語release)。他們都是函數指針,分别指向skel_read、skel_write、skel_open、skel_release這四個函數,這四個函數應該由開發人員自己實作。
當裝置被拔出集線器時,usb子系統會自動地調用disconnect,它做的事情不多,最重要的是登出class_driver(交還次裝置号)和interface的data:
dev = usb_get_intfdata(interface);
usb_set_intfdata(interface, NULL);
/* give back our minor */
usb_deregister_dev(interface, &skel_class);
然後會用kref_put(&dev->kref, skel_delete)進行清理。