天天看點

十四、Linux驅動之USB驅動分析1. 基本概念2. USB驅動分析

1. 基本概念

1.1 簡介

    USB是英文"Universal Serial Bus"的縮寫,意為"通用串行總線"。USB最初是為了替代許多不同的低速總線(包括并行、串行和鍵盤連接配接)而設計的,它以單一類型的總線連接配接各種不同的類型的裝置。USB的發展已經超越了這些低速的連接配接方式,它現在可以支援幾乎所有可以連接配接到PC上的裝置。最新的USB規範修訂了理論上高達480Mbps的高速連接配接。

    USB的驅動可以分為3類:SoC的USB控制器的驅動,主機端USB裝置的驅動,裝置上的USB Gadget驅動,通常,對于USB這種标準化的裝置,核心已經将主機控制器的驅動編寫好了,裝置上的Gadget驅動通常隻運作固件程式而不是基于Linux, 是以驅動工程師的主要工作就是編寫主機端的USB裝置驅動。

1.2 熱拔插

    當一個USB裝置插入PC機,PC機怎麼知道有裝置插入? 如下圖所示,USB接口隻有4條線:VCC(5V),GND,D-,D+。 PC機的USB插孔的D-和D+資料線均連接配接15K歐姆的下拉電阻。而USB裝置端的D-或D+資料線連接配接1.5K歐姆的上拉電阻。當裝置插入PC機的時候,會将PC機的D-或D+端的電壓拉高,當PC機在D-或D+端檢測到高電平時,就知道有裝置插入了。如果是PC機D-端被拉高,接入的則是USB低速裝置;如果是PC機D+端被拉高,接入的則是USB全速或高速裝置,具體是全速裝置還是高速裝置,會由PC機和USB裝置發包握手确定。

十四、Linux驅動之USB驅動分析1. 基本概念2. USB驅動分析
十四、Linux驅動之USB驅動分析1. 基本概念2. USB驅動分析

                       USB低速裝置硬體接線圖                                                         USB全速(高速)裝置硬體接線圖            

1.3 傳輸類型

    控制傳輸(control):是每一個USB裝置必須支援的,通常用來擷取裝置描述符、設定裝置的狀态等等。一個USB裝置從插入到最後的拔出這個過程一定會産生控制傳輸(即便這個USB裝置不能被這個系統支援)。

    中斷傳輸(interrupt):支援中斷傳輸的典型裝置有USB滑鼠、 USB鍵盤等等。中斷傳輸不是說我的裝置真正發出一個中斷,然後主機會來讀取資料。它其實是一種輪詢的方式來完成資料的通信。USB裝置會在裝置驅動程式中設定一個參數叫做interval,它是endpoint的一個成員。 interval是間隔時間的意思,表示我這個裝置希望主機多長時間來輪詢自己,隻要這個值确定了之後,我主機就會周期性的來檢視有沒有資料需要處理。

    批量傳輸(bulk):支援批量傳輸最典型的裝置就是U盤,它進行大數量的資料傳輸,能夠保證資料的準确性,但是時間不是固定的。

    實時傳輸(isochronous):USB攝像頭就是實時傳輸裝置的典型代表,它同樣進行大數量的資料傳輸,資料的準确性無法保證,但是對傳輸延遲非常敏感,也就是說對實時性要求比較高。

1.4 USB裝置邏輯結構

    為了更好地描述USB裝置的特征,USB提出了裝置架構的概念。從這個角度來看,可以認為USB裝置是由一些配置、接口和端點組成(裝置通常有一個或多個配置,配置通常有一個或多個接口,接口通常有一個或多個設定,接口有零或多個端點)。其中,配置和接口是對USB裝置功能的抽象,實際的資料傳輸由端點來完成。在使用USB裝置前,必須指明其采用的配置和接口。這個步驟一般是在裝置接入主機時裝置進行枚舉時完成的。

    USB裝置與主機會有若幹個通信的”端點”,每個端點都有個端點号,除了端點0外,每一個端點隻能工作在一種傳輸類型下。傳輸方向都是基于USB主機的立場說的,比如:滑鼠的資料是從滑鼠傳到PC機,對應的端點稱為"中斷輸入端點"。其中端點0是裝置的預設控制端點, 既能輸出也能輸入,用于USB裝置的識别過程。

1.5 USB協定

    USB2.0協定中文版

2. USB驅動分析

2.1 USB驅動程式架構

USB驅動程式架構圖如下:

十四、Linux驅動之USB驅動分析1. 基本概念2. USB驅動分析

USB總線驅動程式的作用:

    1. 識别USB裝置

        1.1 配置設定位址

        1.2 并告訴USB裝置(set address)

        1.3 發出指令擷取描述符(描述符的資訊可以在include\linux\usb\Ch9.h看到  (Ch9是指USB規範的第九章))

    2. 查找并安裝對應的裝置驅動程式

    3. 提供USB讀寫函數

    USB總線上的所有通信都是由主機發起的,是以本質上,USB都是采用輪詢的方式進行的。USB總線會使用輪詢的方式不斷檢測總線上是否有裝置接入,如果有裝置接入相應的D+D-就會有電平變化。然後總線就會按照USB規定的協定與裝置進行通信,裝置将存儲在自身的裝置資訊依次交給主機,主機将這些資訊組織起來。上報到核心,核心中的USB子系統再去比對相應的驅動。

2.2 USB驅動分析

    當我們開發闆啟動核心(核心配置好了usbmouse相關驅動)後,開發闆接上一個USB滑鼠,序列槽端輸入如下資訊:

十四、Linux驅動之USB驅動分析1. 基本概念2. USB驅動分析

    從第一行輸出資訊的“USB device using”定位到核心的drivers/usb/core/hub.c檔案的hub_port_init()函數中

dev_info (&udev->dev,"%s %s speed %sUSB device using %s and address %d\n",
		  (udev->config) ? "reset" : "new", speed, type,
		  udev->bus->controller->driver->name, udev->devnum);
           

    這個hub其實就是我們的USB主機控制器的集線器,用來管理多個USB接口。繼續在核心中搜尋hub_port_init()函數被誰調用,最終調用層次如下:

    hub_thread()    //hub線程函數

        hub_events()    //hub事件函數

            hub_port_connect_change()    //hub端口連接配接函數

                hub_port_init()    //hub端口初始化函數

    hub_thread()函數如下:

static int hub_thread(void *__unused)
{

    do {
           hub_events();       //執行一次hub事件函數
           wait_event_interruptible(khubd_wait,!list_empty(&hub_event_list)||kthread_should_stop()); 
                             //每次執行一次hub事件,都會進入一次等待事件中斷函數
           try_to_freeze();            
    } while (!kthread_should_stop() || !list_empty(&hub_event_list));

    pr_debug("%s: khubd exiting\n", usbcore_name);
    return 0;
}
           

    從上面函數中得到, 要想執行hub_events(),都要等待khubd_wait這個中斷喚醒才行,查找核心調用層次如下:

        hub_irq()   

            kick_khubd()

                wake_up(&khubd_wait);    //喚醒hub_thread()函數中陷入的休眠

    hub_irq()函數在usb_fill_int_urb()函數的第三個參數傳入。當usb port上狀态發生變化(比如接入usb裝置),USB主機控制器就會産生一個hub_irq中斷,hub_irq()中又調用了kick_khubd()函數,在該函數裡調用了wake_up()函數喚醒hub_thread()函數中陷入的休眠,後面就會進行一系列的枚舉、注冊等過程。接下來詳細介紹喚醒hub_thread()函數後所做的工作。

2.2.1 hub_events()函數

喚醒hub_thread()函數後會再次執行do while循環,進入hub_events(),hub_events()函數的部分代碼如下:

static void hub_events(void)
{
   	... ...
	while (1) {

		/*
		 * 做了一些邏輯判斷,判斷hub是否連接配接,hub上是否有錯誤,
		 * 如果有錯誤就重新開機hub,如果沒有錯誤,接下來就對hub上的
		 * 每個port進行掃描,判斷各個port是否發生狀态變化,如果
		 * 産生了變化則調用hub_port_connect_change進行處理;
		 */
		... ...
		
		for (i = 1; i <= hub->descriptor->bNbrPorts; i++) {
			... ...
				
			if (connect_change)
				hub_port_connect_change(hub, i,
						portstatus, portchange);
		}
		... ...
	    } 
}
           

    該函數主要工作是周遊hub中的所有port,對各個port的狀态進行檢視,判斷port是否發生了狀态變化(這些狀态主要有:1.原先port上沒有裝置,現在檢測到有裝置連接配接;2.原先port上有裝置,由于控制器不正常關閉或禁止usb port等原圖使得該裝置處于NOATTACHED态),如果發現port的連接配接狀态發生變化或由于EMI導緻連接配接使能發生變化,即connect_change=1,則調用hub_port_connect_change()函數。

2.2.2 hub_port_connect_change()函數

    hub_port_connect_change()函數的部分代碼如下:

static void hub_port_connect_change(struct usb_hub *hub, int port1,u16 portstatus, u16 portchange)
{ 
  ... ...
  udev = usb_alloc_dev(hdev, hdev->bus, port1);          //配置設定設定一個usb_device結構體

  usb_set_device_state(udev, USB_STATE_POWERED);          //設定注冊的USB裝置的狀态标志
  ... ...

  choose_address(udev);                                  //給新的裝置配置設定一個位址編号
 
  status = hub_port_init(hub, udev, port1, i);          //初始化端口,與USB裝置建立連接配接
  ... ...

  status = usb_new_device(udev);                     //建立USB裝置,注冊到系統
  ... ...
}
           

    hub_port_connect_change()函數主要調用以下4個函數:

(1) usb_alloc_dev()函數:

usb_alloc_dev(struct usb_device *parent, struct usb_bus *bus, unsigned port1)
{
       struct usb_device *dev;

       dev = kzalloc(sizeof(*dev), GFP_KERNEL);    //配置設定一個usb_device裝置結構體
       ... ...
       device_initialize(&dev->dev);    //初始化usb_device
            
       dev->dev.bus = &usb_bus_type;    //設定usb_device的成員device->bus等于usb_bus總線

       dev->dev.type = &usb_device_type;    //設定usb_device的成員device->type等于usb_device_type                    
       ... ...
       return dev;    //傳回一個usb_device結構體
}
           

(2) choose_address()函數:

static void choose_address(struct usb_device *udev)
{
    int devnum;
    struct usb_bus *bus = udev->bus;

    devnum = find_next_zero_bit(bus->devmap.devicemap, 128,bus->devnum_next);    //在bus->devnum_next~128區間中,循環查找下一個非0(沒有裝置)的編号

    if (devnum >= 128)    //若編号大于等于128,說明沒有找到空餘的位址編号,從頭開始找
        devnum = find_next_zero_bit(bus->devmap.devicemap, 128, 1);
    
    bus->devnum_next = ( devnum >= 127 ? 1 : devnum + 1);    //設定下次尋址的區間+1

    if (devnum < 128) {                                  
        set_bit(devnum, bus->devmap.devicemap);    //設定位
        udev->devnum = devnum;                 
    }
}
           

    這裡配置設定了位址編号,核心啟動時會進來一次,編号1被使用了,是以接上一個USB滑鼠時列印出的位址編号為2,下次配置設定的位址編号遞增,直到127再從1開始找沒有使用的位址編号。拔插兩次USB滑鼠輸出如下圖:

十四、Linux驅動之USB驅動分析1. 基本概念2. USB驅動分析

