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裝置發包握手确定。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL4gTO1UjMxUTMwMTMxgTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
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驅動程式架構圖如下:
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滑鼠,序列槽端輸入如下資訊:
從第一行輸出資訊的“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滑鼠輸出如下圖:
(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 總結
整體架構圖如下:
USB總線驅動程式在我們接入USB裝置的時候會幫我們構造一個新的usb_device注冊到總線裡面來。我們插上任意的USB裝置,就發現核心列印出一些資訊,說明USB總線驅動程式、USB裝置程式已經做好,後面隻需要編寫USB驅動程式,即下圖中的usb_driver 。(是以,以後寫USB驅動程式,寫的就是USB接口描述符方面的)