Linux系統下USB總線工作原理


USB概念釋義及相關機制

1.熱插拔(hot-plugging或Hot Swap):

即帶電插拔。

熱插拔功能就是允許用戶在不關閉系統,不切斷電源的情況下取出和更換損壞的硬盤、電源或板卡等部件,從而提高了系統對災難的及時恢復能力、擴展性和靈活性等。

例如一些面向高端應用的磁盤鏡像系統都可以提供磁盤的熱插拔功能。

具體用學術的說法就是:熱替換(Hot replacement)、熱添加(hot expansion)和熱升級(hot upgrade)。而熱插拔最早出現在服務器領域,是為了提高服務器用性而提出的,在我們平時用的電腦中一般都有USB接口,這種接口就能夠實現熱插拔。如果沒有熱插拔功能,即使磁盤損壞不會造成數據的丟失,用戶仍然需要暫時關閉系統,以便能夠對硬盤進行更換,而使用熱插拔技術只要簡單的打開連接開關或者轉動手柄就可以直接取出硬盤,而系統仍然可以不間斷地正常運行。

 

2.枚舉:

枚舉就是從設備讀取一些信息,知道設備是什么樣的設備,如何進行通信,這樣主機就可以根據這些信息來加載合適的驅動程序。

調試USB設備,很重要的一點就是USB的枚舉過程,只要枚舉成功了,那么就已經成功大半了。

枚舉是相對於控制器集成Root hub而言的,如果控制器不集成Root hub,那么就是一對一匹配,控制器輪詢到設備插入后,就直接匹配相應的唯一的設備驅動,而不需要枚舉過程。

 

3.收發機制:

永遠是主從機制,主發起通信請求。

 

USB工作原理比喻

 

對於USB的工作,主機好比一個公司,你就是USB設備,你進入公司后,公司前台看到你(輪詢),告訴你首先要面試(枚舉),你到了面試現場(第一次插入設備),面試官首先了解到你的外表,性別已經你要應聘的崗位(設備描述符),然后給你一個號,以后就開始按號叫人,當你被叫到就開始問你的專業知識,性格等(配置描述符),如果你比較合適(通過了枚舉)你就會錄取了,並且注冊一個你的信息到公司(驅動安裝,並且寫入注冊表)。等你下次來公司,只要把工號(PID,VID)報上,就知道是你來了。

 

USB硬件識別原理

USB接口只有4條線(OTG多一根ID線,后面會談): 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設備發包握手確定。

 

 

 

 

                                                      USB低速設備硬件接線圖

                                              USB全速(高速)設備硬件接線圖

 

USB設備識別之驅動總體架構

 

USB驅動識別設備主要包括以下幾個部分:1.控制器中斷輪詢   2.Root hub枚舉設備   3.設備加入到設備列中   4.匹配相應的設備驅動  5.初始化設備驅動

 

 

 控制器輪詢Root hub

該過程主要發生在/drivers/usb/core/hcd.c驅動文件中。

主要流程是:

1. __usb_create_hcd函數,創建hcd控制器,在此定義並創建了一個定時器hcd->rh_timer,關聯其定時器函數rh_timer_func;

2.usb_add_hcd函數添加hcd控制器,在函數中注冊register_root_hub,之后再啟動定時器hcd->rh_timer;

3.rh_timer_func函數調用usb_hcd_poll_rh_status函數檢測root hub狀態。

static void rh_timer_func (unsigned long _hcd)

{
usb_hcd_poll_rh_status((struct usb_hcd *) _hcd);
}

4.usb_hcd_poll_rh_status函數調用主機控制器的hub_status_data函數獲取端口狀態。如果端口的狀態有變化,那么length > 0,把獲取到的端口狀態的數組拷貝到urb->transfer_buffer中,就是前面的hub->buffer中,同時調用usb_hcd_giveback_urb函數。