(3) hub_port_init()函數:

static int hub_port_init (struct usb_hub *hub, struct usb_device *udev, int port1,int retry_counter)
{
    ... ...
    dev_info (&udev->dev,
        "%s %s speed %sUSB device using %s and address %d\n",
        (udev->config) ? "reset" : "new", speed, type,
        udev->bus->controller->driver->name, udev->devnum);    //對應前面列印的USB裝置資訊
    ... ...
    for (j = 0; j < SET_ADDRESS_TRIES; ++j) 
    {
       retval = hub_set_address(udev);     //設定位址,告訴USB裝置新的位址編号

       if (retval >= 0)
                break;
       msleep(200);
    }
    retval = usb_get_device_descriptor(udev, 8);    //獲得USB裝置描述符前8個位元組
    ... ...

    retval = usb_get_device_descriptor(udev, USB_DT_DEVICE_SIZE);    //重新擷取裝置描述符資訊
    ... ...
}
           

    擷取描述符時還不知道端點0一次性傳輸的包大小是多少,可以通過先獲得描述符的第8個位元組得到端點0一次性傳輸的包大小,後面再以該包大小重讀一次目标裝置的裝置描述符。這裡提到了裝置描述符,該資料結構如下:

struct usb_device_descriptor {
 __u8  bLength;                          //本描述符的size
 __u8  bDescriptorType;                //描述符的類型,這裡是裝置描述符DEVICE
 __u16 bcdUSB;                           //指明usb的版本,比如usb2.0
 __u8  bDeviceClass;                    //類
 __u8  bDeviceSubClass;                 //子類
 __u8  bDeviceProtocol;                  //指定協定
 __u8  bMaxPacketSize0;                 //端點0對應的最大包大小
 __u16 idVendor;                         //廠家ID
 __u16 idProduct;                        //産品ID
 __u16 bcdDevice;                        //裝置的釋出号
 __u8  iManufacturer;                    //字元串描述符中廠家ID的索引
 __u8  iProduct;                         //字元串描述符中産品ID的索引
 __u8  iSerialNumber;                   //字元串描述符中裝置序列号的索引
 __u8  bNumConfigurations;               //可能的配置的數目
} __attribute__ ((packed));
           

(4) usb_new_device()函數:

int usb_new_device(struct usb_device *udev)
{
    int err;
    ... ...
    err = usb_get_configuration(udev);    //擷取并解析配置資訊
    ... ...
    err = device_add(&udev->dev);    //把device放入bus的dev連結清單中,并尋找對應的裝置驅動
}
           

    usb_get_configuration()函數裡擷取得到的資訊裡它包含了多種資訊:配置,相關聯接口,接口,端口等,再将這些資訊把它補全到之前申請的usb_host_config結構裡,把各類資訊放到相應的結構中。最後調用device_add()函數,使用裝置模型機制,将該USB裝置注冊到核心中去。

2.2.3 device_add()函數

    device_add()函數部分代碼如下:

int device_add(struct device *dev)
{
    dev = get_device(dev);         //使dev等于usb_device下的device成員
    ... ...
    if ((error = bus_add_device(dev))) // 把這個裝置添加到dev->bus的device連結清單中
              goto BusError;
    ... ...
    bus_attach_device(dev);           //來比對USB總線上的usb_drv
    ... ...
}
           

    與之前章節講解的platform總線相同,bus_attach_device會調用到該裝置總線類型裡的成員.match比對函數,與該總線上的所有的usb_drv進行比對,對于本節的USB驅動則是usb_bus_type,該資料結構如下:

struct bus_type usb_bus_type = {
	.name =		"usb",
	.match =	usb_device_match,    //比對函數
	.uevent =	usb_uevent,
	.suspend =	usb_suspend,
	.resume =	usb_resume,
};
           

    比對函數usb_device_match()部分代碼如下:

