USB: When hot reset for USB3 fails, try warm reset.
authorSarah Sharp <sarah.a.sharp@linux.intel.com>
Wed, 14 Sep 2011 21:24:52 +0000 (14:24 -0700)
committerGreg Kroah-Hartman <gregkh@suse.de>
Tue, 20 Sep 2011 19:33:50 +0000 (12:33 -0700)
When a hot reset (standard USB port reset) fails on a USB 3.0 port, the
host controller transitions to the "Error" state.  It reports the port
link state as "Inactive", sets the link state change flag, and (if the
device disconnects) also reports the disconnect and connect change status.
It's also supposed to transition the link state to "RxDetect", but the NEC
µPD720200 xHCI host does not.

Unfortunately, Harald found that the combination of the NEC µPD720200 and
a LogiLink USB 3.0 to SATA adapter triggered this issue.  The USB core
would reset the device, the port would go into this error state, and the
device would never be enumerated.  This combination works under Windows,
but not under Linux.

When a hot reset fails on a USB 3.0 port, and the link state is reported
as Inactive, fall back to a warm port reset instead.  Harald confirms that
with a warm port reset (along with all the change bits being correctly
cleared), the USB 3.0 device will successfully enumerate.

Harald also had to add two other patches ("xhci: Set change bit when warm
reset change is set." and "usbcore: refine warm reset logic") to make this
setup work.  Since the warm reset refinement patch is not destined for the
stable kernels (it's too big), this patch should not be backported either.

This fixes https://bugzilla.kernel.org/show_bug.cgi?id=41752

Signed-off-by: Sarah Sharp <sarah.a.sharp@linux.intel.com>
Tested-by: Harald Brennich <harald.brennich@gmx.de>
Signed-off-by: Greg Kroah-Hartman <gregkh@suse.de>
drivers/usb/core/hub.c

index aa336f736f0c97e9574b4cae75d0603c4173f44b..1c155123c32f140ea4972df00f02752f9832c248 100644 (file)
@@ -2029,6 +2029,17 @@ static unsigned hub_is_wusb(struct usb_hub *hub)
 #define HUB_LONG_RESET_TIME    200
 #define HUB_RESET_TIMEOUT      500
 
+static int hub_port_reset(struct usb_hub *hub, int port1,
+                       struct usb_device *udev, unsigned int delay, bool warm);
+
+/* Is a USB 3.0 port in the Inactive state? */
+static bool hub_port_inactive(struct usb_hub *hub, u16 portstatus)
+{
+       return hub_is_superspeed(hub->hdev) &&
+               (portstatus & USB_PORT_STAT_LINK_STATE) ==
+               USB_SS_PORT_LS_SS_INACTIVE;
+}
+
 static int hub_port_wait_reset(struct usb_hub *hub, int port1,
                        struct usb_device *udev, unsigned int delay, bool warm)
 {
@@ -2052,6 +2063,38 @@ static int hub_port_wait_reset(struct usb_hub *hub, int port1,
                 * when the port appears not to be connected.
                 */
                if (!warm) {
+                       /*
+                        * Some buggy devices can cause an NEC host controller
+                        * to transition to the "Error" state after a hot port
+                        * reset.  This will show up as the port state in
+                        * "Inactive", and the port may also report a
+                        * disconnect.  Forcing a warm port reset seems to make
+                        * the device work.
+                        *
+                        * See https://bugzilla.kernel.org/show_bug.cgi?id=41752
+                        */
+                       if (hub_port_inactive(hub, portstatus)) {
+                               int ret;
+
+                               if ((portchange & USB_PORT_STAT_C_CONNECTION))
+                                       clear_port_feature(hub->hdev, port1,
+                                                       USB_PORT_FEAT_C_CONNECTION);
+                               if (portchange & USB_PORT_STAT_C_LINK_STATE)
+                                       clear_port_feature(hub->hdev, port1,
+                                                       USB_PORT_FEAT_C_PORT_LINK_STATE);
+                               if (portchange & USB_PORT_STAT_C_RESET)
+                                       clear_port_feature(hub->hdev, port1,
+                                                       USB_PORT_FEAT_C_RESET);
+                               dev_dbg(hub->intfdev, "hot reset failed, warm reset port %d\n",
+                                               port1);
+                               ret = hub_port_reset(hub, port1,
+                                               udev, HUB_BH_RESET_TIME,
+                                               true);
+                               if ((portchange & USB_PORT_STAT_C_CONNECTION))
+                                       clear_port_feature(hub->hdev, port1,
+                                                       USB_PORT_FEAT_C_CONNECTION);
+                               return ret;
+                       }
                        /* Device went away? */
                        if (!(portstatus & USB_PORT_STAT_CONNECTION))
                                return -ENOTCONN;