打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Linux系统下USB总线工作原理
userphoto

2022.08.19 内蒙古

关注

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) */

}

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
linux设备驱动之USB数据传输分析
USB设备主机侧驱动
linux usb hub初始化-steven
Usb设备驱动3:root hub守护进程2
Linux设备驱动子系统终极弹 - USB
Linux下USB suspend/resume源码分析
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服