On Mon, 3 Mar 2014, Dan Williams wrote:
> Subject: usb: find internal hub tier mismatch via acpi
>
> From: Dan Williams <[email protected]>
>
> 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.
>
> When we find port location data for a port then we assume that the
> firmware will also describe its peer port. This allows the
> implementation to only ever set the peer once. This leads to a question
> about what happens when a pm runtime event occurs while the peer
> associations are still resolving. Since we only ever set the peer
> information once, a USB3 port needs to be prevented from suspending
> while its ->peer pointer is NULL (implemented in a subsequent patch).
>
> There is always the possibility that firmware mis-identifies the ports,
> but there is not much the kernel can do in that case.
>
> [1]: xhci 1.1 appendix D figure 131
> [2]: acpi 5 section 6.1.8
>
> [alan]: don't do default peering when acpi data present
> Suggested-by: Alan Stern <[email protected]>
> Signed-off-by: Dan Williams <[email protected]>
> static struct usb_port *find_default_peer(struct usb_hub *hub, int port1)
> {
> + struct usb_port *port_dev = hub->ports[port1 - 1];
> struct usb_device *hdev = hub->hdev;
> struct usb_device *peer_hdev = NULL;
> struct usb_hub *peer_hub;
>
> + /*
> + * If location data is available then we can only peer this port
> + * by a location match, not the default peer (lest we create a
> + * situation where we need to go back and undo a default peering
> + * when the port is later peered by location data)
> + */
> + if (port_dev->location)
> + return NULL;
I think you probably also want to reject matches where
port_dev->location is 0 but the peer port does have location data.
> @@ -222,9 +233,92 @@ 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 || hub->disconnected
> + || hdev->state == USB_STATE_NOTATTACHED)
> + return NULL;
> +
> + level++;
> + for (port1 = 1; port1 <= hdev->maxchild; port1++) {
> + struct usb_port *ret, *port_dev = hub->ports[port1 - 1];
> +
> + if (!port_dev)
> + continue;
> + 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)
> +{
> + usb_port_location_t *loc = _loc;
> +
> + if (port_dev->location == *loc)
> + return port_dev;
> + return NULL;
> +}
> +
> +static struct usb_port *find_port_by_location(struct usb_device *hdev,
> + usb_port_location_t *loc)
> +{
> + return for_each_child_port(hdev, 1, loc, do_match_location);
> +}
> +
> +void usb_set_hub_port_location(struct usb_device *hdev, int port1,
> + usb_port_location_t location)
> +{
> + 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;
> + struct usb_port *peer, *port_dev;
> + struct usb_device *peer_hdev;
> +
> + if (!hub)
> + return;
> +
> + port_dev = hub->ports[port1 - 1];
> + port_dev->location = location;
> + if (port_dev->peer) {
> + WARN_ON(1);
> + return;
> + }
> +
> + if (!peer_hcd || !peer_hcd->rh_registered)
> + return;
> +
> + peer_hdev = peer_hcd->self.root_hub;
> + peer = find_port_by_location(peer_hdev, &port_dev->location);
> + if (!peer)
> + return;
> +
> + link_peers(port_dev, peer);
> +}
This all looks more complicated than necessary. Why not define a
usb_for_each_port function, like the usb_for_each_dev routine in usb.c?
And instead of calling it in a new usb_set_hub_port_location routine,
why not call it from find_default_peer?
> @@ -247,9 +341,12 @@ int usb_hub_create_port_device(struct usb_hub *hub, int
> port1)
> if (retval)
> goto error_register;
>
> - peer = find_default_peer(hub, port1);
> - if (peer)
> - link_peers(port_dev, peer);
> + if (!port_dev->peer) {
port_dev->peer should never be set at this point, if you don't try to
do the location matching during the ACPI callback.
> + struct usb_port *peer = find_default_peer(hub, port1);
> +
> + if (peer)
> + link_peers(port_dev, peer);
> + }
>
> pm_runtime_set_active(&port_dev->dev);
>
> diff --git a/drivers/usb/core/usb-acpi.c b/drivers/usb/core/usb-acpi.c
> index ce0f4e8d81cb..e1e1f40f6950 100644
> --- a/drivers/usb/core/usb-acpi.c
> +++ b/drivers/usb/core/usb-acpi.c
> @@ -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);
> +}
Why call an external function? Just store the value in the appropriate
port_dev->location.
Alan Stern
--
To unsubscribe from this list: send the line "unsubscribe linux-usb" in
the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html