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:
parent
983b4ed05a
commit
92b03b8d07
|
|
@ -0,0 +1,103 @@
|
|||
# USB hubs (M22)
|
||||
|
||||
A hub is USB **bus infrastructure**, not an application peripheral, so hub
|
||||
topology is handled **inside the `usb-xhci-bus` driver** — the process that owns
|
||||
the controller's device slots and contexts. A device behind a hub is not reached
|
||||
by any hub-specific software path: it is reached by the **controller**,
|
||||
programmed with a *route string* in its slot context. Route strings and slot
|
||||
contexts are xHCI hardware concepts that only exist inside the controller driver,
|
||||
so that is where hub handling belongs. Class drivers (HID, storage) stay separate
|
||||
and unaware — the hub is transparent to them; a keyboard behind a hub reaches the
|
||||
same `usb-hid-keyboard` driver as one on a root port.
|
||||
|
||||
This is a deliberate scoping choice, not a microkernel compromise: the USB *bus*
|
||||
driver handles USB *bus* topology. The alternative — a separate `usb-hub`
|
||||
class-driver process plus a cross-process enumeration protocol — would only
|
||||
shuttle the bus's own topology state (slot ids, route strings, TT linkage) out to
|
||||
another process and back, since the hub driver cannot build a slot context
|
||||
itself.
|
||||
|
||||
## The compound-hub reality
|
||||
|
||||
A USB 3.0 hub is physically **two hubs** sharing each connector: a SuperSpeed hub
|
||||
and a USB 2.0 companion hub, enumerated as **separate devices on separate root
|
||||
ports**. A full- or low-speed device plugged into a USB 3.0 hub attaches to the
|
||||
**USB 2.0 companion**, not the SuperSpeed hub. So supporting full-speed devices
|
||||
(keyboards, mice) behind a hub means driving the USB 2.0 companion and handling
|
||||
**transaction translators** — there is no SuperSpeed-only shortcut that reaches a
|
||||
full-speed keyboard.
|
||||
|
||||
## Slot-context fields for a downstream device
|
||||
|
||||
`buildAddressInputContext` fills the Slot Context. Today it hardcodes route 0 and
|
||||
the root-hub port. A downstream device additionally needs:
|
||||
|
||||
- **Route String** (Slot Context dword 0, bits 19:0) — 5 tiers × 4 bits, each
|
||||
tier the downstream hub-port number. Composed as
|
||||
`route = (parent_route << 4) | hub_port`, capped at the xHCI 5-tier max.
|
||||
- **Root Hub Port Number** (dword 1, bits 23:16) — the *root* port the whole hub
|
||||
chain hangs off, inherited from the parent hub (not the hub's own port number).
|
||||
- **Speed** (dword 0, bits 23:20) — read from the hub's downstream port status
|
||||
after reset, not assumed.
|
||||
- **Parent Hub Slot ID** (dword 2, bits 7:0) + **Parent Port Number** (dword 2,
|
||||
bits 13:8) — the **transaction translator**: set when a full/low-speed device
|
||||
sits behind a high-speed hub, so the controller routes split transactions
|
||||
through that hub's TT. For a multi-TT hub, **MTT** (Slot Context dword 0 bit
|
||||
25) is set and the TT port is the device's own hub port.
|
||||
|
||||
## Detection: the status-change interrupt endpoint
|
||||
|
||||
A hub has one interrupt IN endpoint that returns a **port-status-change bitmap**
|
||||
(bit N set = port N changed). The bus arms an interrupt transfer on it (reusing
|
||||
the controller's existing interrupt-endpoint machinery, but serviced
|
||||
**in-process** — no class-driver subscription IPC), and on each report:
|
||||
|
||||
1. For each changed port, `GET_STATUS` (hub class request) reads the port's
|
||||
connect/enable/reset state and speed, and `CLEAR_FEATURE(C_PORT_*)`
|
||||
acknowledges the change.
|
||||
2. On a **connect**: `SET_FEATURE(PORT_RESET)`, wait for reset-complete via a
|
||||
later status-change report, read the enabled speed, then `setupDevice` with
|
||||
the composed route string / root port / TT fields, `enumerate`, and register
|
||||
the interfaces — exactly the existing path, recursing if the new device is
|
||||
itself a hub.
|
||||
3. On a **disconnect**: tear down the downstream device (report each interface
|
||||
`ChildRemoved`, Disable Slot) — the B3 teardown path, keyed by the device's
|
||||
route rather than a root port.
|
||||
|
||||
## Hub setup (once, when the hub enumerates)
|
||||
|
||||
When the bus scan (or a hot-plug bring-up) finds a device of class 9:
|
||||
|
||||
1. Read the **hub descriptor** (class GET_DESCRIPTOR, type 0x2A for a USB 3.0
|
||||
hub / 0x29 for USB 2.0) → downstream port count, characteristics.
|
||||
2. For a USB 3.0 hub, `SET_FEATURE(BH_PORT_RESET)` semantics and the depth
|
||||
(`SET_HUB_DEPTH`) so the hub knows its tier for route-string forwarding.
|
||||
3. `SET_FEATURE(PORT_POWER)` each downstream port.
|
||||
4. Configure the hub's slot as a hub: **Hub** bit (Slot Context dword 0 bit 26),
|
||||
**Number of Ports** (dword 1, bits 31:24), **TT Think Time** and **MTT** for a
|
||||
USB 2.0 multi-TT hub — via an Evaluate/Configure Endpoint on the hub's slot.
|
||||
5. Arm the status-change interrupt endpoint.
|
||||
|
||||
## Testing
|
||||
|
||||
QEMU's `usb-hub` is a USB 2.0 single-TT hub. A **static boot topology**
|
||||
(`-device usb-hub,bus=xhci.0,id=h -device usb-kbd,bus=h.0`) presents the
|
||||
downstream device connected from the start, so the bus reads it on the first
|
||||
status-change report — exercising the full path (hub setup, TT slot context,
|
||||
downstream enumerate, class-driver bind) without needing a hot-plug event. A new
|
||||
`usb-hub` QEMU case asserts the hub enumerates, the downstream keyboard
|
||||
enumerates behind it, and `usb-hid-keyboard` binds.
|
||||
|
||||
Real-hardware validation (the user's SuperSpeed Genesys hub + full-speed
|
||||
keyboard/mouse on its USB 2.0 companion) is flagged separately — the compound
|
||||
USB 3.0 hub path is not modelled by QEMU's USB 2.0 hub.
|
||||
|
||||
## Milestones
|
||||
|
||||
- **B4a** — hub recognition + setup: detect class 9 in the scan, read the hub
|
||||
descriptor, mark the slot a hub, power downstream ports, arm the status-change
|
||||
endpoint, log the topology. No downstream enumeration yet.
|
||||
- **B4b** — downstream enumeration: status-change handling, port reset,
|
||||
`setupDevice` with route string + root port + TT fields, enumerate + register.
|
||||
A full-speed device behind the hub reaches its class driver.
|
||||
- **B4c** — disconnect teardown + hub-behind-hub recursion + polish.
|
||||
|
|
@ -235,6 +235,21 @@ fn bringUpPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u
|
|||
interface.registered_device_id = registered;
|
||||
}
|
||||
}
|
||||
|
||||
// A hub (class 9) is bus infrastructure the bus drives itself: configure it
|
||||
// and power its downstream ports (docs/usb-hub.md). Its interfaces are still
|
||||
// reported above, but no external class driver binds it.
|
||||
if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
|
||||
}
|
||||
|
||||
/// Whether an enumerated device is a hub — class 9 at the device or the
|
||||
/// interface level (a hub's single interface is class 9/0/0).
|
||||
fn deviceIsHub(usb_device: *const library.Device) bool {
|
||||
if (usb_device.device_descriptor.device_class == @intFromEnum(usb_ids.Class.hub)) return true;
|
||||
for (usb_device.interfaces[0..usb_device.interface_count]) |interface| {
|
||||
if (interface.class == @intFromEnum(usb_ids.Class.hub)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Tear down whatever was on `port` after an unplug: report each registered
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -461,6 +461,23 @@ CASES = [
|
|||
# usb-kbd/usb-mouse ride the default boot xHCI bus (see qemu_args).
|
||||
"expect": r"(?=[\s\S]*usb-hid-keyboard: ok)(?=[\s\S]*usb-hid-mouse: ok)",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# USB hub (B4a/B4b, docs/usb-hub.md): a USB2 hub on the xHCI bus with a
|
||||
# keyboard behind it (a STATIC boot topology — no hot-plug event needed).
|
||||
# B4a: the hub enumerates and powers its downstream ports. B4b extends the
|
||||
# expect to the downstream keyboard binding usb-hid-keyboard.
|
||||
{"name": "usb-hub",
|
||||
"build_case": "usb-hid",
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
# A SECOND xhci controller carries the hub topology, isolated from the boot
|
||||
# controller's auto-assigned devices (whose ports the hub would collide
|
||||
# with). danos spawns a second usb-xhci-bus for it. The hub sits on port 1,
|
||||
# the keyboard on the hub's downstream port 1 (port=1.1).
|
||||
"qemu_extra": ["-device", "qemu-xhci,id=xhci2",
|
||||
"-device", "usb-hub,bus=xhci2.0,port=1",
|
||||
"-device", "usb-kbd,bus=xhci2.0,port=1.1"],
|
||||
"expect": r"hub slot \d+: \d+ downstream ports powered",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# USB mass storage end to end: the boot usb-storage device (the FAT32 image,
|
||||
# which has a real 0x55AA boot sector) is enough — the manager spawns
|
||||
# usb-storage, which opens the device, runs the Bulk-Only / SCSI bring-up,
|
||||
|
|
|
|||
Loading…
Reference in New Issue