[PATCH v5 04/16] usb: find internal hub tier mismatch via acpi

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

 



ACPI identifies peer ports by setting their 'group_token' and
'group_position' _PLD data to the same value.  If a platform has tier
mismatch [1] , ACPI can override the default (USB3 defined) peer port
association for internal hubs.  External hubs follow the default peer
association scheme.

Location data is cached as an opaque cookie in usb_port_location data.

Note that we only consider the group_token and group_position attributes
from the _PLD data as ACPI specifies that group_token is a unique
identifier.

The bulk of the implementation is recursively fixing up peer
associations once we detect tier mismatch.  Due to the way acpi data is
associated to a usb_port device (via a callback triggered by
device_register()) we do not discover tier mismatch until after the port
has been registered.  This leads to a question about what happens when a
pm runtime event occurs while the peer associations are wrong, and can
we prevent that from occurring?  The result of wrong peer associations
is always the possibility that a USB3 device switches to its USB2
connection upon detecting the USB3 port being turned off.  As far as
closing the race there are 2 considerations:

1/ the acpi data may be wrong so the risk of wrong peer associations
cannot be entirely eliminated by the kernel

2/ if the acpi data is good then we can potentially close the race by
waiting for all the initial hub discovery to complete before processing
the first pm runtime suspend event.  (not implemented in this patch).

[1]: xhci 1.1 appendix D figure 131
[2]: acpi 5 section 6.1.8

Signed-off-by: Dan Williams <dan.j.williams@xxxxxxxxx>
---
 drivers/usb/core/hub.h      |    6 ++
 drivers/usb/core/port.c     |  161 ++++++++++++++++++++++++++++++++++++++++++-
 drivers/usb/core/usb-acpi.c |   35 +++++++--
 drivers/usb/core/usb.h      |    2 +
 4 files changed, 192 insertions(+), 12 deletions(-)

diff --git a/drivers/usb/core/hub.h b/drivers/usb/core/hub.h
index d51feb68165b..e7a9666e7cd6 100644
--- a/drivers/usb/core/hub.h
+++ b/drivers/usb/core/hub.h
@@ -76,6 +76,10 @@ struct usb_hub {
 	struct usb_port		**ports;
 };
 
