天天看點

Linux USB驅動架構分析

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)進行清理。

繼續閱讀