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
This commit is contained in:
ShengYi Hung committed 2026-11-02 14:02:11 +11:00
1 parent 23ef0bdb48
commit fb470e8f1c
4 files changed
+248 -8

No files matched your search

+218 -8
View File
@@ -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;
+21
View File
@@ -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_ */
+2
View File
@@ -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;
+7
View File
@@ -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: