[RFC v2 7/9] USB/xHCI: Support device-initiated USB 3.0 resume.

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



USB 3.0 hubs don't have a port suspend change bit (that bit is now
reserved).  Instead, when a host-initiated resume finishes, the hub sets
the port link state change bit.

When a USB 3.0 device initiates remote wakeup, the parent hubs with
their upstream links in U3 will pass the LFPS up the chain.  The first
hub that has an upstream link in U0 (which may be the roothub) will
reflect that LFPS back down the path to the device.

However, the parent hubs in the resumed path will not set their link
state change bit.  Instead, the device that initiated the resume has to
send an asynchronous "Function Wake" Device Notification up to the host
controller.

Unfortunately, this notification can take up to 2.5 seconds
(tNotification in section 8.13 of the USB3 spec).  That means that we
need to prevent the parent hub from being suspended before the
notification has been sent.

First, make the xHCI roothub act like an external USB 3.0 hub and not
pass up the port link state change bit when a device-initiated resume
finishes.  Introduce a new xHCI bit field, port_remote_wakeup, so that
we can tell the difference between a port coming out of the U3Exit state
(host-initiated resume) and the RExit state (ending state of
device-initiated resume).

Since the USB core can't tell whether a port on a hub has resumed by
looking at the Hub Status buffer, we need to introduce a bitfield,
wakeup_bits, that indicates which ports have resumed.  When the xHCI
driver notices a port finishing a device-initiated resume, we call into
a new USB core function, usb_super_speed_remote_wakeup(), that will set
the right bit in wakeup_bits, and kick khubd for that hub.

We also call usb_super_speed_remote_wakeup() when the Function Wake
Device Notification is received by the xHCI driver.  This covers the
case where the link between the roothub and the first-tier hub is in U0,
and the hub reflects the resume signaling back to the device without
giving any indication it has done so until the device sends the Function
Wake notification.

Change the code in khubd that handles the remote wakeup to look at the
state the USB core thinks the device is in, and handle the remote wakeup
if the port's wakeup bit is set.

This patch only takes care of the case where the device is attached
directly to the roothub, or the USB 3.0 hub that is attached to the root
hub is the device sending the Function Wake Device Notification (e.g.
because a new USB device was attached).  The other cases will be covered
in a second patch.

Signed-off-by: Sarah Sharp <sarah.a.sharp@xxxxxxxxxxxxxxx>
---
 drivers/usb/core/hub.c       |   60 ++++++++++++++++++++++++++++++++----------
 drivers/usb/host/xhci-ring.c |   40 ++++++++++++++++++++++-----
 drivers/usb/host/xhci.h      |    1 +
 include/linux/usb/hcd.h      |    2 +
 4 files changed, 81 insertions(+), 22 deletions(-)

diff --git a/drivers/usb/core/hub.c b/drivers/usb/core/hub.c
index ed699c6..657d6ad 100644
--- a/drivers/usb/core/hub.c
+++ b/drivers/usb/core/hub.c
@@ -62,6 +62,8 @@ struct usb_hub {
 							resumed */
 	unsigned long		removed_bits[1]; /* ports with a "removed"
 							device present */
+	unsigned long		wakeup_bits[1];	/* ports that have signaled
+							remote wakeup */
 #if USB_MAXCHILDREN > 31 /* 8*sizeof(unsigned long) - 1 */
 #error event_bits[] is too short!
 #endif
@@ -411,6 +413,34 @@ void usb_kick_khubd(struct usb_device *hdev)
 		kick_khubd(hub);
 }
 
+/*
+ * USB 3.0 devices are supposed to send a Device Notification Function Wake
+ * message after they are finished signaling resume.  The USB 3.0 bus
+ * specification does not specify the maximum amount of time between the end of
+ * resume signaling and the first wake notification.  It does specify
+ * TNotification, the rate at which the device should send a Function Wake
+ * notification if the device has not been accessed.  That rate is 2.5 seconds,
+ * which is far too long when the hub will be re-suspended by default after
+ * 2 seconds.
+ *
+ * To prevent suspend of hubs while we're waiting for the device notification
+ * to be sent, mark the resumed port as woken up and kick khubd for the hub.
+ */
+void usb_super_speed_remote_wakeup(struct usb_device *hdev,
+		unsigned int portnum)
+{
+	struct usb_hub *hub;
+
+	if (!hdev)
+		return;
+
+	hub = hdev_to_hub(hdev);
+	if (hub) {
+		set_bit(portnum, hub->wakeup_bits);
+		kick_khubd(hub);
+	}
+}
+EXPORT_SYMBOL_GPL(usb_super_speed_remote_wakeup);
 
 /* completion function, fires on port status changes and various faults */
 static void hub_irq(struct urb *urb)
@@ -807,12 +837,6 @@ static void hub_activate(struct usb_hub *hub, enum hub_activation_type type)
 			clear_port_feature(hub->hdev, port1,
 					USB_PORT_FEAT_C_ENABLE);
 		}
-		if (portchange & USB_PORT_STAT_C_LINK_STATE) {
-			need_debounce_delay = true;
-			clear_port_feature(hub->hdev, port1,
-					USB_PORT_FEAT_C_PORT_LINK_STATE);
-		}
-
 		if ((portchange & USB_PORT_STAT_C_BH_RESET) &&
 				hub_is_superspeed(hub->hdev)) {
 			need_debounce_delay = true;
@@ -3458,11 +3482,18 @@ static int hub_handle_remote_wakeup(struct usb_hub *hub, unsigned int port,
 	int ret;
 
 	hdev = hub->hdev;
-	if (!(portchange & USB_PORT_STAT_C_SUSPEND))
-		return 0;
-	clear_port_feature(hdev, port, USB_PORT_FEAT_C_SUSPEND);
-
 	udev = hdev->children[port-1];
+	if (!hub_is_superspeed(hdev)) {
+		if (!(portchange & USB_PORT_STAT_C_SUSPEND))
+			return 0;
+		clear_port_feature(hdev, port, USB_PORT_FEAT_C_SUSPEND);
+	} else {
+		if (!udev || udev->state != USB_STATE_SUSPENDED ||
+				!test_and_clear_bit(udev->portnum,
+					hub->wakeup_bits))
+			return 0;
+	}
+
 	if (udev) {
 		/* TRSMRCY = 10 msec */
 		msleep(10);
@@ -3493,7 +3524,7 @@ static void hub_events(void)
 	u16 portstatus;
 	u16 portchange;
 	int i, ret;
-	int connect_change;
+	int connect_change, wakeup_change;
 
 	/*
 	 *  We restart the list every time to avoid a deadlock with
@@ -3572,8 +3603,9 @@ static void hub_events(void)
 			if (test_bit(i, hub->busy_bits))
 				continue;
 			connect_change = test_bit(i, hub->change_bits);
+			wakeup_change = test_bit(i, hub->wakeup_bits);
 			if (!test_and_clear_bit(i, hub->event_bits) &&
-					!connect_change)
+					!connect_change && !wakeup_change)
 				continue;
 
 			ret = hub_port_status(hub, i,
@@ -3648,8 +3680,8 @@ static void hub_events(void)
 				clear_port_feature(hdev, i,
 					USB_PORT_FEAT_C_BH_PORT_RESET);
 			}
-			if (portchange & USB_PORT_STAT_C_LINK_STATE) {
-				clear_port_feature(hub->hdev, i,
+			if (portchange & USB_PORT_FEAT_C_PORT_LINK_STATE) {
+				clear_port_feature(hdev, i,
 						USB_PORT_FEAT_C_PORT_LINK_STATE);
 			}
 			if (portchange & USB_PORT_STAT_C_CONFIG_ERROR) {
diff --git a/drivers/usb/host/xhci-ring.c b/drivers/usb/host/xhci-ring.c
index 8790308..fbf030d 100644
--- a/drivers/usb/host/xhci-ring.c
+++ b/drivers/usb/host/xhci-ring.c
@@ -1241,18 +1241,20 @@ static void handle_device_notification(struct xhci_hcd *xhci,
 		union xhci_trb *event)
 {
 	u32 slot_id;
+	struct usb_device *udev;
 
 	slot_id = TRB_TO_SLOT_ID(event->generic.field[3]);
-	if (!xhci->devs[slot_id])
+	if (!xhci->devs[slot_id]) {
 		xhci_warn(xhci, "Device Notification event for "
 				"unused slot %u\n", slot_id);
-	else
-		xhci_dbg(xhci, "Device Notification event for slot ID %u\n",
-				slot_id);
-	/* XXX should we kick khubd for the parent hub?  It should have send an
-	 * interrupt transfer when the port started signaling resume, so there's
-	 * probably no need to do so.
-	 */
+		return;
+	}
+
+	xhci_dbg(xhci, "Device Wake Notification event for slot ID %u\n",
+			slot_id);
+	udev = xhci->devs[slot_id]->udev;
+	if (udev && udev->parent)
+		usb_super_speed_remote_wakeup(udev->parent, udev->portnum);
 }
 
 static void handle_port_status(struct xhci_hcd *xhci,
@@ -1340,6 +1342,11 @@ static void handle_port_status(struct xhci_hcd *xhci,
 
 		if (DEV_SUPERSPEED(temp)) {
 			xhci_dbg(xhci, "remote wake SS port %d\n", port_id);
+			/* Set a flag to say the port signaled remote wakeup,
+			 * so we can tell the difference between the end of
+			 * device and host initiated resume.
+			 */
+			bus_state->port_remote_wakeup |= 1 << faked_port_index;
 			xhci_test_and_clear_bit(xhci, port_array,
 					faked_port_index, PORT_PLC);
 			xhci_set_link_state(xhci, port_array, faked_port_index,
@@ -1362,10 +1369,27 @@ static void handle_port_status(struct xhci_hcd *xhci,
 	if ((temp & PORT_PLC) && (temp & PORT_PLS_MASK) == XDEV_U0 &&
 			DEV_SUPERSPEED(temp)) {
 		xhci_dbg(xhci, "resume SS port %d finished\n", port_id);
+		/* We've just brought the device into U0 through either the
+		 * Resume state after a device remote wakeup, or through the
+		 * U3Exit state after a host-initiated resume.  If it's a device
+		 * initiated remote wake, don't pass up the link state change,
+		 * so the roothub behavior is consistent with external
+		 * USB 3.0 hub behavior.
+		 */
 		slot_id = xhci_find_slot_id_by_port(hcd, xhci,
 				faked_port_index + 1);
 		if (slot_id && xhci->devs[slot_id])
 			xhci_ring_device(xhci, slot_id);
+		if (bus_state->port_remote_wakeup && (1 << faked_port_index)) {
+			bus_state->port_remote_wakeup &=
+				~(1 << faked_port_index);
+			xhci_test_and_clear_bit(xhci, port_array,
+					faked_port_index, PORT_PLC);
+			usb_super_speed_remote_wakeup(hcd->self.root_hub,
+					faked_port_index + 1);
+			bogus_port_status = true;
+			goto cleanup;
+		}
 	}
 
 	if (hcd->speed != HCD_USB3)
diff --git a/drivers/usb/host/xhci.h b/drivers/usb/host/xhci.h
index fb99c83..0f49369 100644
--- a/drivers/usb/host/xhci.h
+++ b/drivers/usb/host/xhci.h
@@ -1344,6 +1344,7 @@ struct xhci_bus_state {
 	/* ports suspend status arrays - max 31 ports for USB2, 15 for USB3 */
 	u32			port_c_suspend;
 	u32			suspended_ports;
+	u32			port_remote_wakeup;
 	unsigned long		resume_done[USB_MAXCHILDREN];
 };
 
diff --git a/include/linux/usb/hcd.h b/include/linux/usb/hcd.h
index b2f62f3..b46b6aa 100644
--- a/include/linux/usb/hcd.h
+++ b/include/linux/usb/hcd.h
@@ -412,6 +412,8 @@ extern irqreturn_t usb_hcd_irq(int irq, void *__hcd);
 
 extern void usb_hc_died(struct usb_hcd *hcd);
 extern void usb_hcd_poll_rh_status(struct usb_hcd *hcd);
+extern void usb_super_speed_remote_wakeup(struct usb_device *roothub,
+		unsigned int portnum);
 
 /* The D0/D1 toggle bits ... USE WITH CAUTION (they're almost hcd-internal) */
 #define usb_gettoggle(dev, ep, out) (((dev)->toggle[out] >> (ep)) & 1)
-- 
1.7.5.4

--
To unsubscribe from this list: send the line "unsubscribe linux-usb" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html


[Index of Archives]     [Linux Media]     [Linux Input]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [Old Linux USB Devel Archive]

  Powered by Linux