From 92b03b8d07cc6929a08a8fa3d18263d3c26407de Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:56:59 +0100 Subject: [PATCH] usb: recognize and configure hubs, power downstream ports (B4a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- docs/usb-hub.md | 103 +++++++++ system/drivers/usb-xhci-bus/usb-xhci-bus.zig | 15 ++ .../drivers/usb-xhci-bus/usb-xhci-library.zig | 195 +++++++++++++++++- test/qemu_test.py | 17 ++ 4 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 docs/usb-hub.md diff --git a/docs/usb-hub.md b/docs/usb-hub.md new file mode 100644 index 0000000..fed770e --- /dev/null +++ b/docs/usb-hub.md @@ -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. diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index f8dd08d..3ce0485 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -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 diff --git a/system/drivers/usb-xhci-bus/usb-xhci-library.zig b/system/drivers/usb-xhci-bus/usb-xhci-library.zig index 544e766..1ebe594 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-library.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-library.zig @@ -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; diff --git a/test/qemu_test.py b/test/qemu_test.py index 34b262e..7d7b66c 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -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,