+struct usb_port_location {
+	u32 cookie;
+};
+
 /**
  * struct usb port - kernel's representation of a usb port
  * @child: usb device attached to the port
@@ -83,6 +87,7 @@ struct usb_hub {
  * @port_owner: port's owner
  * @peer: related usb2 and usb3 ports (share the same connector)
  * @connect_type: port's connect type
+ * @location: opaque representation of platform connector location
  * @portnum: port index num based one
  * @power_is_on: port's power state
  * @did_runtime_put: port has done pm_runtime_put().
@@ -93,6 +98,7 @@ struct usb_port {
 	struct dev_state *port_owner;
 	struct usb_port *peer;
 	enum usb_port_connect_type connect_type;
+	struct usb_port_location location;
 	u8 portnum;
 	unsigned power_is_on:1;
 	unsigned did_runtime_put:1;
diff --git a/drivers/usb/core/port.c b/drivers/usb/core/port.c
index 068d495007e1..0b8ae73b0466 100644
--- a/drivers/usb/core/port.c
+++ b/drivers/usb/core/port.c
@@ -154,11 +154,14 @@ struct device_type usb_port_device_type = {
  */
 static struct usb_port *find_default_peer(struct usb_hub *hub, int port1)
 {
-	struct usb_device *hdev = hub->hdev;
+	struct usb_device *hdev = hub ? hub->hdev : NULL;
 	struct usb_device *peer_hdev;
 	struct usb_port *peer = NULL;
 	struct usb_hub *peer_hub;
 
+	if (!hub)
+		return NULL;
+
 	if (!hdev->parent) {
 		struct usb_hcd *hcd = bus_to_hcd(hdev->bus);
 		struct usb_hcd *peer_hcd = hcd->primary_hcd;
@@ -239,9 +242,154 @@ static void unlink_peers(struct usb_port *left, struct usb_port *right)
 	left->peer = NULL;
 }
 
+/**
+ * for_each_child_port() - invoke 'fn' on all usb_port instances beneath 'hdev'
+ * @hdev: potential hub usb_device (validated by usb_hub_to_struct_hub)
+ * @level: track recursion level to stop after reaching USB spec max depth
+ * @p: parameter to pass to 'fn'
+ * @fn: routine to invoke on each port
+ *
+ * Recursively iterate all ports (depth-first) beneath 'hdev' until 'fn'
+ * returns a non-NULL usb_port or all ports have been visited.
+ */
+static struct usb_port *for_each_child_port(struct usb_device *hdev, int level,
+		void *p, struct usb_port * (*fn)(struct usb_port *, void *))
+{
+	struct usb_hub *hub = usb_hub_to_struct_hub(hdev);
+	int port1;
+
+#define MAX_HUB_DEPTH 5
+	if (!hub || level > MAX_HUB_DEPTH)
+		return NULL;
+
+	level++;
+	for (port1 = 1; port1 <= hdev->maxchild; port1++) {
+		struct usb_port *ret, *port_dev = hub->ports[port1 - 1];
+
+		ret = fn(port_dev, p);
+		if (ret)
+			return ret;
+		ret = for_each_child_port(port_dev->child, level, p, fn);
+		if (ret)
+			return ret;
+	}
+
+	return NULL;
+}
+
+static struct usb_port *do_match_location(struct usb_port *port_dev, void *_loc)
+{
+	struct usb_port_location *loc = _loc;
+
+	if (memcmp(&port_dev->location, loc, sizeof(*loc)) == 0)
+		return port_dev;
+	return NULL;
+}
+
+static struct usb_port *find_port_by_location(struct usb_device *hdev,
+		struct usb_port_location *loc)
+{
+	return for_each_child_port(hdev, 1, loc, do_match_location);
+}
+
+static struct usb_port *do_default_link(struct usb_port *port_dev, void *p)
+{
+	struct usb_device *hdev = to_usb_device(port_dev->dev.parent->parent);
+	struct usb_hub *hub = usb_hub_to_struct_hub(hdev);
+	struct usb_port *peer;
+
+	peer = find_default_peer(hub, port_dev->portnum);
+
+	/*
+	 * Assign the peer, but since we may have run
+	 * enumerate_dependent_peers() on the peer bus it may already be
+	 * set
+	 */
+	if (peer && !port_dev->peer)
+		link_peers(port_dev, peer);
+	return NULL;
+}
+
+static void enumerate_dependent_peers(struct usb_device *hdev)
+{
+	for_each_child_port(hdev, 1, NULL, do_default_link);
+}
+
+static struct usb_port *do_unlink_peers(struct usb_port *port_dev, void *p)
+{
+	if (port_dev->peer)
+		unlink_peers(port_dev, port_dev->peer);
+	return NULL;
+}
+
+static void invalidate_dependent_peers(struct usb_port *port_dev)
+{
+	unlink_peers(port_dev, port_dev->peer);
+	for_each_child_port(port_dev->child, 1, NULL, do_unlink_peers);
+}
+
+void usb_set_hub_port_location(struct usb_device *hdev, int port1,
+		u32 cookie)
+{
+	struct usb_hub *hub = usb_hub_to_struct_hub(hdev);
+	struct usb_hcd *hcd = bus_to_hcd(hdev->bus);
+	struct usb_hcd *peer_hcd = hcd->shared_hcd;
+	int enum_port_dev = 0, enum_peer = 0;
+	struct usb_port *peer, *port_dev;
+	struct usb_device *peer_hdev;
+
+	if (cookie == 0)
+		return;
+
+	if (!hub)
+		return;
+
+	port_dev = hub->ports[port1 - 1];
+	port_dev->location.cookie = cookie;
+
+	/*
+	 * Once we set the location we need to check if this invalidates
+	 * the current peer mapping for this port
+	 */
+	if (!peer_hcd)
+		return;
+
+	mutex_lock(&peer_lock);
+	peer_hdev = peer_hcd->self.root_hub;
+	peer = find_port_by_location(peer_hdev, &port_dev->location);
+
+	/*
+	 * If the peer we found does not match the current one then we
+	 * need to invalidate all the peer relationships beneath each
+	 * port
+	 */
+	if (port_dev->peer && port_dev->peer != peer) {
+		invalidate_dependent_peers(port_dev);
+		enum_port_dev = 1;
+	}
+	if (peer->peer && peer->peer != port_dev) {
+		invalidate_dependent_peers(peer);
+		enum_peer = 1;
+	}
+
+	link_peers(port_dev, peer);
+
+	/*
+	 * If a peer relationship was invalidated then we need to
+	 * re-enumerate all the descendants.  We descend both 'port_dev'
+	 * and 'peer' since tier-mismatch implies a mismatch in the
+	 * number of descendants.
+	 */
+	if (enum_port_dev)
+		enumerate_dependent_peers(port_dev->child);
+	if (enum_peer)
+		enumerate_dependent_peers(peer->child);
+	mutex_unlock(&peer_lock);
+}
+
 int usb_hub_create_port_device(struct usb_hub *hub, int port1)
 {
-	struct usb_port *port_dev, *peer;
+	struct usb_port *port_dev;
 	int retval;
 
 	port_dev = kzalloc(sizeof(*port_dev), GFP_KERNEL);
@@ -262,9 +410,12 @@ int usb_hub_create_port_device(struct usb_hub *hub, int port1)
 		goto error_register;
 
 	mutex_lock(&peer_lock);
-	peer = find_default_peer(hub, port1);
-	if (peer)
-		link_peers(port_dev, peer);
+	if (!port_dev->peer) {
+		struct usb_port *peer = find_default_peer(hub, port1);
+
+		if (peer)
+			link_peers(port_dev, peer);
+	}
 	mutex_unlock(&peer_lock);
 
 	pm_runtime_set_active(&port_dev->dev);
diff --git a/drivers/usb/core/usb-acpi.c b/drivers/usb/core/usb-acpi.c
index 5ca4070b1f38..887b25d4409a 100644
--- a/drivers/usb/core/usb-acpi.c
+++ b/drivers/usb/core/usb-acpi.c
@@ -17,7 +17,7 @@
 #include <linux/pci.h>
 #include <linux/usb/hcd.h>
 
-#include "usb.h"
+#include "hub.h"
 
 /**
  * usb_acpi_power_manageable - check whether usb port has
@@ -41,6 +41,17 @@ bool usb_acpi_power_manageable(struct usb_device *hdev, int index)
 }
 EXPORT_SYMBOL_GPL(usb_acpi_power_manageable);
 
+static void usb_acpi_check_port_peer(struct usb_device *hdev,
+	acpi_handle *handle, int port1, struct acpi_pld_info *pld)
+{
+	if (!pld)
+		return;
+
+	#define USB_ACPI_LOCATION_VALID (1 << 31)
+	usb_set_hub_port_location(hdev, port1, USB_ACPI_LOCATION_VALID
+		| pld->group_token << 8 | pld->group_position);
+}
+
 /**
  * usb_acpi_set_power_state - control usb port's power via acpi power
  * resource
@@ -82,12 +93,11 @@ int usb_acpi_set_power_state(struct usb_device *hdev, int index, bool enable)
 EXPORT_SYMBOL_GPL(usb_acpi_set_power_state);
 
 static int usb_acpi_check_port_connect_type(struct usb_device *hdev,
-	acpi_handle handle, int port1)
+	acpi_handle handle, int port1, struct acpi_pld_info *pld)
 {
 	acpi_status status;
 	struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL };
 	union acpi_object *upc;
-	struct acpi_pld_info *pld;
 	int ret = 0;
 
 	/*
@@ -98,8 +108,7 @@ static int usb_acpi_check_port_connect_type(struct usb_device *hdev,
 	 * a usb device is directly hard-wired to the port. If no visible and
 	 * no connectable, the port would be not used.
 	 */
-	status = acpi_get_physical_device_location(handle, &pld);
-	if (ACPI_FAILURE(status))
+	if (!pld)
 		return -ENODEV;
 
 	status = acpi_evaluate_object(handle, "_UPC", NULL, &buffer);
@@ -121,7 +130,6 @@ static int usb_acpi_check_port_connect_type(struct usb_device *hdev,
 		usb_set_hub_port_connect_type(hdev, port1, USB_PORT_NOT_USED);
 
 out:
-	ACPI_FREE(pld);
 	kfree(upc);
 	return ret;
 }
@@ -176,6 +184,8 @@ static struct acpi_device *usb_acpi_find_companion(struct device *dev)
 					      udev->portnum, false);
 	} else if (is_usb_port(dev)) {
 		struct acpi_device *adev = NULL;
+		struct acpi_pld_info *pld;
+		acpi_status status;
 
 		sscanf(dev_name(dev), "port%d", &port_num);
 		/* Get the struct usb_device point of port's hub */
@@ -208,7 +218,18 @@ static struct acpi_device *usb_acpi_find_companion(struct device *dev)
 			if (!adev)
 				return NULL;
 		}
-		usb_acpi_check_port_connect_type(udev, adev->handle, port_num);
+
+		status = acpi_get_physical_device_location(adev->handle, &pld);
+		if (ACPI_FAILURE(status))
+			pld = NULL;
+
+		usb_acpi_check_port_connect_type(udev, adev->handle, port_num,
+						 pld);
+		usb_acpi_check_port_peer(udev, adev->handle, port_num, pld);
+
+		if (pld)
+			ACPI_FREE(pld);
+
 		return adev;
 	}
 
diff --git a/drivers/usb/core/usb.h b/drivers/usb/core/usb.h
index 823857767a16..303031d125db 100644
--- a/drivers/usb/core/usb.h
+++ b/drivers/usb/core/usb.h
@@ -183,6 +183,8 @@ extern enum usb_port_connect_type
 	usb_get_hub_port_connect_type(struct usb_device *hdev, int port1);
 extern void usb_set_hub_port_connect_type(struct usb_device *hdev, int port1,
 	enum usb_port_connect_type type);
+extern void usb_set_hub_port_location(struct usb_device *hdev, int port1,
+	u32 cookie);
 extern void usb_hub_adjust_deviceremovable(struct usb_device *hdev,
 		struct usb_hub_descriptor *desc);
 

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