[RFC PATCH 11/15] usbcore: power down peer port on endpoint connect

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

 



Once a port connects to a non-hub device disable its peer until
disconnect.  This enables safe power cycling of the port that is to remain
connected.  Otherwise, if a connected port is powered off while the peer
remains powered the device may attempt to change speeds and connect to the
secondary port when it loses power on its current connection.

This implementation toggles the power policy for the peer port.  When force_off
is set the port will be scheduled for poweroff provided the current
pm_qos_no_power_off setting allows.  On disconnect the port returns to being
powered on by default if it is a hotplug port.

Updated summary of the port power management rules:

Events:               | Power: State:  Count:
pm_qos_no_power_off=1 | on     suspend 0
enable hotplug        | on     suspend 0
device connect        | on     active  child
pm_qos_no_power_off=0 | on     active  child
device sleep          | on     suspend 0
device wake           | on     active  child
device disconnect     | on     suspend 0
peer connect          | off    suspend 0
peer disconnect       | on     suspend 0
disable hotplug       | off    suspend 0

Signed-off-by: Dan Williams <dan.j.williams@xxxxxxxxx>
---
 drivers/usb/core/hub.c          |   18 ++++-
 drivers/usb/core/hub.h          |    4 +
 drivers/usb/core/port.c         |    3 +
 drivers/usb/core/usb-platform.c |  140 ++++++++++++++++++++++++++++++++++++++-
 drivers/usb/core/usb-platform.h |    9 +++
 5 files changed, 167 insertions(+), 7 deletions(-)

diff --git a/drivers/usb/core/hub.c b/drivers/usb/core/hub.c
index d1a08d00e7bb..9ebb58168199 100644
--- a/drivers/usb/core/hub.c
+++ b/drivers/usb/core/hub.c
@@ -31,6 +31,7 @@
 #include <asm/byteorder.h>
 
 #include "hub.h"
+#include "usb-platform.h"
 
 /* if we are in debug mode, always announce new devices */
 #ifdef DEBUG
@@ -2016,9 +2017,12 @@ void usb_disconnect(struct usb_device **pdev)
 	usb_hcd_synchronize_unlinks(udev);
 
 	if (udev->parent) {
+		struct usb_hub *hub = usb_hub_to_struct_hub(udev->parent);
+		struct usb_port *port_dev = hub->ports[udev->portnum - 1];
 		struct usb_device *hdev = udev->parent;
 
 		sysfs_remove_link(&hdev->dev.kobj, dev_name(&udev->dev));
+		usb_connector_disconnect(port_dev);
 	}
 
 	usb_remove_ep_devs(&udev->ep0);
@@ -2321,11 +2325,15 @@ int usb_new_device(struct usb_device *udev)
 	/* Create link files between child device and usb port device. */
 	if (udev->parent) {
 		struct usb_device *hdev = udev->parent;
+		struct usb_hub *hub = usb_hub_to_struct_hub(hdev);
+		struct usb_port	*port_dev = hub->ports[udev->portnum - 1];
 
 		err = sysfs_create_link(&hdev->dev.kobj, &udev->dev.kobj,
 					dev_name(&udev->dev));
 		if (err)
 			goto fail;
+
+		usb_connector_connect(port_dev);
 	}
 
 	(void) usb_create_ep_devs(&udev->dev, &udev->ep0, udev);
@@ -4367,11 +4375,13 @@ static void hub_port_connect_change(struct usb_hub *hub, int port1,
 		}
 	}
 
-	/* Return now if debouncing failed or nothing is connected or
-	 * the device was "removed".
+	/* Return now if debouncing failed, nothing is connected, the
+	 * device was "removed", or the connector is occupied by a
+	 * non-hub device
 	 */
-	if (!(portstatus & USB_PORT_STAT_CONNECTION) ||
-			test_bit(port1, hub->removed_bits)) {
+	if (!(portstatus & USB_PORT_STAT_CONNECTION)
+	    || test_bit(port1, hub->removed_bits)
+	    || usb_connector_state(port_dev) == USB_CONNECTION_EXCLUSIVE) {
 
 		/* maybe switch power back on (e.g. root hub was reset) */
 		if ((wHubCharacteristics & HUB_CHAR_LPSM) < 2
diff --git a/drivers/usb/core/hub.h b/drivers/usb/core/hub.h
index 915f76fe64c7..14439e98e573 100644
--- a/drivers/usb/core/hub.h
+++ b/drivers/usb/core/hub.h
@@ -87,6 +87,9 @@ struct usb_hub {
  * @port_owner: port's owner
  * @connect_type: port's connect type
  * @portnum: port index num based one
+ * @force_off: peer port in a connector has exclusive conncection
+ *
+ * Note force_off is still beholden to pm_qos_no_power_off
  */
 struct usb_port {
 	struct usb_device *child;
@@ -96,6 +99,7 @@ struct usb_port {
 	struct dev_state *port_owner;
 	enum usb_port_connect_type connect_type;
 	u8 portnum;
+	unsigned force_off:1;
 };
 
 #define to_usb_port(_dev) \
diff --git a/drivers/usb/core/port.c b/drivers/usb/core/port.c
index bef75e5adff2..d609b25746d2 100644
--- a/drivers/usb/core/port.c
+++ b/drivers/usb/core/port.c
@@ -76,7 +76,8 @@ static bool is_power_policy_on(struct usb_port *port_dev)
 {
 	int flag = PM_QOS_FLAG_NO_POWER_OFF;
 
-	if (port_dev->connect_type <= USB_PORT_CONNECT_TYPE_HOT_PLUG)
+	if (port_dev->connect_type <= USB_PORT_CONNECT_TYPE_HOT_PLUG
+	    && !port_dev->force_off)
 		return true;
 	if (dev_pm_qos_flags(&port_dev->dev, flag) == PM_QOS_FLAGS_ALL)
 		return true;
diff --git a/drivers/usb/core/usb-platform.c b/drivers/usb/core/usb-platform.c
index 8bec79131bb1..0e24110e8ba2 100644
--- a/drivers/usb/core/usb-platform.c
+++ b/drivers/usb/core/usb-platform.c
@@ -109,19 +109,155 @@ static struct usb_connector *create_connector(struct usb_domain *udom,
 	return uconn;
 }
 
+static enum usb_connector_state connector_occupation(struct usb_port *port_dev)
+{
+	struct usb_connector *uconn = port_dev->connector;
+	struct usb_port *peer;
+
+	for_each_connector_peer(peer, port_dev, uconn)
+		if (peer->child) {
+			struct usb_device *udev = peer->child;
+
+			if (udev->descriptor.bDeviceClass == USB_CLASS_HUB)
+				return USB_CONNECTION_SHARED;
+			else
+				return USB_CONNECTION_EXCLUSIVE;
+		}
+
+	return USB_CONNECTION_NONE;
+}
+
+enum usb_connector_state usb_connector_state(struct usb_port *port_dev)
+{
+	struct usb_connector *uconn = port_dev->connector;
+	enum usb_connector_state ret = USB_CONNECTION_NONE;
+
+	if (uconn) {
+		struct usb_domain *udom = uconn->domain;
+
+		mutex_lock(&udom->lock);
+		ret = connector_occupation(port_dev);
+		mutex_unlock(&udom->lock);
+	}
+
+	return ret;
+}
+
+/* validate that each port in the connector specifies the same connect type
+ * if the port is a hotplug port take a pm_runtime reference
+ */
 static void check_connector(struct usb_connector *uconn,
 			    struct usb_port *port_dev)
 {
 	if (uconn->connect_type != port_dev->connect_type) {
 		struct device *intf_dev = port_dev->dev.parent;
+		struct usb_port *peer;
 
 		dev_info(intf_dev, "port%d inconsistent connect types %d:%d",
 			 port_dev->portnum, port_dev->connect_type,
 			 uconn->connect_type);
-		list_for_each_entry(port_dev, &uconn->ports, node)
-			port_dev->connect_type = USB_PORT_CONNECT_TYPE_UNKNOWN;
+		for_each_connector_peer(peer, port_dev, uconn)
+			peer->connect_type = USB_PORT_CONNECT_TYPE_UNKNOWN;
+		port_dev->connect_type = USB_PORT_CONNECT_TYPE_UNKNOWN;
 		uconn->connect_type = USB_PORT_CONNECT_TYPE_UNKNOWN;
 	}
+
+	/* catch this port up with the state of the connection, i.e.
+	 * power off when peer has a single connection
+	 */
+	if (port_dev->connect_type == USB_PORT_CONNECT_TYPE_HOT_PLUG
+	    && connector_occupation(port_dev) == USB_CONNECTION_EXCLUSIVE)
+		pm_runtime_suspend(&port_dev->dev);
+}
+
+void usb_connector_connect(struct usb_port *port_dev)
+{
+	struct usb_connector *uconn = port_dev->connector;
+	struct usb_device *udev = port_dev->child;
+	struct usb_domain *udom;
+
+	if (!uconn)
+		return;
+
+	udom = uconn->domain;
+
+	mutex_lock(&udom->lock);
+	switch (connector_occupation(port_dev)) {
+	case USB_CONNECTION_SHARED:
+		WARN_ONCE(udev->descriptor.bDeviceClass != USB_CLASS_HUB,
+			  "%s not a hub and connector already connected\n",
+			  dev_name(&udev->dev));
+		break;
+	case USB_CONNECTION_NONE: {
+		struct usb_port *peer;
+
+		if (uconn->connect_type != USB_PORT_CONNECT_TYPE_HOT_PLUG)
+			break;
+
+		/* if we connected a hub we want our peers to stay up */
+		if (udev->descriptor.bDeviceClass == USB_CLASS_HUB)
+			break;
+
+		/* we're the exclusive connection, turn off hotplug
+		 * peers
+		 */
+		pm_runtime_get_sync(&port_dev->dev);
+		for_each_connector_peer(peer, port_dev, uconn) {
+			pm_runtime_get_sync(&peer->dev);
+			peer->force_off = 1;
+			pm_runtime_put(&peer->dev);
+		}
+		pm_runtime_put(&port_dev->dev);
+		break;
+	}
+	case USB_CONNECTION_EXCLUSIVE:
+		WARN_ONCE(1, "%s connector already connected\n",
+			  dev_name(&udev->dev));
+		break;
+	}
+	mutex_unlock(&udom->lock);
+}
+
+void usb_connector_disconnect(struct usb_port *port_dev)
+{
+	struct usb_connector *uconn = port_dev->connector;
+	struct usb_device *udev = port_dev->child;
+	struct usb_domain *udom;
+
+	if (!uconn)
+		return;
+
+	udom = uconn->domain;
+
+	mutex_lock(&udom->lock);
+	switch (connector_occupation(port_dev)) {
+	case USB_CONNECTION_SHARED:
+		break; /* we expect multi-disconnects in this case */
+	case USB_CONNECTION_NONE: {
+		struct usb_port *peer;
+
+		if (uconn->connect_type != USB_PORT_CONNECT_TYPE_HOT_PLUG)
+			break;
+
+		/* nothing to do if we were hub connected */
+		if (udev->descriptor.bDeviceClass == USB_CLASS_HUB)
+			break;
+
+		/* resume hotplug notifications for all ports in the connector */
+		pm_runtime_get_sync(&port_dev->dev);
+		for_each_connector_peer(peer, port_dev, uconn) {
+			pm_runtime_get_sync(&peer->dev);
+			peer->force_off = 0;
+			pm_runtime_put(&peer->dev);
+		}
+		pm_runtime_put(&port_dev->dev);
+		break;
+	}
+	case USB_CONNECTION_EXCLUSIVE:
+		WARN_ONCE(1, "%s connector conflict\n", dev_name(&udev->dev));
+		break;
+	}
+	mutex_unlock(&udom->lock);
 }
 
 static void add_port_connector(struct usb_port *port_dev,
diff --git a/drivers/usb/core/usb-platform.h b/drivers/usb/core/usb-platform.h
index 1938479dc7ad..648dbe6d9e54 100644
--- a/drivers/usb/core/usb-platform.h
+++ b/drivers/usb/core/usb-platform.h
@@ -39,7 +39,16 @@ struct usb_connector {
 	u8 pair_data[0];
 };
 
+enum usb_connector_state {
+	USB_CONNECTION_NONE,
+	USB_CONNECTION_SHARED,
+	USB_CONNECTION_EXCLUSIVE,
+};
+
 int usb_domain_pair_port(struct usb_domain *udom, struct usb_port *port_dev,
 			 void *data, size_t size);
 void usb_domain_remove_port(struct usb_domain *udom, struct usb_port *port_dev);
 bool usb_connector_notify_flags(struct usb_port *port_dev, s32 mask, bool set);
+enum usb_connector_state usb_connector_state(struct usb_port *port_dev);
+void usb_connector_connect(struct usb_port *port_dev);
+void usb_connector_disconnect(struct usb_port *port_dev);

--
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