void usb_hcd_poll_rh_status(struct usb_hcd *hcd)
{
      struct urb *urb;
      int length;
      unsigned long flags;
      char buffer[6]; /* Any root hubs with > 31 ports? */

      if (unlikely(!hcd->rh_pollable))
             return;
      if (!hcd->uses_new_polling && !hcd->status_urb)
             return;

      length = hcd->driver->hub_status_data(hcd, buffer);
      if (length > 0) {

             /* try to complete the status urb */
             spin_lock_irqsave(&hcd_root_hub_lock, flags);
           urb = hcd->status_urb;
           if (urb) {
                 clear_bit(HCD_FLAG_POLL_PENDING, &hcd->flags);
                 hcd->status_urb = NULL;
                 urb->actual_length = length;
                 memcpy(urb->transfer_buffer, buffer, length);

                 usb_hcd_unlink_urb_from_ep(hcd, urb);
                 usb_hcd_giveback_urb(hcd, urb, 0);
           } else {
                length = 0;
                set_bit(HCD_FLAG_POLL_PENDING, &hcd->flags);
          }
     spin_unlock_irqrestore(&hcd_root_hub_lock, flags);
     }

     /* The USB 2.0 spec says 256 ms. This is close enough and won't
     * exceed that limit if HZ is 100. The math is more clunky than
     * maybe expected, this is to make sure that all timers for USB devices
     * fire at the same time to give the CPU a break in between */
     if (hcd->uses_new_polling ? HCD_POLL_RH(hcd) :
          (length == 0 && hcd->status_urb != NULL))
          mod_timer (&hcd->rh_timer, (jiffies/(HZ/4) + 1) * (HZ/4));
}

5.usb_hcd_giveback_urb函數中調用urb->complete (urb),而urb->complete = hub_irq,這樣就返回到了hub中 ;

6.hub_irq函數(/drivers/usb/core/hub.c) ,將獲得的端口狀態的數組存入一個long型的整數hub->event_bits[0]中,它對應一個Bitmap,bit 0表示Hub有變化,而其它bit則具體表示某一個端口有沒有變化,如果一個端口沒有變化,對應的那一位就是0。我們通過按位與的方式可以知道哪一個端口發生了改變。

static void hub_irq(struct urb *urb)
{
       //填充urb的時候,urb->context就是賦的hub

       struct usb_hub *hub = urb->context;
       int status = urb->status;
       unsigned i;
       unsigned long bits;

       //判斷urb的狀態,前三種都是出錯

       switch (status) {
            case -ENOENT: /* synchronous unlink */
            case -ECONNRESET: /* async unlink */
            case -ESHUTDOWN: /* hardware going away */
                     return;

            default: /* presumably an error */
                    /* Cause a hub reset after 10 consecutive errors */
                   dev_dbg(hub->intfdev, "transfer --> %d\n", status);
                   //剛開始hub->error為0,hub->nerrors也為0

                   if ((++hub->nerrors < 10) || hub->error)
                           goto resubmit;
                   hub->error = status;
                   /* FALL THROUGH */

           /* let hub_wq handle things */

             //urb被順利的處理
          case 0: /* we got data: port status changed */
          bits = 0;
          for (i = 0; i < urb->actual_length; ++i)
               bits |= ((unsigned long) ((*hub->buffer)[i]))
                    << (i*8);

           //event_bits[0],是一個數組,對應Bitmap,bit 0表示Hub有變化,而其它bit則具體表示某一個端口有沒有變化,如果一個端口沒有變化,對應的那一位就是0
         hub->event_bits[0] = bits;
         break;
         }

        hub->nerrors = 0;

        /* Something happened, let hub_wq figure it out */
        //調用kick_khubd()函數,於是會再一次觸發hub_events()

        kick_hub_wq(hub);  or   kick_khubd(hub);

        resubmit:
            //如果hub被掛起了,或者要被reset了,那么就不用重新提交urb了,hub_irq()函數直接返回

            if (hub->quiescing)
                   return;

            status = usb_submit_urb(hub->urb, GFP_ATOMIC);
            if (status != 0 && status != -ENODEV && status != -EPERM)
                  dev_err(hub->intfdev, "resubmit --> %d\n", status);
}

7-1. 老版kick_khubd將event_list添加到hub_event_list鏈表中,表示有事件產生,同時喚醒hub_thread線程,喚醒hub_thread調用的函數是wake_up(&khubd_wait),hub_thread線程在hub初始化時就創建好了。hub_thread線程在沒有事件時,一直在睡眠,除非event list不為空或者線程停掉了,hub_thread線程被喚醒后,進入了hub_events函數(枚舉過程)。

int usb_hub_init(void)

