usb: recognize and configure hubs, power downstream ports (B4a)

First slice of hub support (docs/usb-hub.md), handled IN the xhci-bus
because a device behind a hub is reached by the CONTROLLER via a route
string in its slot context — topology only exists inside the driver
that owns the controller. When the scan enumerates a class-9 device it
now calls setupHub: read the hub descriptor for the downstream port
count and TT arrangement, tell the controller the slot is a hub
(Configure Endpoint with the slot add-flag sets the Hub bit, Number of
Ports, and MTT/TT-Think-Time per xHCI 4.6.6), SET_HUB_DEPTH for a
SuperSpeed hub so it can compose route strings, and SET_FEATURE
PORT_POWER every downstream port.

The Device gains topology fields (route string, root port, parent hub
slot/port for the TT) and buildAddressInputContext now fills them, so
the Address Device path is ready for downstream devices — those are
enumerated in B4b. A root-port device gets route 0 and its own port as
the chain root, unchanged behavior.

Verified in QEMU with a hub on a second controller and a device behind
it (a usb-hub QEMU case, since the boot controller's auto-assigned
devices collide on the low ports): 'hub slot 1: 8 downstream ports
powered (USB2 single-TT)'. Full suite 89/89. The user's SuperSpeed
Genesys hub is the real-hardware target, flagged separately (QEMU's
USB2 hub doesn't model the compound USB3 hub).
This commit is contained in:
Daniel Samson
2026-07-21 21:56:59 +01:00
parent 983b4ed05a
commit 92b03b8d07
4 changed files with 326 additions and 4 deletions
@@ -280,6 +280,18 @@ pub const Device = struct {
// Transfer rings configured for this device's interrupt/bulk endpoints.
endpoint_ring_count: u8 = 0,
endpoint_rings: [max_configured_endpoints]ConfiguredEndpoint = [_]ConfiguredEndpoint{.{}} ** max_configured_endpoints,
// Hub topology (docs/usb-hub.md). A device behind a hub is addressed with a
// route string; these carry the fields buildAddressInputContext needs.
route: u32 = 0, // Slot Context route string (5 tiers x 4 bits); 0 = on a root port
root_port: u32 = 0, // the ROOT-hub port the chain hangs off (inherited down a chain)
parent_slot: u8 = 0, // the parent hub's slot id (0 = on a root port) — the TT hub
parent_port: u8 = 0, // the parent hub's downstream port this device sits on
// Set when this device IS a hub, after setupHub configures it.
is_hub: bool = false,
hub_ports: u8 = 0, // downstream port count from the hub descriptor
hub_multi_tt: bool = false,
};
// A standing interrupt-IN subscription: the endpoint's ring is kept armed with a
@@ -315,6 +327,101 @@ const max_devices = 8;
const max_subscriptions = 8;
const report_queue_capacity = 16;
// --- USB hub class requests + constants (docs/usb-hub.md) ------------------
//
// A hub is bus infrastructure the CONTROLLER driver handles in-process: the
// route strings and slot contexts a downstream device needs only exist here.
// These are the class-specific control requests to a hub device.
fn routeDepth(route: u32) u16 {
// Tiers used by a route string: each nonzero 4-bit nibble is one tier.
var depth: u16 = 0;
var r = route;
while (r != 0) : (r >>= 4) {
if (r & 0xF != 0) depth += 1;
}
return depth;
}
const hub = struct {
// Hub descriptor types (GET_DESCRIPTOR value high byte).
const descriptor_usb2: u8 = 0x29;
const descriptor_usb3: u8 = 0x2A;
// Hub/port feature selectors (SET_FEATURE / CLEAR_FEATURE value).
const feature_port_reset: u16 = 4;
const feature_port_power: u16 = 8;
const feature_c_port_connection: u16 = 16;
const feature_c_port_reset: u16 = 20;
const feature_c_port_link_state: u16 = 25; // SS
const feature_bh_port_reset: u16 = 28; // SS
const feature_c_bh_port_reset: u16 = 29; // SS
// Port status (wPortStatus, first 16 bits of the 4-byte GET_STATUS result).
const status_connection: u16 = 1 << 0;
const status_enable: u16 = 1 << 1;
const status_reset: u16 = 1 << 4;
// Port-status change bits (wPortChange, the high 16 bits).
const change_connection: u16 = 1 << 0;
const change_reset: u16 = 1 << 4;
// wHubCharacteristics bit 7: multiple transaction translators.
const characteristics_multi_tt: u16 = 1 << 7;
// SET_HUB_DEPTH (SuperSpeed hubs, so they can compose route strings).
const request_set_hub_depth: u8 = 12;
fn getDescriptor(kind: u8, length: u16) usb_abi.Request {
return .{
.request_type = .{ .recipient = .device, .kind = .class, .direction = .device_to_host },
.request_code = .get_descriptor,
.value = @as(u16, kind) << 8,
.index = 0,
.length = length,
};
}
fn setPortFeature(feature: u16, port: u16) usb_abi.Request {
return .{
.request_type = .{ .recipient = .other, .kind = .class, .direction = .host_to_device },
.request_code = .set_feature,
.value = feature,
.index = port,
.length = 0,
};
}
fn clearPortFeature(feature: u16, port: u16) usb_abi.Request {
return .{
.request_type = .{ .recipient = .other, .kind = .class, .direction = .host_to_device },
.request_code = .clear_feature,
.value = feature,
.index = port,
.length = 0,
};
}
fn getPortStatus(port: u16) usb_abi.Request {
return .{
.request_type = .{ .recipient = .other, .kind = .class, .direction = .device_to_host },
.request_code = .get_status,
.value = 0,
.index = port,
.length = 4,
};
}
fn setHubDepth(depth: u16) usb_abi.Request {
return .{
.request_type = .{ .recipient = .device, .kind = .class, .direction = .host_to_device },
.request_code = @enumFromInt(request_set_hub_depth),
.value = depth,
.index = 0,
.length = 0,
};
}
};
pub const Controller = struct {
register_base: usize,
op_base: usize,
@@ -623,10 +730,19 @@ pub const Controller = struct {
const base = device.input_context.virtual;
// Input Control Context (index 0): Add flags in dword 1 = A0 | A1.
contextDword(base, 0, 1, cs).* = 0b11;
// Slot Context (index 1): speed[23:20], Context Entries[31:27] = 1.
contextDword(base, 1, 0, cs).* = (device.speed << 20) | (@as(u32, 1) << 27);
// Root Hub Port Number[23:16].
contextDword(base, 1, 1, cs).* = device.port << 16;
// Slot Context dword 0: Route String[19:0], Speed[23:20], Context
// Entries[31:27] = 1.
contextDword(base, 1, 0, cs).* = (device.route & 0xFFFFF) | (device.speed << 20) | (@as(u32, 1) << 27);
// Slot Context dword 1: Root Hub Port Number[23:16] — the ROOT port the
// hub chain hangs off (inherited down a chain), not the device's own
// downstream hub port.
contextDword(base, 1, 1, cs).* = device.root_port << 16;
// Slot Context dword 2: the transaction translator — a full/low-speed
// device behind a high-speed hub routes split transactions through the
// parent hub's TT. Parent Hub Slot ID[7:0], Parent Port Number[13:8].
if (device.parent_slot != 0 and device.speed < 3) {
contextDword(base, 1, 2, cs).* = @as(u32, device.parent_slot) | (@as(u32, device.parent_port) << 8);
}
// EP0 Context (index 2): CErr[2:1]=3, EP Type[5:3]=Control(4), MPS[31:16].
contextDword(base, 2, 1, cs).* = (@as(u32, 3) << 1) | (@as(u32, 4) << 3) | (device.max_packet_size_0 << 16);
// TR Dequeue Pointer (dwords 2:3) with Dequeue Cycle State = 1.
@@ -691,6 +807,7 @@ pub const Controller = struct {
.port = port,
.speed = effective_speed,
.max_packet_size_0 = defaultMaxPacketSize0(effective_speed),
.root_port = port, // a root-port device: the chain root IS this port
};
device.input_context = dma.alloc(page_size, dma.coherent) orelse return self.abandon(device);
device.device_context = dma.alloc(page_size, dma.coherent) orelse return self.abandon(device);
@@ -706,6 +823,76 @@ pub const Controller = struct {
}
/// Configure an enumerated class-9 device as a hub (docs/usb-hub.md): read
/// the hub descriptor for the downstream port count, tell the controller the
/// slot is a hub (so it routes downstream traffic), SET_HUB_DEPTH for a
/// SuperSpeed hub, and power every downstream port. Downstream enumeration
/// (status-change handling) is B4b. Returns false on a control-transfer
/// failure; the hub is still registered, just inert.
pub fn setupHub(self: *Controller, device: *Device) bool {
const is_usb3 = device.speed >= 4;
var descriptor: [16]u8 = undefined;
const kind: u8 = if (is_usb3) hub.descriptor_usb3 else hub.descriptor_usb2;
if (!self.controlTransfer(device, hub.getDescriptor(kind, descriptor.len), descriptor[0..], true)) {
std.log.info("hub slot {d}: hub descriptor read failed", .{device.slot_id});
return false;
}
device.is_hub = true;
device.hub_ports = descriptor[2]; // bNbrPorts
const characteristics = @as(u16, descriptor[3]) | (@as(u16, descriptor[4]) << 8);
device.hub_multi_tt = !is_usb3 and (characteristics & hub.characteristics_multi_tt != 0);
// A SuperSpeed hub needs its depth (tiers from the root) to compose the
// route strings of devices below it.
if (is_usb3) {
const depth = routeDepth(device.route);
_ = self.controlTransfer(device, hub.setHubDepth(depth), &.{}, false);
}
// Tell the controller the slot is a hub — Hub bit, Number of Ports, and
// (for a USB2 multi-TT hub) MTT + TT Think Time. Configure Endpoint with
// only the slot add-flag (A0) evaluates these (xHCI 4.6.6).
if (!self.configureSlotAsHub(device)) {
std.log.info("hub slot {d}: could not configure slot as a hub", .{device.slot_id});
return false;
}
// Power every downstream port.
var port: u16 = 1;
while (port <= device.hub_ports) : (port += 1) {
_ = self.controlTransfer(device, hub.setPortFeature(hub.feature_port_power, port), &.{}, false);
}
std.log.info("hub slot {d}: {d} downstream ports powered ({s})", .{
device.slot_id,
device.hub_ports,
if (is_usb3) "SuperSpeed" else if (device.hub_multi_tt) "USB2 multi-TT" else "USB2 single-TT",
});
return true;
}
/// Configure Endpoint with only A0 (slot) set: rebuild the slot context with
/// the Hub bit, Number of Ports, and MTT/TT-Think-Time, so the controller
/// treats this slot as a hub.
fn configureSlotAsHub(self: *Controller, device: *Device) bool {
const cs = self.context_size;
const base = device.input_context.virtual;
@memset(@as([*]u8, @ptrFromInt(base))[0 .. 2 * cs], 0);
contextDword(base, 0, 1, cs).* = 0b1; // Input Control Context add flags: A0 (slot)
// Slot Context dword 0: route, speed, context entries, plus Hub[26] and
// (USB2 multi-TT) MTT[25].
var dword0: u32 = (device.route & 0xFFFFF) | (device.speed << 20) | (@as(u32, 1) << 27) | (@as(u32, 1) << 26);
if (device.hub_multi_tt) dword0 |= (@as(u32, 1) << 25);
contextDword(base, 1, 0, cs).* = dword0;
// Slot Context dword 1: Root Hub Port Number[23:16], Number of Ports[31:24].
contextDword(base, 1, 1, cs).* = (device.root_port << 16) | (@as(u32, device.hub_ports) << 24);
// Slot Context dword 2: TT Think Time[17:16] = 0 (8 FS bit times); the
// parent-TT fields (if this hub is itself behind a hub) carry over.
if (device.parent_slot != 0 and device.speed < 3) {
contextDword(base, 1, 2, cs).* = @as(u32, device.parent_slot) | (@as(u32, device.parent_port) << 8);
}
return self.configureEndpointCommand(device);
}
fn abandon(self: *Controller, device: *Device) ?*Device {
_ = self;
device.used = false;