git: fb470e8f1c58 - main - usb: Add support for SSP and SSPx2 hubs

From: ShengYi Hung <aokblast_at_FreeBSD.org>
Date: Mon, 02 Nov 2026 03:02:53 UTC
The branch main has been updated by aokblast:

URL: https://cgit.FreeBSD.org/src/commit/?id=fb470e8f1c58f4fcdeb163d118fb1d6526da325d

commit fb470e8f1c58f4fcdeb163d118fb1d6526da325d
Author:     ShengYi Hung <aokblast@FreeBSD.org>
AuthorDate: 2026-05-23 07:00:45 +0000
Commit:     ShengYi Hung <aokblast@FreeBSD.org>
CommitDate: 2026-11-02 03:02:11 +0000

    usb: Add support for SSP and SSPx2 hubs
    
    An SSP hub reports its sublink speeds in the BOS descriptor, and
    Extended Port Status tells which one each port uses.  Read it based on
    bcdUSB, since the ports of a hub can be faster than its upstream link.
    Without it, the ports are treated as SuperSpeed.
    
    Read the speed again in usbd_req_re_enumerate(), since a link can come
    back at another speed.
    
    Sponsored by:   The FreeBSD Foundation
    Differential Revision:  https://reviews.freebsd.org/D57204
---
 sys/dev/usb/usb_hub.c         | 226 ++++++++++++++++++++++++++++++++++++++++--
 sys/dev/usb/usb_hub.h         |  21 ++++
 sys/dev/usb/usb_hub_private.h |   2 +
 sys/dev/usb/usb_request.c     |   7 ++
 4 files changed, 248 insertions(+), 8 deletions(-)

diff --git a/sys/dev/usb/usb_hub.c b/sys/dev/usb/usb_hub.c
index ee9d8ab0c9bb..c8f5e8204391 100644
--- a/sys/dev/usb/usb_hub.c
+++ b/sys/dev/usb/usb_hub.c
@@ -588,6 +588,97 @@ uhub_read_port_status(struct uhub_softc *sc, uint8_t portno)
 	return (err);
 }
 