{

       if (usb_register(&hub_driver) < 0)

       {

              printk(KERN_ERR "%s: can't register hub driver\n", usbcore_name);

              return -1;

       }

       khubd_task = kthread_run(hub_thread, NULL, "khubd");

       if (!IS_ERR(khubd_task))

            return 0;

       /* Fall through if kernel_thread failed */

      usb_deregister(&hub_driver);

      printk(KERN_ERR "%s: can't start khubd\n", usbcore_name);

      return -1;

static int hub_thread(void *__unused)

{

     /* khubd needs to be freezable to avoid intefering with USB-PERSIST

     * port handover. Otherwise it might see that a full-speed device

     * was gone before the EHCI controller had handed its port over to

     * the companion full-speed controller.

     */

       set_freezable(); 

       do {

             hub_events();

             //判斷event list不為空或者線程停掉了

             wait_event_freezable(khubd_wait, !list_empty(&hub_event_list) || kthread_should_stop()); 

       } while (!kthread_should_stop() || !list_empty(&hub_event_list)); 

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

       return 0;

}

7-2.新版kick_hub_wq,直接調用hub_events()枚舉函數。

static void kick_hub_wq(struct usb_hub *hub)
{
       struct usb_interface *intf;

       if (hub->disconnected || work_pending(&hub->events))
            return;

      * Suppress autosuspend until the event is proceed.
      * Be careful and make sure that the symmetric operation is
      * always called. We are here only when there is no pending
      * work for this hub. Therefore put the interface either when
      * the new work is called or when it is canceled.

      intf = to_usb_interface(hub->intfdev);
      usb_autopm_get_interface_no_resume(intf);
      kref_get(&hub->kref);

      if (queue_work(hub_wq, &hub->events))
          return;

      /* the work has already been scheduled */
     usb_autopm_put_interface_async(intf);
     kref_put(&hub->kref, hub_release);
}

參考文章:https://blog.csdn.net/weixin_38696651/article/details/89632477

 

根集線器(Root Hub)枚舉

 

 

該過程主要發生在/drivers/usb/core/hub.c驅動文件中。

主要流程為:

1 主機知道了新設備連接后

2 集線器重新設置這個新設備

3 集線器在設備和主機之間建立一個信號通路

4 集線器檢測設備速度

5 獲取最大數據包長度

6 主機分配一個新的地址給設備

7 主機向新地址重新發送Get_Device_Descriptor命令,此次讀取其設備描述符的全部字段,以了解該設備的總體信息,如VID,PID

8 主機向設備循環發送Get_Device_Configuration命令,要求USB設備回答,以讀取全部配置信息

9 主機發送Get_Device_String命令,獲得字符集描述(unicode),比如產商、產品描述、型號等等

10 此時主機將會彈出窗口,展示發現新設備的信息,產商、產品描述、型號等

11 根據Device_Descriptor和Device_Configuration應答,PC判斷是否能夠提供USB的Driver

12 加載了USB設備驅動以后,主機發送Set_Configuration(x)命令請求為該設備選擇一個合適的配置(x代表非0的配置值)

 

主要代碼:

static void hub_events(void)

{

    struct list_head *tmp;

    struct usb_device *hdev;

    struct usb_interface *intf;

    struct usb_hub *hub;

    struct device *hub_dev;

    u16 hubstatus;

    u16 hubchange;

    u16 portstatus;

    u16 portchange;

    int i, ret;

    int connect_change;

 

    /*

     *  We restart the list every time to avoid a deadlock with

     * deleting hubs downstream from this one. This should be

     * safe since we delete the hub from the event list.

     * Not the most efficient, but avoids deadlocks.

     */

    while (1) {

 

       /* Grab the first entry at the beginning of the list */

       spin_lock_irq(&hub_event_lock);

       if (list_empty(&hub_event_list)) {

           spin_unlock_irq(&hub_event_lock);

           break;

       }

       //取出event_list取出消息放入tmp, 並把tmp從隊列里刪除掉

       tmp = hub_event_list.next;

       list_del_init(tmp);

       //根據tmp得到usb_hub這個結構體

       hub = list_entry(tmp, struct usb_hub, event_list);

       kref_get(&hub->kref);

       spin_unlock_irq(&hub_event_lock);

 

       hdev = hub->hdev;

       hub_dev = hub->intfdev;

       intf = to_usb_interface(hub_dev);

       dev_dbg(hub_dev, "state %d ports %d chg %04x evt %04x\n",

              hdev->state, hub->descriptor

                  ? hub->descriptor->bNbrPorts

                  : 0,

              /* NOTE: expects max 15 ports... */

              (u16) hub->change_bits[0],

              (u16) hub->event_bits[0]);

 

       /* Lock the device, then check to see if we were

        * disconnected while waiting for the lock to succeed. */

       usb_lock_device(hdev);

       if (unlikely(hub->disconnected))

           goto loop;

 

       /* If the hub has died, clean up after it */

       //一下幾種情況會將hub設置為USB_STATE_NOTATTACHED

       //匯報Host Controller異常死機的函數,usb_hc_died()

       //hub驅動自己提供的函數,hub_port_disable(),用於關掉一個端口的函數

       //斷開設備的函數usb_disconnect()

       if (hdev->state == USB_STATE_NOTATTACHED) {

           hub->error = -ENODEV;

           hub_quiesce(hub, HUB_DISCONNECT);

           goto loop;

       }

 

       /* Autoresume */

       //讓這個usb interface的電源引用計數加一,只要這個引用計數大於0,這個設備就不允許autosuspend

       //autosuspend就是當用戶在指定的時間內沒有什么活動的話,就自動掛起

       ret = usb_autopm_get_interface(intf);

       if (ret) {

           dev_dbg(hub_dev, "Can't autoresume: %d\n", ret);

           goto loop;

       }

 

       /* If this is an inactive hub, do nothing */

       //quiescing是停止的意思,在reset的時候我們會設置它為1,在suspend的時候我們也會把它設置為1,

       //一旦把它設置成了1,那么hub驅動程序就不會再提交任何URB

       if (hub->quiescing)

           goto loop_autopm;

 

       if (hub->error) {

           dev_dbg (hub_dev, "resetting for error %d\n",

              hub->error);

           //把設備reset

           ret = usb_reset_device(hdev);

           if (ret) {

              dev_dbg (hub_dev,

                  "error resetting hub: %d\n", ret);

              goto loop_autopm;

           }

           //記錄發生錯誤的次數

           hub->nerrors = 0;

           hub->error = 0;

       }

 

       /* deal with port status changes */

       //表示這個hub有幾個端口

       for (i = 1; i <= hub->descriptor->bNbrPorts; i++) {

           //這個端口正在執行reset或者resume操作

           if (test_bit(i, hub->busy_bits))

              continue;

           //這個端口對應的change_bits沒有設置

           connect_change = test_bit(i, hub->change_bits);

           //這個端口對應的event_bits沒有設置

           if (!test_and_clear_bit(i, hub->event_bits) &&

                  !connect_change)

              continue;

           //獲取端口狀態,保存在portstatus和portchange

           ret = hub_port_status(hub, i,

                  &portstatus, &portchange);

           if (ret < 0)

              continue;

           //這個端口的Current Connect Status位是否有變化

           //如果有變化,發送另一個請求以清除這個flag,並且將connect_change也設置為1

           if (portchange & USB_PORT_STAT_C_CONNECTION) {

              clear_port_feature(hdev, i,

                  USB_PORT_FEAT_C_CONNECTION);

              connect_change = 1;

           }

           //每個端口都有一個開關,這叫做enable或者disable一個端口

           if (portchange & USB_PORT_STAT_C_ENABLE) {

              if (!connect_change)

                  dev_dbg (hub_dev,

                     "port %d enable change, "

                     "status %08x\n",

                     i, portstatus);

              //清除USB_PORT_FEAT_C_ENABLE這個flag

              clear_port_feature(hdev, i,

                  USB_PORT_FEAT_C_ENABLE);

 

              /*

               * EM interference sometimes causes badly

               * shielded USB devices to be shutdown by

               * the hub, this hack enables them again.

               * Works at least with mouse driver.

               */

              //端口被disable了,但是連接沒有變化,並且hdev->children[i]還有值

              //有子設備連在端口上,可是端口卻被disable了,基本上這種情況就是電磁干擾造成的,設置connect_change為1

              if (!(portstatus & USB_PORT_STAT_ENABLE)

                  && !connect_change

                  && hdev->children[i-1]) {

                  dev_err (hub_dev,

                      "port %i "

                      "disabled by hub (EMI?), "

                      "re-enabling...\n",

                     i);

                  connect_change = 1;

              }

           }

           //連在該端口的設備的suspend狀態有變化,從suspended狀態出來,也就是說resume完成

           if (portchange & USB_PORT_STAT_C_SUSPEND) {

              struct usb_device *udev;

              //清除掉SUSPEND這個flag

              clear_port_feature(hdev, i,

                  USB_PORT_FEAT_C_SUSPEND);

              udev = hdev->children[i-1];

              //該端口連了子設備的情況就把子設備喚醒,如果端口沒有連子設備,那么就把端口disable掉

              if (udev) {

                  usb_lock_device(udev);

                  ret = remote_wakeup(hdev->

                         children[i-1]);

                  usb_unlock_device(udev);

                  if (ret < 0)

                     connect_change = 1;

              } else {

                  ret = -ENODEV;

                  hub_port_disable(hub, i, 1);

              }

              dev_dbg (hub_dev,

                  "resume on port %d, status %d\n",

                  i, ret);

           }

           //這個端口可能曾經存在電流過大的情況

           if (portchange & USB_PORT_STAT_C_OVERCURRENT) {

               dev_err (hub_dev,

                  "over-current change on port %d\n",

                  i);

              //清除掉OVER_CURRENT這個flag

              clear_port_feature(hdev, i,

                  USB_PORT_FEAT_C_OVER_CURRENT);

              //如果其它的端口電流過大,那么將會導致本端口斷電,

              //即hub上一個端口出現over-current條件將有可能引起hub上其它端口陷入powered off的狀態.

              //對於over-current的情況我們都把hub重新上電

              hub_power_on(hub, true);

           }

           //一個端口從Resetting狀態進入到Enabled狀態

           if (portchange & USB_PORT_STAT_C_RESET) {

              dev_dbg (hub_dev,

                  "reset change on port %d\n",

                  i);

              clear_port_feature(hdev, i,

                  USB_PORT_FEAT_C_RESET);

           }

           //如果鏈接狀態發生了變化,執行hub_port_connect_change函數

           //有三種情況會調用這個函數,一個是連接有變化,

           //一個是端口本身重新使能,即所謂的enable,這種情況通常就是為了對付電磁干擾的

           //第三種情況就是在復位一個設備的時候發現其描述符變了,這通常對應的是硬件本身有了升級

           if (connect_change)

              hub_port_connect_change(hub, i,

                     portstatus, portchange);

       } /* end for i */

 

       /* deal with hub status changes */

       //bit 0表示Hub有變化

       if (test_and_clear_bit(0, hub->event_bits) == 0)

           ;   /* do nothing */

       else if (hub_hub_status(hub, &hubstatus, &hubchange) < 0)

           dev_err (hub_dev, "get_hub_status failed\n");

       else {

           //電源有變化,一個hub可以用兩種供電方式,一種是自帶電源.另一種是沒有自帶電源,由總線來供電

           if (hubchange & HUB_CHANGE_LOCAL_POWER) {

              dev_dbg (hub_dev, "power change\n");

              //先清除掉標志位

              clear_hub_feature(hdev, C_HUB_LOCAL_POWER);

              //HUB_STATUS_LOCAL_POWER用來標志這個hub是有專門的外接電源的還是從usb總線上獲取電源

              //原來是有電源的,而現在沒了,把limited_power設置為1

              if (hubstatus & HUB_STATUS_LOCAL_POWER)

                  /* FIXME: Is this always true? */

                  hub->limited_power = 1;

              //如果是原來沒有電源現在有了電源,那么可以取消limited_power了,把它設置為0

              else

                  hub->limited_power = 0;

           }

           //有過流的改變

           if (hubchange & HUB_CHANGE_OVERCURRENT) {

              dev_dbg (hub_dev, "overcurrent change\n");

              msleep(500);  /* Cool down */

              clear_hub_feature(hdev, C_HUB_OVER_CURRENT);

              //重新給它上電

                            hub_power_on(hub, true);

           }

       }

 

loop_autopm:

       /* Allow autosuspend if we're not going to run again */

       if (list_empty(&hub->event_list))

           //調用了這個函數這個hub就可以被掛起

           usb_autopm_enable(intf);

loop:

       usb_unlock_device(hdev);

       //減少hub的引用計數

       kref_put(&hub->kref, hub_release);

 

        } /* end while (1) */

}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM