diff --git a/docs/usb-hub.md b/docs/usb-hub.md index fed770e..085cfdb 100644 --- a/docs/usb-hub.md +++ b/docs/usb-hub.md @@ -92,12 +92,20 @@ 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 +## Milestones (all complete) -- **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. +- **B4a** ✓ — hub recognition + setup: detect class 9 in the scan, read the hub + descriptor, configure the slot as a hub, power downstream ports, log the + topology. +- **B4b** ✓ — downstream enumeration: the in-process status-change subscription, + port reset, Address Device with route string + root port + TT fields, + enumerate + register. A full-speed keyboard behind a USB2 hub binds + `usb-hid-keyboard` in QEMU. +- **B4c** ✓ — disconnect teardown (recursive: a hub takes its subtree with it) + and hub-behind-hub recursion (route strings compose across tiers). QEMU's hub + *does* raise downstream status changes, so both connect and disconnect are + harness-tested (`usb-hub`, `usb-hub-nested`, `usb-hub-unplug`). + +Real-hardware validation of the user's SuperSpeed Genesys hub with full-speed +devices on its USB 2.0 companion remains pending — QEMU's USB 2.0 hub does not +model the compound USB 3.0 hub. diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index 63d532d..ca59841 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -242,11 +242,29 @@ fn bringUpPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device); } -/// Bring up a device on hub downstream `port`: setup+address (with route string -/// and TT), enumerate, register its interfaces, and — if it is itself a hub — -/// set it up (recursion). Mirrors bringUpPort for a root-port device. +// A compact topology-unique port key for a hub downstream port: 1000 + slot*100 +// + port. Stays a few digits (the id tag "PI" has an 8-byte cap) +// while never colliding with a root port (1..N) or another (hub, port). +fn hubPortKey(hub_slot: u8, port: u16) u32 { + return 1000 + @as(u32, hub_slot) * 100 + port; +} + +/// Service a change on hub downstream `port`: dispatch a connect (enumerate the +/// new device) or a disconnect (tear the old one down). Recurses for a hub +/// behind a hub — a nested hub is set up on connect and its downstream devices +/// torn down first on disconnect. fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hub: *library.Device, port: u16) void { - const usb_device = engine.serviceHubPort(hub, port) orelse return; // empty/disconnect/failure + const status = engine.hubPortStatusAck(hub, port) orelse return; + const connected = library.Controller.hubPortConnected(status); + const existing = engine.deviceOnHubPort(hub, port); + + if (!connected) { + if (existing) |dev| tearDownHubDevice(manager, engine, dev); + return; + } + if (existing != null) return; // already up + + const usb_device = engine.serviceHubPort(hub, port) orelse return; if (!engine.enumerate(usb_device)) { std.log.info("hub slot {d} port {d}: enumeration failed", .{ hub.slot_id, port }); return; @@ -258,17 +276,36 @@ fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hu usb_device.interface_count, }); for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| { - // A compact topology-unique port key: 1000 + slot*100 + port. Stays a few - // digits (the hid tag "PI" has an 8-byte cap) while never - // colliding with a root port (1..N) or another (hub, port). - const port_key = 1000 + @as(u32, hub.slot_id) * 100 + port; - if (reportInterface(manager, port_key, interface.*)) |registered| { + if (reportInterface(manager, hubPortKey(hub.slot_id, port), interface.*)) |registered| { interface.registered_device_id = registered; } } if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device); } +/// Tear down a device that disconnected from a hub: recursively tear down its +/// own downstream devices first if it is a hub, report each interface removed, +/// then Disable Slot. Mirrors tearDownPort for a hub-attached device. +fn tearDownHubDevice(manager: runtime.ipc.Handle, engine: *library.Controller, dev: *library.Device) void { + // A hub that left takes its whole subtree with it — tear children down first. + if (dev.is_hub) { + while (engine.nextChildOf(dev.slot_id, 0)) |child| tearDownHubDevice(manager, engine, child); + } + std.log.info("hub device slot {d} disconnected", .{dev.slot_id}); + const key = hubPortKey(dev.parent_slot, dev.parent_port); + for (dev.interfaces[0..dev.interface_count]) |*interface| { + if (interface.registered_device_id == 0) continue; + const event = protocol.ChildRemoved{ + .parent = controller_id, + .bus_address = (@as(u64, key) << 8) | interface.number, + }; + var reply: [protocol.message_maximum]u8 = undefined; + _ = runtime.ipc.call(manager, std.mem.asBytes(&event), &reply) catch {}; + interface.registered_device_id = 0; + } + engine.tearDownDevice(dev); +} + /// 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 { diff --git a/system/drivers/usb-xhci-bus/usb-xhci-library.zig b/system/drivers/usb-xhci-bus/usb-xhci-library.zig index ab00970..c88bc0f 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-library.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-library.zig @@ -947,19 +947,25 @@ pub const Controller = struct { /// reset the port, read the speed, and setup+address the downstream device /// (route string + TT). Returns the addressed device for the bus to enumerate /// and register, or null (empty port, disconnect, or a failure). - pub fn serviceHubPort(self: *Controller, hub: *Device, port: u16) ?*Device { + /// Read a downstream hub port's status and acknowledge its latched change + /// bits (so it can signal again). Returns the wPortStatus word. + pub fn hubPortStatusAck(self: *Controller, hub: *Device, port: u16) ?u32 { const status = self.readHubPortStatus(hub, port) orelse return null; - const connected = status & hubreq.status_connection != 0; - - // Acknowledge the latched change bits so the port can signal again. _ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_connection, port), &.{}, false); _ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_reset, port), &.{}, false); + return status; + } - if (!connected) { - // Disconnect (or an empty port) — teardown is B4c. - return null; - } - if (self.deviceOnHubPort(hub, port) != null) return null; // already up + pub fn hubPortConnected(status: u32) bool { + return status & hubreq.status_connection != 0; + } + + /// Reset + address a device on a connected, empty downstream hub `port` (the + /// bus has confirmed connect and no existing device): reset the port, read + /// the speed, and setup+address the downstream device (route string + TT). + /// Returns the addressed device for the bus to enumerate + register. + pub fn serviceHubPort(self: *Controller, hub: *Device, port: u16) ?*Device { + const status = self.readHubPortStatus(hub, port) orelse return null; // Reset the port if not yet enabled, then wait (bounded) for enable. if (status & hubreq.status_enable == 0) { @@ -982,7 +988,7 @@ pub const Controller = struct { return self.setupDeviceBehindHub(hub, port, downstream_speed); } - fn deviceOnHubPort(self: *Controller, hub: *Device, port: u16) ?*Device { + pub fn deviceOnHubPort(self: *Controller, hub: *Device, port: u16) ?*Device { for (&self.devices) |*device| { if (device.used and device.parent_slot == hub.slot_id and device.parent_port == port) return device; } @@ -1511,6 +1517,15 @@ pub const Controller = struct { return null; } + /// The next used device whose parent hub is `hub_slot` and slot id > `after` + /// (for recursive teardown when a hub itself disconnects), or null. + pub fn nextChildOf(self: *Controller, hub_slot: u8, after: u8) ?*Device { + for (&self.devices) |*device| { + if (device.used and device.parent_slot == hub_slot and device.slot_id > after) return device; + } + return null; + } + /// Tear a device down after unplug: cancel its interrupt subscriptions, /// Disable Slot (frees the controller's slot state), clear its context-array /// entry, and release the tracking slot. DMA regions leak (as elsewhere) — diff --git a/test/qemu_test.py b/test/qemu_test.py index fbd7e9b..66308ba 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -651,6 +651,34 @@ CASES = [ # cannot exercise the path. The hot-plug code — port-change queue, teardown # via Disable Slot, ChildRemoved reporting — is validated on real hardware, # flagged for the user. The qmp_sequence harness support it added remains.) + # Hub-behind-hub (B4c): route strings compose across tiers — a keyboard two + # hubs deep enumerates and binds. Static nested topology on a 2nd controller. + {"name": "usb-hub-nested", + "build_case": "usb-hid", + "smp": 4, + "timeout": 150, + "qemu_extra": ["-device", "qemu-xhci,id=xhci2", + "-device", "usb-hub,bus=xhci2.0,port=1", + "-device", "usb-hub,bus=xhci2.0,port=1.1", + "-device", "usb-kbd,bus=xhci2.0,port=1.1.1"], + "expect": r"(?s)(?=.*hub slot \d+ port \d+ device vendor 0x0409)" + r"(?=.*usb-hid-keyboard: ok \(device 3)", + "fail": r"DANOS-TEST-RESULT: FAIL"}, + # Hub-downstream disconnect (B4c): device_del the keyboard behind the hub; + # the hub's status-change endpoint reports it, the device is torn down + # (ChildRemoved + Disable Slot). QEMU's hub DOES raise downstream changes + # (unlike root-port hot-plug). + {"name": "usb-hub-unplug", + "build_case": "usb-hid", + "smp": 4, + "timeout": 150, + "qemu_extra": ["-device", "qemu-xhci,id=xhci2", + "-device", "usb-hub,bus=xhci2.0,port=1", + "-device", "usb-kbd,bus=xhci2.0,port=1.1,id=dkbd"], + "qmp_after": {"delay": 8, "command": "device_del", "arguments": {"id": "dkbd"}}, + "expect": r"(?s)(?=.*usb-hid-keyboard: ok \(device 3)" + r"(?=.*slot \d+ disconnected)", + "fail": r"DANOS-TEST-RESULT: FAIL"}, # The kernel VFS root (M-F): the mount table serves the initrd at /system — # path resolution, node status/read (an ELF magic), and directory listing, # asserted kernel-side.