static int usb_device_match(struct device *dev, struct device_driver *drv)
{
	if (is_usb_device(dev)) {	//判斷是不是USB裝置
	      if (!is_usb_device_driver(drv))
	             return 0;
	      return 1;
	 }
	else {	//否則就是USB驅動或者USB裝置的接口
	      struct usb_interface *intf;
	      struct usb_driver *usb_drv;
	      const struct usb_device_id *id;           

	      if (is_usb_device_driver(drv))		//如果是USB驅動,就不需要比對,直接return
	             return 0; 

	      intf = to_usb_interface(dev);		//擷取USB裝置的接口
	      usb_drv = to_usb_driver(drv);		//擷取USB驅動

	      id = usb_match_id(intf, usb_drv->id_table);	//比對USB驅動的成員id_table
	      if (id)
	             return 1;

	      id = usb_match_dynamic_id(intf, usb_drv);
	      if (id)
	      	return 1;
	}
	return 0;
}
           

    而其中的usb_match_id()函數最終又調用到了usb_match_device()函數,部分代碼如下:

/* 傳回0比對成功,傳回1比對失敗 */
int usb_match_device(struct usb_device *dev, const struct usb_device_id *id)
{
        ... ...

	if ((id->match_flags & USB_DEVICE_ID_MATCH_DEV_CLASS) &&
	    (id->bDeviceClass != dev->descriptor.bDeviceClass))
		return 0;

	if ((id->match_flags & USB_DEVICE_ID_MATCH_DEV_SUBCLASS) &&
	    (id->bDeviceSubClass!= dev->descriptor.bDeviceSubClass))
		return 0;

	if ((id->match_flags & USB_DEVICE_ID_MATCH_DEV_PROTOCOL) &&
	    (id->bDeviceProtocol != dev->descriptor.bDeviceProtocol))
		return 0;

	return 1;
}
           

    即比對前面提到的裝置描述符裡的三個成員:

struct usb_device_descriptor {
... ...
 __u8  bDeviceClass;                    //類
 __u8  bDeviceSubClass;                 //子類
 __u8  bDeviceProtocol;                  //指定協定
... ...
} __attribute__ ((packed));
           

    參考/drivers/hid/usbhid/usbmouse.c(核心自帶的USB滑鼠驅動),看核心是如何使用比對參數的:

/*
#define USB_INTERFACE_INFO(cl,sc,pr) \
	.match_flags = USB_DEVICE_ID_MATCH_INT_INFO, .bInterfaceClass = (cl), \
	.bInterfaceSubClass = (sc), .bInterfaceProtocol = (pr)
*/

static struct usb_device_id usb_mouse_id_table [] = {
	{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,
		USB_INTERFACE_PROTOCOL_MOUSE) },
	{ }	/* Terminating entry */
};

static struct usb_driver usb_mouse_driver = {
	.name		= "usbmouse",
	.probe		= usb_mouse_probe,
	.disconnect	= usb_mouse_disconnect,
	.id_table	= usb_mouse_id_table,
};
           

是以可以看出對于USB驅動device_add的作用:

    1. 把device放入usb_bus_type的dev連結清單

    2. 從usb_bus_type的driver連結清單裡取出usb_driver

    3. 把usb_interface和usb_driver的id_table比較

    4. 如果能比對,調用usb_driver的probe

2.2.4 總結

整體架構圖如下:

十四、Linux驅動之USB驅動分析1. 基本概念2. USB驅動分析

    USB總線驅動程式在我們接入USB裝置的時候會幫我們構造一個新的usb_device注冊到總線裡面來。我們插上任意的USB裝置,就發現核心列印出一些資訊,說明USB總線驅動程式、USB裝置程式已經做好,後面隻需要編寫USB驅動程式,即下圖中的usb_driver 。(是以,以後寫USB驅動程式,寫的就是USB接口描述符方面的)

十四、Linux驅動之USB驅動分析1. 基本概念2. USB驅動分析

繼續閱讀