+static enum usb_dev_speed
+usb_hub_sublink_status_to_speed(struct usb_hub_ss_sublinks *links, int id,
+    int lanes)
+{
+	uint32_t gbps;
+	uint8_t exp;
+
+	DPRINTFN(4, "exp: %d, val: %d, lanes: %d\n", links->hub_status[id].exp,
+	    links->hub_status[id].val, lanes);
+
+	gbps = links->hub_status[id].val;
+	if (gbps == 0)
+		return (USB_SPEED_VARIABLE);
+
+	for (exp = links->hub_status[id].exp; exp < 3; exp++)
+		gbps /= 1000;
+
+	if (links->hub_status[id].lp != USB_DEVCAP_SSPLUS_LP_SSP)
+		return (USB_SPEED_SUPER);
+	if (lanes == 1)
+		return (gbps >= 10 ? USB_SPEED_SUPER_PLUS : USB_SPEED_SUPER);
+	if (gbps >= 10)
+		return (USB_SPEED_SUPER_PLUS_GEN2_X2);
+	if (gbps >= 5)
+		return (USB_SPEED_SUPER_PLUS_X2);
+
+	return (USB_SPEED_VARIABLE);
+}
+
+/*------------------------------------------------------------------------*
+ *	uhub_get_ext_port_speed
+ *
+ * Returns:
+ * The speed of the link on the given HUB port.
+ * USB_SPEED_VARIABLE: The speed could not be determined.
+ *------------------------------------------------------------------------*/
+static enum usb_dev_speed
+uhub_get_ext_port_speed(struct usb_device *udev, struct mtx *mtx,
+    uint8_t portno)
+{
+	struct usb_ext_status ps;
+	struct uhub_softc *sc;
+	enum usb_dev_speed rx, tx;
+	uint32_t status, rx_id, tx_id;
+
+	sc = udev->hub->hubsoftc;
+	if ((sc->sc_flags & UHUB_FLAG_HAS_SUBLINKS) == 0)
+		return (USB_SPEED_VARIABLE);
+	if (usbd_req_get_ext_port_status(udev, mtx, &ps, portno) != 0)
+		return (USB_SPEED_VARIABLE);
+
+	status = UGETDW(ps.dwExtPortStatus);
+	rx_id = UDS_RX_SUBLINK_SPEED_ID_BITS(status);
+	tx_id = UDS_TX_SUBLINK_SPEED_ID_BITS(status);
+	DPRINTFN(4, "port %d, rx_id: %d tx_id: %d\n", portno, rx_id, tx_id);
+
+	/* The lane counts are zero based */
+	rx = usb_hub_sublink_status_to_speed(&sc->sc_slinks, rx_id * 2,
+	    UDS_RX_SUBLINK_LANE_COUNT_BITS(status) + 1);
+	tx = usb_hub_sublink_status_to_speed(&sc->sc_slinks, (tx_id * 2) + 1,
+	    UDS_TX_SUBLINK_LANE_COUNT_BITS(status) + 1);
+
+	return (MIN(rx, tx));
+}
+
+/*------------------------------------------------------------------------*
+ *	uhub_update_ss_port_speed
+ *
+ * Reads the speed of the Enhanced SuperSpeed link on the given HUB port
+ * and records if a link above SuperSpeed has an unknown speed.
+ *
+ * Returns:
+ * The speed of the link, "speed" if it could not be determined.
+ *------------------------------------------------------------------------*/
+enum usb_dev_speed
+uhub_update_ss_port_speed(struct usb_device *udev, struct mtx *mtx,
+    uint8_t portno, enum usb_dev_speed speed)
+{
+	enum usb_dev_speed ext_speed;
+
+	if (udev->hub == NULL)
+		return (speed);
+
+	ext_speed = uhub_get_ext_port_speed(udev, mtx, portno);
+	udev->hub->ports[portno - 1].ss_speed_unknown =
+	    (ext_speed == USB_SPEED_VARIABLE &&
+	    udev->speed > USB_SPEED_SUPER);
+
+	return (ext_speed != USB_SPEED_VARIABLE ? ext_speed : speed);
+}
+
 /*------------------------------------------------------------------------*
  *	uhub_reattach_port
  *
@@ -660,6 +751,9 @@ repeat:
 		power_mask = UPS_PORT_POWER;
 		break;
 	case USB_SPEED_SUPER:
+	case USB_SPEED_SUPER_PLUS:
+	case USB_SPEED_SUPER_PLUS_X2:
+	case USB_SPEED_SUPER_PLUS_GEN2_X2:
 		if (udev->parent_hub == NULL)
 			power_mask = 0;	/* XXX undefined */
 		else
@@ -753,9 +847,11 @@ repeat:
 	case USB_SPEED_LOW:
 		speed = USB_SPEED_LOW;
 		break;
+	case USB_SPEED_SUPER_PLUS:
+	case USB_SPEED_SUPER_PLUS_X2:
+	case USB_SPEED_SUPER_PLUS_GEN2_X2:
 	case USB_SPEED_SUPER:
 		if (udev->parent_hub == NULL) {
-			/* Root HUB - special case */
 			switch (sc->sc_st.port_status & UPS_OTHER_SPEED) {
 			case 0:
 				speed = USB_SPEED_FULL;
@@ -773,22 +869,26 @@ repeat:
 		} else {
 			speed = USB_SPEED_SUPER;
 		}
+		if (speed == USB_SPEED_SUPER) {
+			speed = uhub_update_ss_port_speed(udev, NULL, portno,
+			    speed);
+		}
 		break;
 	default:
 		/* same speed like parent */
 		speed = udev->speed;
 		break;
 	}
