From: Arun Iyengar via B4 Relay <devnull+aiyengar1990.gmail.com@kernel.org>
To: Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
Rob Herring <robh@kernel.org>,
Alan Stern <stern@rowland.harvard.edu>
Cc: linux-usb@vger.kernel.org, linux-kernel@vger.kernel.org,
Mathias Nyman <mathias.nyman@intel.com>,
Chen-Yu Tsai <wenst@chromium.org>,
Chen-Yu Tsai <wens@kernel.org>,
Arun Iyengar <aiyengar1990@gmail.com>
Subject: [PATCH v2 2/2] usb: hub: Retry SuperSpeed for a device stuck on its USB 2.0 peer
Date: Thu, 12 Nov 2026 08:26:17 +0000 [thread overview]
Message-ID: <20261112-usb-port-pairing-v2-2-45f5a1a29f1f@gmail.com> (raw)
In-Reply-To: <20261112-usb-port-pairing-v2-0-45f5a1a29f1f@gmail.com>
From: Arun Iyengar <aiyengar1990@gmail.com>
A SuperSpeed device looks for a SuperSpeed link first when it sees
VBUS, and connects at high speed when the link doesn't come up. It
looks again only when VBUS is cycled or after a USB 2.0 bus reset.
When a connector's two halves belong to different host controllers,
the USB 2.0 host can start first. On the Orange Pi 5 (RK3588S), whose
USB 3 Type-A port has VBUS on from power-up, a USB 3 flash drive
plugged in at boot was reset by the EHCI about 10 ms before the
SuperSpeed-only xHCI registered, so it came up at 480 Mb/s on the EHCI
in every one of 6 boots, while plugging it in after boot gave
5 Gb/s. It stays at high speed until it is unplugged.
When a SuperSpeed root hub with ports paired through a connector to a
root hub port of another host controller has started, check those
USB 2.0 peers. If a SuperSpeed-capable device that is not a hub is
connected at high speed there, reset it once, so that it can connect
at SuperSpeed now that the link can train. Lock the peer root hub
first, so an enumeration that is under way completes before the check.
A device that still can't train its SuperSpeed link stays connected at
high speed after that one reset.
Only the start of the root hub does this, not a resume or a reset of
it: a device that is suspended with the system can't be reset, and the
problem is the start-up order of the two controllers. Hubs are left
alone, because the USB 2.0 half of a USB 3 hub stays at high speed next
to its SuperSpeed half. The work runs on the freezable hub_wq, holds a
runtime PM reference on the SuperSpeed hub and is cancelled when the
hub is quiesced.
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/usb/core/hub.c | 113 +++++++++++++++++++++++++++++++++++++++++++++++++
drivers/usb/core/hub.h | 1 +
2 files changed, 114 insertions(+)
diff --git a/drivers/usb/core/hub.c b/drivers/usb/core/hub.c
index 199d3f535a89..eaa2a5b6aba0 100644
--- a/drivers/usb/core/hub.c
+++ b/drivers/usb/core/hub.c
@@ -1076,6 +1076,109 @@ int usb_remove_device(struct usb_device *udev)
return 0;
}
+/*
+ * A SuperSpeed root hub port can be paired through a connector with a USB 2.0
+ * root hub port of another host controller. With VBUS always on, a device
+ * present at boot looks for a SuperSpeed link while the SuperSpeed host is
+ * still down, gives up and connects at high speed to the USB 2.0 host. It
+ * only tries SuperSpeed again after VBUS is cycled or after a USB 2.0 bus
+ * reset, so reset it once when the SuperSpeed root hub has started.
+ */
+static void hub_reset_peer_child(struct usb_device *peer_hdev, int peer_port1)
+{
+ struct usb_hub *peer_hub;
+ struct usb_device *udev;
+
+ /* Wait for an enumeration in progress on the peer port to finish */
+ usb_lock_device(peer_hdev);
+ peer_hub = usb_hub_to_struct_hub(peer_hdev);
+ if (!peer_hub || peer_port1 > peer_hdev->maxchild)
+ goto out;
+
+ udev = peer_hub->ports[peer_port1 - 1]->child;
+ if (!udev || udev->speed != USB_SPEED_HIGH)
+ goto out;
+
+ /*
+ * The USB 2.0 half of a USB 3 hub stays at high speed next to its
+ * SuperSpeed half, and resetting it would disconnect its children.
+ */
+ if (udev->descriptor.bDeviceClass == USB_CLASS_HUB)
+ goto out;
+
+ usb_lock_device(udev);
+ if (udev->state != USB_STATE_NOTATTACHED &&
+ udev->bos && udev->bos->ss_cap) {
+ dev_info(&udev->dev,
+ "SuperSpeed peer port available, resetting\n");
+ usb_reset_device(udev);
+ }
+ usb_unlock_device(udev);
+out:
+ usb_unlock_device(peer_hdev);
+}
+
+static void hub_peer_reset_work(struct work_struct *ws)
+{
+ struct usb_hub *hub = container_of(ws, struct usb_hub, peer_reset_work);
+ struct usb_device *hdev = hub->hdev;
+ int port1;
+
+ for (port1 = 1; ; port1++) {
+ struct usb_device *peer_hdev = NULL;
+ struct usb_port *port_dev;
+ int peer_port1 = 0;
+
+ mutex_lock(&usb_port_peer_mutex);
+ if (port1 > hdev->maxchild) {
+ mutex_unlock(&usb_port_peer_mutex);
+ break;
+ }
+ port_dev = hub->ports[port1 - 1];
+ if (port_dev->connector_peer && port_dev->peer) {
+ peer_hdev = to_usb_device(port_dev->peer->dev.parent->parent);
+ peer_port1 = port_dev->peer->portnum;
+ /* Only root hub ports of another host controller */
+ if (peer_hdev->parent ||
+ peer_hdev->bus->controller == hdev->bus->controller)
+ peer_hdev = NULL;
+ else
+ usb_get_dev(peer_hdev);
+ }
+ mutex_unlock(&usb_port_peer_mutex);
+
+ if (peer_hdev) {
+ hub_reset_peer_child(peer_hdev, peer_port1);
+ usb_put_dev(peer_hdev);
+ }
+ }
+
+ usb_autopm_put_interface_async(to_usb_interface(hub->intfdev));
+ hub_put(hub);
+}
+
+/* Called when a SuperSpeed root hub has started */
+static void hub_queue_peer_reset(struct usb_hub *hub)
+{
+ struct usb_interface *intf = to_usb_interface(hub->intfdev);
+ int port1;
+
+ for (port1 = 1; port1 <= hub->hdev->maxchild; port1++) {
+ if (hub->ports[port1 - 1]->connector_peer)
+ break;
+ }
+ if (port1 > hub->hdev->maxchild)
+ return;
+
+ /* Keep the hub active until the work is done; hub_quiesce() cancels it */
+ usb_autopm_get_interface_no_resume(intf);
+ hub_get(hub);
+ if (!queue_work(hub_wq, &hub->peer_reset_work)) {
+ usb_autopm_put_interface_async(intf);
+ hub_put(hub);
+ }
+}
+
enum hub_activation_type {
HUB_INIT, HUB_INIT2, HUB_INIT3, /* INITs must come first */
HUB_POST_RESET, HUB_RESUME, HUB_RESET_RESUME,
@@ -1346,6 +1449,11 @@ static void hub_activate(struct usb_hub *hub, enum hub_activation_type type)
/* Scan all ports that need attention */
kick_hub_wq(hub);
+
+ /* Let devices stuck at high speed on a connector peer move over */
+ if (!hdev->parent && hub_is_superspeed(hdev) &&
+ (type == HUB_INIT2 || type == HUB_INIT3))
+ hub_queue_peer_reset(hub);
abort:
if (type == HUB_INIT2 || type == HUB_INIT3) {
/* Allow autosuspend if it was suppressed */
@@ -1419,6 +1527,10 @@ static void hub_quiesce(struct usb_hub *hub, enum hub_quiescing_type type)
/* Stop hub_wq and related activity */
timer_delete_sync(&hub->irq_urb_retry);
flush_delayed_work(&hub->post_resume_work);
+ if (cancel_work_sync(&hub->peer_reset_work)) {
+ usb_autopm_put_interface_no_suspend(to_usb_interface(hub->intfdev));
+ hub_put(hub);
+ }
usb_kill_urb(hub->urb);
if (hub->has_indicators)
cancel_delayed_work_sync(&hub->leds);
@@ -1978,6 +2090,7 @@ static int hub_probe(struct usb_interface *intf, const struct usb_device_id *id)
INIT_DELAYED_WORK(&hub->leds, led_work);
INIT_DELAYED_WORK(&hub->init_work, NULL);
INIT_DELAYED_WORK(&hub->post_resume_work, hub_post_resume);
+ INIT_WORK(&hub->peer_reset_work, hub_peer_reset_work);
INIT_WORK(&hub->events, hub_event);
INIT_LIST_HEAD(&hub->onboard_devs);
spin_lock_init(&hub->irq_urb_lock);
diff --git a/drivers/usb/core/hub.h b/drivers/usb/core/hub.h
index 78382839be48..edce19d7bf13 100644
--- a/drivers/usb/core/hub.h
+++ b/drivers/usb/core/hub.h
@@ -71,6 +71,7 @@ struct usb_hub {
struct delayed_work leds;
struct delayed_work init_work;
struct delayed_work post_resume_work;
+ struct work_struct peer_reset_work;
struct work_struct events;
spinlock_t irq_urb_lock;
struct timer_list irq_urb_retry;
--
2.55.0
prev parent reply other threads:[~2026-11-12 8:26 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-12 8:26 [PATCH v2 0/2] usb: core: Pair ports of different host controllers through their connector Arun Iyengar via B4 Relay
2026-11-12 8:26 ` [PATCH v2 1/2] usb: core: Pair ports of different hubs " Arun Iyengar via B4 Relay
2026-11-12 8:26 ` Arun Iyengar via B4 Relay [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261112-usb-port-pairing-v2-2-45f5a1a29f1f@gmail.com \
--to=devnull+aiyengar1990.gmail.com@kernel.org \
--cc=aiyengar1990@gmail.com \
--cc=gregkh@linuxfoundation.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-usb@vger.kernel.org \
--cc=mathias.nyman@intel.com \
--cc=robh@kernel.org \
--cc=stern@rowland.harvard.edu \
--cc=wens@kernel.org \
--cc=wenst@chromium.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®