一、Linux 下USB Hub热插拔处理
1.1 Linux下USB HUB的驱动的实现和分析:
USB设备是热插拔,因此在hub_probe函数中调用hub_configure函数来配置hub,在这个函数中主要是利用函数usb_alloc_urb函数来分配一个urb,利用usb_fill_int_urb来初始化这个urb结构,包括hub的中断服务程序hub_irq的,查询的周期等。
每当有设备连接到USB接口时,USB总线在查询hub状态信息的时候会触发hub的中断服务程序hub_irq, 在该函数中置位event_bits,运行工作队列。进入hub_event函数,该函数用来处理端口变化的事件。然后通过一个for循环来检测每个端口的状态信息。利用usb_port_status获取端口信息,如果发生变化就调用hub_port_connect_change函数来配置端口等。
在系统初始化的时候在usb_init函数中调用usb_hub_init函数,就进入了hub的初始化。
代码路径:drivers\usb\core\hub.c
在usb_hub_init函数中完成了注册hub驱动,并且利用函数alloc_workqueue创建一个工作队列。
1.2 初始化
这里我们主要分析khub的工作原理: 硬件层次是hub的工作,如何和host及其设备间通信及相应事件
hub的驱动定义为hub_driver,也是一个usb_driver结构体。hub驱动的匹配方式由hub_id_table定义,可以根据PID、VID、接口类型匹配。
[usb/core/hub.c ]
int usb_hub_init(void)
{if (usb_register(&hub_driver) < 0) { //注册usb HUB设备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.*//* 工作队列需要是可冻结的,以避免干扰usb持续的端口切换。否则它可能会看到全速设备在EHCI控制器把端口交给全速控制器之前就消失了 */hub_wq = alloc_workqueue("usb_hub_wq", WQ_FREEZABLE, 0); //创建工作队列if (hub_wq)return 0;
}
[drivers/usb/core/hub.c]
static const struct usb_device_id hub_id_table[] = {{ .match_flags = USB_DEVICE_ID_MATCH_VENDOR| USB_DEVICE_ID_MATCH_PRODUCT| USB_DEVICE_ID_MATCH_INT_CLASS,.idVendor = USB_VENDOR_SMSC,.idProduct = USB_PRODUCT_USB5534B,.bInterfaceClass = USB_CLASS_HUB,.driver_info = HUB_QUIRK_DISABLE_AUTOSUSPEND},{ .match_flags = USB_DEVICE_ID_MATCH_VENDOR| USB_DEVICE_ID_MATCH_PRODUCT,.idVendor = USB_VENDOR_CYPRESS,.idProduct = USB_PRODUCT_CY7C65632,.driver_info = HUB_QUIRK_DISABLE_AUTOSUSPEND},{ .match_flags = USB_DEVICE_ID_MATCH_VENDOR| USB_DEVICE_ID_MATCH_INT_CLASS,.idVendor = USB_VENDOR_GENESYS_LOGIC,.bInterfaceClass = USB_CLASS_HUB,.driver_info = HUB_QUIRK_CHECK_PORT_AUTOSUSPEND},{ .match_flags = USB_DEVICE_ID_MATCH_DEV_CLASS,.bDeviceClass = USB_CLASS_HUB},{ .match_flags = USB_DEVICE_ID_MATCH_INT_CLASS,.bInterfaceClass = USB_CLASS_HUB},{ } /* Terminating entry */
};MODULE_DEVICE_TABLE(usb, hub_id_table);
static struct usb_driver hub_driver = {.name = "hub",.probe = hub_probe,.disconnect = hub_disconnect,.suspend = hub_suspend,.resume = hub_resume,.reset_resume = hub_reset_resume,.pre_reset = hub_pre_reset,.post_reset = hub_post_reset,.unlocked_ioctl = hub_ioctl,.id_table = hub_id_table,.supports_autosuspend = 1,
};
hub驱动匹配和初始化过程如下图所示,主要的工作流程有:
当注册root hub时,会调用device_add注册设备。内核调用usb_hub_init函数初始化hub,此时会调用usb_register_driver注册hub驱动hub_driver。hub设备或驱动注册时都会调用usb_device_match匹配到对方,若匹配成功,则hub_probe函数被调用,进入hub的初始化流程。
hub_probe函数中做了两件事,第一件是初始化轮询hub所用的工作队列和定时器,hub_event比较重要,用于处理USB设备连接、断开、低功耗模式等,后续会介绍,第二件是配置hub,具体的工作如下:
获取hub描述符,hub有专用的描述。
初始化用于事务分割的工作队列。主要是将USB2.0事物转换成USB1.0或USB1.1。
通过请求获取hub的信息和状态。
分配hub中断传输的urb,用以查询hub状态。
填充hub中断传输的urb,回调函数为hub_irq。
给hub的每个port创建usb_port数据结构。
使能hub,主要是给port上电,检查port状态、提交之前设置的中断传输urb,用于查询hub的状态;最后调度处理hub事件的工作队列,当有事件发生时会调用hub_event处理。
然后进入hub的probe函数,主要是一些工作的初始化和hub的配置,我这里化简了
static int hub_probe(struct usb_interface *intf, const struct usb_device_id *id)
{hub = kzalloc(sizeof(*hub), GFP_KERNEL); //分配usb_hub结构体if (!hub)return -ENOMEM;INIT_DELAYED_WORK(&hub->leds, led_work); //用于hub led闪烁的指示灯INIT_DELAYED_WORK(&hub->init_work, NULL);INIT_WORK(&hub->events, hub_event); //用于处理hub的事件if (hub_configure(hub, &desc->endpoint[0].desc) >= 0) //设置hub的端点0return 0;
}
hub_configure配置hub,包括不同的hub的判断和配置,但Linux认为最多只能31个接口,里面比较复杂,这里也化简了
static int hub_configure(struct usb_hub *hub,struct usb_endpoint_descriptor *endpoint)
{ret = hub_hub_status(hub, &hubstatus, &hubchange); //获取hub的状态,主要是通过usb_control_msg进行通信hub->urb = usb_alloc_urb(0, GFP_KERNEL); //分配urb/* UHCI必须要知道HUB的端口的一些连接状态,因此,需要HUB周期性的上报它的端口连接状态.这个URB就是用来做这个用途的.UHCI周期性的发送IN方向中断传输传输给HUB.HUB就会通过这个URB将端口信息发送给UHCI.那这个轮询周期是多长呢?根据我们之前分析的UHCI的知识,它的调度周期是由endpoint的bInterval 字段所决定的.*/usb_fill_int_urb(hub->urb, hdev, pipe, *hub->buffer, maxp, hub_irq,hub, endpoint->bInterval); //填充urb,完成之后调用hub_irq函数for (i = 0; i < maxchild; i++) {ret = usb_hub_create_port_device(hub, i + 1); //创建hub的端点设备,比如/sys/devices/platform/soc@0/38100000.usb/xhci-hcd.0.auto/usb1/1-0:1.0/usb1-port1}}hub_activate(hub, HUB_INIT);return 0;
}
继续分析hub_activate,主要是启动hub,我们这里传入的参数是HUB_INIT
static void hub_activate(struct usb_hub *hub, enum hub_activation_type type)
{/* Continue a partial initialization */if (type == HUB_INIT2 || type == HUB_INIT3) {device_lock(&hdev->dev);/* Was the hub disconnected while we were waiting? */if (hub->disconnected)goto disconnected;if (type == HUB_INIT2)goto init2;goto init3;}if (type == HUB_INIT) {delay = hub_power_on_good_delay(hub); //上电后延时,使hub稳定hub_power_on(hub, false); //对所有的端点上电,usb_control_msg(hdev, usb_sndctrlpipe(hdev, 0), USB_REQ_SET_FEATURE, USB_RT_PORT, feature, port1, NULL, 0, 1000);INIT_DELAYED_WORK(&hub->init_work, hub_init_func2);queue_delayed_work(system_power_efficient_wq,&hub->init_work,msecs_to_jiffies(delay)); //进入初始化的第二阶段hub_init_func2,也就是下面的 init2:init2:/** Check each port and set hub->change_bits to let hub_wq know* which ports need attention.*/for (port1 = 1; port1 <= hdev->maxchild; ++port1) {struct usb_port *port_dev = hub->ports[port1 - 1];struct usb_device *udev = port_dev->child;u16 portstatus, portchange;portstatus = portchange = 0;status = hub_port_status(hub, port1, &portstatus, &portchange); //获取端口的状态,..........//后面进行一系列判断,包括usb2.0,3.0等。并且set_bit(port1, hub->change_bits);/* If no port-status-change flags were set, we don't need any* debouncing. If flags were set we can try to debounce the* ports all at once right now, instead of letting hub_wq do them* one at a time later on.** If any port-status changes do occur during this delay, hub_wq* will see them later and handle them normally.*/if (need_debounce_delay) { //用于消抖delay = HUB_DEBOUNCE_STABLE;/* Don't do a long sleep inside a workqueue routine */if (type == HUB_INIT2) {INIT_DELAYED_WORK(&hub->init_work, hub_init_func3); //进行第三个阶段init3:queue_delayed_work(system_power_efficient_wq,&hub->init_work,msecs_to_jiffies(delay));device_unlock(&hdev->dev);return; /* Continues at init3: below */}init3:status = usb_submit_urb(hub->urb, GFP_NOIO); //提交urb,等执行完成就会回调hub_irqif (status < 0)dev_err(hub->intfdev, "activate --> %d\n", status);if (hub->has_indicators && blinkenlights) //如果有指示灯,点亮queue_delayed_work(system_power_efficient_wq,&hub->leds, LED_CYCLE_PERIOD);/* Scan all ports that need attention */kick_hub_wq(hub); //主要是queue_work(hub_wq, &hub->events),也就是把hub_event加入工作队列,开始运行
}
我们来看看hub_event,前面int2的时候有设置hub->change_bits,这里会进行处理
static void hub_event(struct work_struct *work)
{if (hub->error) {dev_dbg(hub_dev, "resetting for error %d\n", hub->error);ret = usb_reset_device(hdev); //警告接口驱动程序并执行USB端口重置}/* 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)) { //如果这几个条件都满足,就port_event/** 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);}}
}
我们再看看 port_event做了什么。
来看一下,什么情况下, hub_port_connect_change才会被设为1.
1:端口在hub->change_bits中被置位.搜索整个代码树,发生在设置hub->change_bits的地方,只有在hub_port_logical_disconnect()中手动将端口禁用,会将对应位置1.
2:hub上没有这个设备树上没有这个端口上的设备.但显示端口已经连上了设备
3:hub这个端口上的连接发生了改变,从端口有设备连接变为无设备连接,或者从无设备连接变为有设备连接.
4:hub的端口变为了disable,此时这个端口上连接了设备,但被显示该端口已经变禁用,需要将connect_change设为1.
5:端口状态从SUSPEND变成了RESUME,远程唤醒端口上的设备失败,就需要将connect_change设为1.
static void port_event(struct usb_hub *hub, int port1)__must_hold(&port_dev->status_lock)
{if (hub_port_status(hub, port1, &portstatus, &portchange) < 0) //确认端口改变了return;if (connect_change)hub_port_connect_change(hub, port1, portstatus, portchange); //处理端口改变的情况
}
调用hub_port_connect_change再调用hub_port_connect
static void hub_port_connect(struct usb_hub *hub, int port1, u16 portstatus,u16 portchange)
{/* Disconnect any existing devices under this port */if (udev) {if (hcd->usb_phy && !hdev->parent) //断开该端口设备的连接:如果是root hub,挂接在控制器上的,则断开该端口下的设备。usb_phy_notify_disconnect(hcd->usb_phy, udev->speed);usb_disconnect(&port_dev->child);}for (i = 0; i < SET_CONFIG_TRIES; i++) {/* reallocate for each attempt, since references* to the previous one can escape in various ways*/// 分配usb设备内存并初始化bus、type、group、设备在系统中的路径(dev->path)、ep0的属性并设置设备状态为attached。udev = usb_alloc_dev(hdev, hdev->bus, port1);if (!udev) {dev_err(&port_dev->dev,"couldn't allocate usb_device\n");goto done;}usb_set_device_state(udev, USB_STATE_POWERED); //设置为 power状态,并设置电源等。udev->bus_mA = hub->mA_per_port;udev->level = hdev->level + 1;udev->wusb = hub_is_wusb(hub);/* Devices connected to SuperSpeed hubs are USB 3.0 or later */if (hub_is_superspeed(hub->hdev)) //判断速度是否为高速udev->speed = USB_SPEED_SUPER;elseudev->speed = USB_SPEED_UNKNOWN;choose_devnum(udev); //获取设备号,在usbfs中,设备号被用作文件名.if (udev->devnum <= 0) {status = -ENOTCONN; /* Don't retry */goto loop;}/* reset (non-USB 3.0 devices) and get descriptor */usb_lock_port(port_dev);status = hub_port_init(hub, udev, port1, i); //初始化设备,设置地址,读取设备描述符usb_unlock_port(port_dev);/* Run it through the hoops (find a driver, etc) */if (!status) {status = usb_new_device(udev); //执行初始设备设置if (status) {mutex_lock(&usb_port_peer_mutex);spin_lock_irq(&device_state_lock);port_dev->child = NULL;spin_unlock_irq(&device_state_lock);mutex_unlock(&usb_port_peer_mutex);} else {if (hcd->usb_phy && !hdev->parent)usb_phy_notify_connect(hcd->usb_phy,udev->speed);}}}
}
1.3 初始化流程图
二,枚举设备
2.1 Hub部分热拔插
前面主要是初始化的操作,也会去识别开机的时候已经插入的设备,如果是开机之后插入的设备,又是什么流程呢
前面是每当有设备连接到USB接口时,USB总线在查询hub状态信息的时候会触发hub的中断服务程序hub_irq
static void hub_irq(struct urb *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);if ((++hub->nerrors < 10) || hub->error)goto resubmit;hub->error = status;/* FALL THROUGH *//* 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; //置位event_bits /* status change bitmask */break;}hub->nerrors = 0;/* Something happened, let hub_wq figure it out */kick_hub_wq(hub); //}
kick_hub_wq主要是queue_work(hub_wq, &hub->events),也就是把hub_event加入工作队列,开始运行,前面有分析了。
我们重点分析一下usb_new_device,初始化设备
int usb_new_device(struct usb_device *udev)
{err = usb_enumerate_device(udev); /* Read descriptors */ //读取描述符/* Tell the world! */announce_device(udev); //打印出来,需要内核打开CONFIG_USB_ANNOUNCE_NEW_DEVICES宏/* Register the device. The device driver is responsible* for configuring the device and invoking the add-device* notifier chain (used by usbfs and possibly others).*/err = device_add(&udev->dev); //将设备添加到设备层次结构中}
USB主机通过hub感知USB设备的连接、断开、状态变化等事件。当事件发生会产生xHCI中断,在中断处理函数中处理USB设备事件,具体的流程如下:
产生中断后会最终调用到handle_port_status函数处理hub port口事件,没事件发生则直接退出,有事件发生,且status_urb空闲,则走下面的流程。
设置HCD_FLAG_POLL_RH标志,调用usb_hcd_poll_rh_status函数开始轮询hub状态。
调用xHCI驱动的xhci_hub_status_data函数查询root hub每个port的状态,若有变化,则会反汇事件数据的长度。port的状态由port_开头的宏定义定义。
清除轮询hub的标志,将查询hub状态的status_urb从对应的端点队中移除,然后调用usb_hcd_giveback_urb,最终通过调度tasklet处理hub事件。详细的处理过程后续介绍。
当status_urb被占用时,即hcd->status_urb为NULL时,说明上一个hub事件还没处理完(tasklet没处理完),因此会设置HCD_FLAG_POLL_RH标志,调用mod_timer开启定时器,定时器到期后调用rh_timer_func处理hub事件,处理流程和上面一样。
三,总结
在众多事件中,枚举USB设备是最重要的,下图描述了USB主机枚举USB设备的过程。主要的工作有:
通过usb_hcd_poll_rh_status函数轮询到了hub的port上有事件发生,最终通过调用hub_irq函数处理这些事件。
hub_irq函数做了两件事,第一件是重新提交查询hub状态的urb,即设置hcd->status_urb = urb,当下一次处理hub事件时,又会调用到hub_irq;第二件事是调度工作队列处理hub事件。
hub事件通过hub_event函数处理,该函数会遍历每个port,当port上有USB设备连接时,调用hub_port_connect处理。
首先为设备分配usb_device数据结构,接着设置USB设备号,然后初始化设备,包括复位设备并获取设备速度、设置设备地址、获取设备描述符。
枚举USB设备和匹配USB设备驱动在usb_new_device函数中完成,主要的工作如下:
获取USB设备的配置、接口、端点等描述符,若开启了相关选项,则内核会打印USB设备的详细信息。
通过usb_device_match匹配设备驱动,即usb_device_type类型,最终会调用到内核提供的通用的USB设备驱动usb_generic_driver_probe。
在通用的USB设备驱动中,首先会选择配置,某些USB设备有多个配置,因此需要选择其中一个用于工作。
接着设置配置,包括为USB设备分配带宽、设置配置等。
最后遍历USB设备的所有接口,调用usb_device_match为接口匹配驱动(USB驱动和USB接口对应,此时的匹配类型为usb_if_device_type),匹配成功后接口驱动的probe函数被调用。