-	if (speed == USB_SPEED_SUPER) {
-		err = usbd_req_set_hub_u1_timeout(udev, NULL,
-		    portno, 128 - (2 * udev->depth));
+	if (speed >= USB_SPEED_SUPER) {
+		err = usbd_req_set_hub_u1_timeout(udev, NULL, portno,
+		    127 - (2 * udev->depth));
 		if (err) {
 			DPRINTFN(0, "port %d U1 timeout "
 			    "failed, error=%s\n",
 			    portno, usbd_errstr(err));
 		}
-		err = usbd_req_set_hub_u2_timeout(udev, NULL,
-		    portno, 128 - (2 * udev->depth));
+		err = usbd_req_set_hub_u2_timeout(udev, NULL, portno,
+		    127 - (2 * udev->depth));
 		if (err) {
 			DPRINTFN(0, "port %d U2 timeout "
 			    "failed, error=%s\n",
@@ -973,6 +1073,9 @@ uhub_is_too_deep(struct usb_device *udev)
 			return (1);
 		break;
 	case USB_SPEED_SUPER:
+	case USB_SPEED_SUPER_PLUS:
+	case USB_SPEED_SUPER_PLUS_X2:
+	case USB_SPEED_SUPER_PLUS_GEN2_X2:
 		if (udev->depth > USB_SS_HUB_DEPTH_MAX)
 			return (1);
 		break;
@@ -1088,7 +1191,7 @@ uhub_explore(struct usb_device *udev)
 			if (err != USB_ERR_NORMAL_COMPLETION)
 				retval = err;
 		}
-		if (udev->speed == USB_SPEED_SUPER &&
+		if (udev->speed >= USB_SPEED_SUPER &&
 		    (sc->sc_st.port_change & UPS_C_BH_PORT_RESET) != 0) {
 			DPRINTF("Warm reset finished on port %u.\n", portno);
 			err = usbd_req_clear_port_feature(
@@ -1138,6 +1241,96 @@ uhub_probe(device_t dev)
 	return (ENXIO);
 }
 
+static usb_error_t
+uhub_ss_caps_get(struct usb_bos_descriptor *bdesc,
+    struct usb_devcap_ss_plus_descriptor **desc)
+{
+	struct usb_bos_cap_descriptor *cap = NULL;
+	uint8_t i;
+
+	*desc = NULL;
+	if (bdesc == NULL)
+		return (USB_ERR_INVAL);
+
+	for (i = 0; i != bdesc->bNumDeviceCaps; i++) {
+		cap = usbd_bos_foreach(bdesc, cap);
+		if (cap == NULL)
+			break;
+		if (cap->bDescriptorType == UDESC_DEVICE_CAPABILITY &&
+		    cap->bDevCapabilityType == USB_DEVCAP_SUPERSPEED_PLUS) {
+			*desc = (struct usb_devcap_ss_plus_descriptor *)cap;
+			return (0);
+		}
+	}
+	return (USB_ERR_INVAL);
+}
+
+static usb_error_t
+uhub_ss_caps_to_hub_sslink(struct usb_devcap_ss_plus_descriptor *desc,
+    struct usb_hub_ss_sublinks *links)
+{
+	uint32_t attrs;
+	uint32_t n;
+	uint32_t link_val;
+	uint16_t val;
+	uint8_t exp, id, lp, st, first, last;
+	uint32_t i;
+
+	if (desc->hdr.bLength < sizeof(*desc))
+		return (USB_ERR_INVAL);
+
+	attrs = UGETDW(desc->hdr.bmAttributes);
+	n = USB_DEVCAP_SSPLUS_SSAC_GET(attrs) + 1;
+	if (desc->hdr.bLength < sizeof(*desc) + n * sizeof(uDWord))
+		return (USB_ERR_INVAL);
+
+	for (i = 0; i < n; ++i) {
+		link_val = UGETDW(desc->bmSublinkSpeedAttr[i]);
+		id = USB_DEVCAP_SSPLUS_SSID_GET(link_val);
+		st = USB_DEVCAP_SSPLUS_ST_GET(link_val);
+		exp = USB_DEVCAP_SSPLUS_LSE_GET(link_val);
+		lp = USB_DEVCAP_SSPLUS_LP_GET(link_val);
+		val = USB_DEVCAP_SSPLUS_LSM_GET(link_val);
+
+		/*
+		 * A symmetric link applies to both directions
+		 */
+		first = id * 2 + ((st & 0x01) ? (st >> 1) : 0);
+		last = id * 2 + ((st & 0x01) ? (st >> 1) : 1);
+		for (; first <= last; first++) {
+			links->hub_status[first].exp = exp;
+			links->hub_status[first].lp = lp;
+			links->hub_status[first].val = val;
+		}
+	}
+
+	return (USB_ERR_NORMAL_COMPLETION);
+}
+
+static void
+uhub_read_sublinks(struct uhub_softc *sc)
+{
+	struct usb_device *udev = sc->sc_udev;
+	struct usb_bos_descriptor *bdesc;
+	struct usb_devcap_ss_plus_descriptor *dev_desc;
+	usb_error_t err;
+
+	err = usbd_req_get_bos_descriptor_full(udev, NULL, &bdesc);
+	if (err == 0) {
+		err = uhub_ss_caps_get(bdesc, &dev_desc);
+		if (err == 0)
+			err = uhub_ss_caps_to_hub_sslink(dev_desc,
+			    &sc->sc_slinks);
+		free(bdesc, M_USBDEV);
+	}
+	if (err == 0) {
+		sc->sc_flags |= UHUB_FLAG_HAS_SUBLINKS;
+	} else if (udev->speed != USB_SPEED_SUPER) {
+		DPRINTFN(0, "Getting USB SS Plus sublinks failed, "
+		    "error=%s\n", usbd_errstr(err));
+	}
+}
+
 /* NOTE: The information returned by this function can be wrong. */
 usb_error_t
 uhub_query_info(struct usb_device *udev, uint8_t *pnports, uint8_t *ptt)
@@ -1172,7 +1365,9 @@ uhub_query_info(struct usb_device *udev, uint8_t *pnports, uint8_t *ptt)
 		if (udev->speed == USB_SPEED_HIGH)
 			tt = (UGETW(hubdesc20.wHubCharacteristics) >> 5) & 3;
 		break;
-
+	case USB_SPEED_SUPER_PLUS:
+	case USB_SPEED_SUPER_PLUS_X2:
+	case USB_SPEED_SUPER_PLUS_GEN2_X2:
 	case USB_SPEED_SUPER:
 		err = usbd_req_get_ss_hub_descriptor(udev, NULL, &hubdesc30, 1);
 		if (err) {
@@ -1302,7 +1497,19 @@ uhub_attach(device_t dev)
 			}
 		}
 		break;
+	case USB_SPEED_SUPER_PLUS:
+	case USB_SPEED_SUPER_PLUS_X2:
+	case USB_SPEED_SUPER_PLUS_GEN2_X2:
 	case USB_SPEED_SUPER:
+		/*
+		 * The ports of a HUB can be faster than its own upstream
+		 * link, so check the revision instead of the speed. The
+		 * HUB stays usable without sublink information, its ports
+		 * are then treated as plain SuperSpeed. Only an SSP capable
+		 * HUB is required to have it.
+		 */
+		if (UGETW(udev->ddesc.bcdUSB) >= 0x0310)
+			uhub_read_sublinks(sc);
 		if (udev->parent_hub != NULL) {
 			err = usbd_req_set_hub_depth(udev, NULL,
 			    udev->depth - 1);
@@ -1474,6 +1681,9 @@ uhub_attach(device_t dev)
 				removable++;
 			break;
 		case USB_SPEED_SUPER:
+		case USB_SPEED_SUPER_PLUS:
+		case USB_SPEED_SUPER_PLUS_X2:
+		case USB_SPEED_SUPER_PLUS_GEN2_X2:
 			if (!UHD_NOT_REMOV(&hubdesc30, portno))
 				removable++;
 			break;
diff --git a/sys/dev/usb/usb_hub.h b/sys/dev/usb/usb_hub.h
index ffeabc7c0f05..23ec62697714 100644
--- a/sys/dev/usb/usb_hub.h
+++ b/sys/dev/usb/usb_hub.h
@@ -36,11 +36,30 @@ struct usb_port {
 #define	USB_RESTART_MAX 5
 	uint8_t	device_index;		/* zero means not valid */
 	enum usb_hc_mode usb_mode;	/* host or device mode */
+	uint8_t	ss_speed_unknown;	/* SSP link of unknown speed */
 #if USB_HAVE_TT_SUPPORT
 	struct usb_device_request req_reset_tt __aligned(4);
 #endif
 };
 
+#define UHUB_SS_SUBLINKS_MAX_IDS 16
+
+/*
+ * The following structure define the usb_sublink_information
+ */
+struct usb_hub_ss_sublinks {
+	struct {
+		uint8_t exp;
+		uint8_t lp;
+		uint16_t val;
+		/*
+		 * Max 4 bit. For each we have 1 rx and 1 tx.
+		 * IDs may be sparse; val is zero if not advertised.
+		 * val is the speed of one lane, not of the link.
+		 */
+	} hub_status[(UHUB_SS_SUBLINKS_MAX_IDS) * 2];
+};
+
 /*
  * The following structure defines an USB HUB.
  */
@@ -77,5 +96,7 @@ void	usb_bus_powerd(struct usb_bus *bus);
 void	uhub_root_intr(struct usb_bus *, const uint8_t *, uint8_t);
 usb_error_t uhub_query_info(struct usb_device *, uint8_t *, uint8_t *);
 void	uhub_explore_handle_re_enumerate(struct usb_device *);
+enum usb_dev_speed uhub_update_ss_port_speed(struct usb_device *, struct mtx *,
+	    uint8_t, enum usb_dev_speed);
 
 #endif					/* _USB_HUB_H_ */
diff --git a/sys/dev/usb/usb_hub_private.h b/sys/dev/usb/usb_hub_private.h
index 5ca4d1ca0a2a..e0d641bf3812 100644
--- a/sys/dev/usb/usb_hub_private.h
+++ b/sys/dev/usb/usb_hub_private.h
@@ -65,6 +65,8 @@ struct uhub_softc {
 #define	UHUB_USB_PORT_ERRORS_MAX 4
 	uint8_t	sc_flags;
 #define	UHUB_FLAG_DID_EXPLORE 0x01
+#define	UHUB_FLAG_HAS_SUBLINKS 0x02	/* sc_slinks is valid */
+	struct usb_hub_ss_sublinks sc_slinks;
 };
 struct hub_result {
 	struct usb_device *udev;
diff --git a/sys/dev/usb/usb_request.c b/sys/dev/usb/usb_request.c
index 2a57c75a85d2..f67c9bf51761 100644
--- a/sys/dev/usb/usb_request.c
+++ b/sys/dev/usb/usb_request.c
@@ -68,6 +68,7 @@
 
 #include <dev/usb/usb_controller.h>
 #include <dev/usb/usb_bus.h>
+#include <dev/usb/usb_hub.h>
 #include <sys/ctype.h>
 #endif			/* USB_GLOBAL_INCLUDE_FILE */
 
@@ -2198,6 +2199,12 @@ retry:
 		goto done;
 	}
 
+	/* The link may have been trained at another speed */
+	if (udev->speed >= USB_SPEED_SUPER) {
+		udev->speed = uhub_update_ss_port_speed(parent_hub, mtx,
+		    udev->port_no, udev->speed);
+	}
+
 	/*
 	 * After that the port has been reset our device should be at
 	 * address zero: