USB枚举过程及Linux下U盘识别流程
USB_2">USB枚举过程
USB协议定义了设备的6种状态,仅在枚举过程种,设备就经历了4个状态的迁移:上电状态(Powered),默认状态(Default),地址状态(Address)和配置状态(Configured)(其他两种是连接状态和挂起状态(Suspend))。
USB枚举基本的流程为:
-
用户把USB设备插入USB端口或给系统启动时设备上电
USB端口指的是主机下的根hub或主机下行端口上的hub端口。Hub给端口供电,连接着的设备处于上电状态。 -
Hub检测它各个端口数据线上(D+/D-)的电压
在hub端,数据线D+和D-都有一个阻值在14.25k到24.8k的下拉电阻Rpd,而在设备端,D+(全速,高速)和D-(低速)上有一个1.5k的上拉电阻Rpu。当设备插入到hub端口时,有上拉电阻的一根数据线被拉高到幅值的90%的电压(大致是3V)。hub检测到它的一根数据线是高电平,就认为是有设备插入,并能根据是D+还是D-被拉高来判断到底是什么设备(全速/低速)插入端口。
检测到设备后,hub继续给设备供电,但并不急于与设备进行USB传输。 -
Host了解连接的设备
每个hub利用它自己的中断端点向主机报告它的各个端口的状态(对于这个过程,设备是看不到的,也不必关 心),报告的内容只是hub端口的设备连接/断开的事件。如果有连接/断开事件发生,那么host会发送一个 Get_Port_Status请求(request)以了解更多hub上的信息。Get_Port_Status等请求属于所有hub都要 求支持的hub类标准请求(standard hub-class requests)。 -
Hub检测所插入的设备是高速还是低速设备
hub通过检测USB总线空闲(Idle)时差分线的高低电压来判断所连接设备的速度类型,当host发来 Get_Port_Status请求时,hub就可以将此设备的速度类型信息回复给host。(USB 2.0规范要求速度检 测要先于复位(Reset)操作)。 -
hub复位设备
当主机获悉一个新的设备后,主机控制器就向hub发出一个 Set_Port_Feature请求让hub复位其管理的端 口。hub通过驱动数据线到复位状态(D+和D-全为低电平),并持续至少10ms。当然,hub不会把这样的复位 信号发送给其他已有设备连接的端口,所以其他连在该hub上的设备自然看不到复位信号,不受影响。 -
Host检测所连接的全速设备是否支持高速模式
因为根据USB 2.0协议,高速(High Speed)设备在初始时是默认全速(Full Speed )状态运行,所以 对于一个支持USB 2.0的高速hub,当它发现它的端口连接的是一个全速设备时,会进行高速检测,看看目前 这个设备是否还支持高速传输,如果是,那就切到高速信号模式,否则就一直在全速状态下工作。 同样的,从设备的角度来看,如果是一个高速设备,在刚连接bub或上电时只能用全速信号模式运行(根据 USB 2.0协议,高速设备必须向下兼容USB 1.1的全速模式)。随后hub会进行高速检测,之后这个设备才会 切换到高速模式下工作。假如所连接的hub不支持USB 2.0,即不是高速hub,不能进行高速检测,设备将一 直以全速工作。 -
Hub建立设备和主机之间的信息通道
主机不停得向hub发送 Get_Port_Status请求,以查询设备是否复位成功。Hub返回的报告信息中有专门的 一位用来标志设备的复位状态。 当hub撤销了复位信号,设备就处于默认/空闲状态(Default state),准备着主机发来的请求。设备和 主机之间的通信通过控制传输,默认地址0,端点号0进行。在此时,设备能从总线上得到的最大电流是 100mA。 -
主机发送Get_Descriptor请求获取默认管道的最大包长度
默认管道(Default Pipe)在设备一端来看就是端点0。主机此时发送的请求是默认地址0,端点0,虽然所 有位分配地址的设备都是通过地址0来获取主机发来的信息,但由于枚举过程不是多个设备并行处理,而是一 次枚举一个设备的方式进行,所以不会发生多个设备同时响应主机发来的请求。 设备描述符的第8字节代表设备端点0的最大包大小,Get_Descriptor请求中的wLength一项都会设为64, 虽然说设备所返回的设备描述符(Device Descriptor)长度只有18字节(但获取的最大包长至少8字 节),但系统也不在乎,此时,描述符的长度信息对它来说是最重要的,其他的瞄一眼就过了。当完成第一次 的控制传输后,也就是完成控制传输的状态阶段,系统会要求hub对设备进行再一次的复位操作(USB规范里 面可没这要求)。再次复位的目的是使设备进入一个确定的状态。
-
主机给设备分配一个地址
主机控制器通过Set_Address请求向设备分配一个唯一的地址。在完成这次传输之后,设备进入地址状态 (Address state),之后就启用新地址继续与主机通信。这个地址对于设备来说是终身制的,设备在,地 址在;设备消失(被拔出,复位,系统重启),地址被收回。同一个设备当再次被枚举后得到的地址不一定是 上次那个了。
-
主机再次获取设备描述符
主机发送 Get_Descriptor请求到新地址读取设备描述符,这次主机发送Get_Descriptor请求可算是诚 心,它会认真解析设备描述符的内容。设备描述符内信息包括端点0的最大包长度,设备所支持的配置 (Configuration)个数,设备类型,VID(Vendor ID,由USB-IF分配), PID(Product ID,由厂 商自己定制)等信息。Get_Descriptor请求(Device type)和设备描述符(已抹去VID,PID等信息)。
-
主机获取配置描述符
主机发送Get_Descriptor请求,读取配置描述符(Configuration Descriptor),字符串等,逐一了 解设备更详细的信息。事实上,对于配置描述符的标准请求中,有时wLength一项会大于实际配置描述符的长 度(9字节),比如255。这样的效果便是:主机发送了一个Get_Descriptor_Configuration 的请求, 设备会把接口描述符,端点描述符等后续描述符一并回给主机,主机则根据描述符头部的标志判断送上来的具 体是何种描述符。 如果有自己穿描述符,还要获取字符串描述符。另外像HID设备还有报告描述符,他们是单独获取的。
Linux 内核U盘识别流程
打开内核动态输出时候的U盘插拔log
打开动态输出:
echo -n "file drivers/usb/core/hub.c +p" > /sys/kernel/debug/dynamic_debug/control
echo -n "file drivers/usb/storage/usb.c +p" > /sys/kernel/debug/dynamic_debug/control
U盘插入:
[80525.540466] hub 1-5.2:1.0: state 7 ports 4 chg 0000 evt 0002
[80525.540583] usb 1-5.2-port1: status 0101, change 0001, 12 Mb/s
[80525.683717] usb 1-5.2-port1: debounce total 100ms stable 100ms status 0x101 [80525.763651] usb 1-5.2.1: new high-speed USB device number 17 using xhci_hcd [80525.890057] usb 1-5.2.1: udev 17, busnum 1, minor = 16
[80525.890067] usb 1-5.2.1: New USB device found, idVendor=1f75, idProduct=0918, bcdDevice= 2.10
[80525.890073] usb 1-5.2.1: New USB device strings: Mfr=2, Product=3, SerialNumber=4
[80525.890078] usb 1-5.2.1: Product: MiniKing
[80525.890082] usb 1-5.2.1: Manufacturer: aigo
[80525.890087] usb 1-5.2.1: SerialNumber: C0003889
[80525.892239] usb-storage 1-5.2.1:1.0: USB Mass Storage device detected [80525.897026] scsi host2: usb-storage 1-5.2.1:1.0
[80525.897304] usb-storage 1-5.2.1:1.0: waiting for device to settle before scanning
[80526.907541] usb-storage 1-5.2.1:1.0: starting scan
[80526.907735] usb-storage 1-5.2.1:1.0: scan complete
[80526.911022] scsi 2:0:0:0: Direct-Access aigo U330 0009 PQ: 0 ANSI: 4 [80526.911709] sd 2:0:0:0: Attached scsi generic sg1 type 0
[80526.912511] sd 2:0:0:0: [sdb] 61440000 512-byte logical blocks: (31.5 GB/29.3 GiB)
[80526.913106] sd 2:0:0:0: [sdb] Write Protect is off
[80526.913111] sd 2:0:0:0: [sdb] Mode Sense: 23 00 00 00
[80526.913725] sd 2:0:0:0: [sdb] Write cache: disabled, read cache: enabled, doesn't support DPO or FUA
[80526.919820] sdb: sdb4
[80526.922673] sd 2:0:0:0: [sdb] Attached SCSI removable disk
[80528.374262] FAT-fs (sdb4): utf8 is not a recommended IO charset for FAT filesystems, filesystem will be case sensitive!
[80528.376852] FAT-fs (sdb4): Volume was not properly unmounted. Some data may be corrupt. Please run fsck
U盘拔出:
[80578.788424] hub 1-5.2:1.0: state 7 ports 4 chg 0000 evt 0002
[80578.788530] usb 1-5.2-port1: status 0100, change 0001, 12 Mb/s
[80578.788539] usb 1-5.2.1: USB disconnect, device number 17
[80578.788545] usb 1-5.2.1: unregistering device
[80578.795859] blk_update_request: I/O error, dev sdb, sector 257 op 0x1:(WRITE) flags 0x0 phys_seg 1 prio class 0
[80578.806028] Buffer I/O error on dev sdb4, logical block 1, lost async page write [80579.007738] usb 1-5.2-port1: debounce total 100ms stable 100ms status 0x100
内核工作队列检测HUB端口变化
在USB 子系统初始化时(usb.c 中的usb_init()),会去做usb hub的初始化 usb_hub_init(),在其中会分配一个工作队列 usb_hub_wq:
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;}/** The workqueue needs to be freezable to avoid interfering 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.*/hub_wq = alloc_workqueue("usb_hub_wq", WQ_FREEZABLE, 0);if (hub_wq)return 0;/* Fall through if kernel_thread failed */usb_deregister(&hub_driver);pr_err("%s: can't allocate workqueue for usb hub\n", usbcore_name);return -1;
}
INIT_WORK(&hub->events, hub_event);
并在hubi_irq中唤醒hub_event的工作队列kick_hub_wq(hub);:
/* completion function, fires on port status changes and various faults */
static void hub_irq(struct urb *urb)
{struct usb_hub *hub = urb->context;int status = urb->status;unsigned i;unsigned long bits;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 *///连续十次错误导致hub resetdev_dbg(hub->intfdev, "transfer --> %d\n", status);if ((++hub->nerrors < 10) || hub->error) goto resubmit;hub->error = status;fallthrough;/* let hub_wq handle things */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);hub->event_bits[0] = bits;break;}hub->nerrors = 0;/* Something happened, let hub_wq figure it out */kick_hub_wq(hub);resubmit:hub_resubmit_irq_urb(hub);
}
其中 kick_hub_wq 中:
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);
}
hub_event() 函数
所有有关hub端口的变化最终都会在hub_event()中执行,以此来做相应的动作。比如枚举连接上来的USB设备,在 hub_event() 中通过调用 port_event来获取端口变化,后续在port_event中调用hub_hub_status() 来获得 portchange 和 portstatus 的状态并设置 connect_change变量。connect_change 为1表示Hub上的端口状态有变化(设备插入或拔出),进而执行hub_port_connect_change() 的内容。
hub_port_connect_change()中调用hub_port_connect,hub_port_connect中调用
usb_alloc_dev()为新的USB设备申请资源,并进行一些初始化。设置设备的状态为
USB_STATE_POWERED,接下来会枚举USB设备(复位、握手、获取描述符),当USB设备枚举完成后就会触发加载与之对应的USB接口驱动。
hub_event:
static void hub_event(struct work_struct *work)
{....../* deal with port status changes */for (i = 1; i <= hdev->maxchild; i++) {struct usb_port *port_dev = hub->ports[i - 1];if (test_bit(i, hub->event_bits)|| test_bit(i, hub->change_bits)|| test_bit(i, hub->wakeup_bits)) {/** The get_noresume and barrier ensure that if* the port was in the process of resuming, we* flush that work and keep the port active for* the duration of the port_event(). However,* if the port is runtime pm suspended* (powered-off), we leave it in that state, run* an abbreviated port_event(), and move on.*/pm_runtime_get_noresume(&port_dev->dev);pm_runtime_barrier(&port_dev->dev);usb_lock_port(port_dev);port_event(hub, i);usb_unlock_port(port_dev);pm_runtime_put_sync(&port_dev->dev);}}/* deal with hub status changes */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 {if (hubchange & HUB_CHANGE_LOCAL_POWER) {dev_dbg(hub_dev, "power change\n");clear_hub_feature(hdev, C_HUB_LOCAL_POWER);if (hubstatus & HUB_STATUS_LOCAL_POWER)/* FIXME: Is this always true? */hub->limited_power = 1;elsehub->limited_power = 0;}if (hubchange & HUB_CHANGE_OVERCURRENT) {u16 status = 0;u16 unused;dev_dbg(hub_dev, "over-current change\n");clear_hub_feature(hdev, C_HUB_OVER_CURRENT);msleep(500); /* Cool down */hub_power_on(hub, true);hub_hub_status(hub, &status, &unused);if (status & HUB_STATUS_OVERCURRENT)dev_err(hub_dev, "over-current condition\n");}}out_autopm:/* Balance the usb_autopm_get_interface() above */usb_autopm_put_interface_no_suspend(intf);
out_hdev_lock:usb_unlock_device(hdev);/* Balance the stuff in kick_hub_wq() and allow autosuspend */usb_autopm_put_interface(intf);kref_put(&hub->kref, hub_release);kcov_remote_stop();
}
port_event:
static void port_event(struct usb_hub *hub, int port1)__must_hold(&port_dev->status_lock)
{int connect_change;struct usb_port *port_dev = hub->ports[port1 - 1];struct usb_device *udev