usb: hub-downstream disconnect teardown + hub-behind-hub recursion (B4c)
Completes hub support (docs/usb-hub.md). On the bus tick, a downstream hub-port change now dispatches: a connect enumerates the new device (B4b), a disconnect tears the old one down — recursively, since a hub that leaves takes its whole subtree with it (children first), reporting each interface ChildRemoved and Disable-Slotting the device. Route strings compose across tiers, so a device two hubs deep enumerates with a two-tier route. QEMU's hub — unlike root-port hot-plug — DOES raise downstream status-change events, so both paths are harness-tested: usb-hub (a keyboard behind a hub binds usb-hid-keyboard), usb-hub-nested (a keyboard two hubs deep), usb-hub-unplug (device_del behind the hub tears it down). Full suite 91/91. Real-hardware validation of the user's SuperSpeed Genesys hub with full-speed devices on its USB2 companion is flagged for the user (QEMU's USB2 hub does not model the compound USB3 hub).
This commit is contained in:
+16
-8
@@ -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
|
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.
|
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
|
- **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
|
descriptor, configure the slot as a hub, power downstream ports, log the
|
||||||
endpoint, log the topology. No downstream enumeration yet.
|
topology.
|
||||||
- **B4b** — downstream enumeration: status-change handling, port reset,
|
- **B4b** ✓ — downstream enumeration: the in-process status-change subscription,
|
||||||
`setupDevice` with route string + root port + TT fields, enumerate + register.
|
port reset, Address Device with route string + root port + TT fields,
|
||||||
A full-speed device behind the hub reaches its class driver.
|
enumerate + register. A full-speed keyboard behind a USB2 hub binds
|
||||||
- **B4c** — disconnect teardown + hub-behind-hub recursion + polish.
|
`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.
|
||||||
|
|||||||
@@ -242,11 +242,29 @@ fn bringUpPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u
|
|||||||
if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
|
if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bring up a device on hub downstream `port`: setup+address (with route string
|
// A compact topology-unique port key for a hub downstream port: 1000 + slot*100
|
||||||
/// and TT), enumerate, register its interfaces, and — if it is itself a hub —
|
// + port. Stays a few digits (the id tag "P<key>I<iface>" has an 8-byte cap)
|
||||||
/// set it up (recursion). Mirrors bringUpPort for a root-port device.
|
// 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 {
|
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)) {
|
if (!engine.enumerate(usb_device)) {
|
||||||
std.log.info("hub slot {d} port {d}: enumeration failed", .{ hub.slot_id, port });
|
std.log.info("hub slot {d} port {d}: enumeration failed", .{ hub.slot_id, port });
|
||||||
return;
|
return;
|
||||||
@@ -258,17 +276,36 @@ fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hu
|
|||||||
usb_device.interface_count,
|
usb_device.interface_count,
|
||||||
});
|
});
|
||||||
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
|
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
|
||||||
// A compact topology-unique port key: 1000 + slot*100 + port. Stays a few
|
if (reportInterface(manager, hubPortKey(hub.slot_id, port), interface.*)) |registered| {
|
||||||
// digits (the hid tag "P<key>I<iface>" 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| {
|
|
||||||
interface.registered_device_id = registered;
|
interface.registered_device_id = registered;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
|
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
|
/// 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).
|
/// interface level (a hub's single interface is class 9/0/0).
|
||||||
fn deviceIsHub(usb_device: *const library.Device) bool {
|
fn deviceIsHub(usb_device: *const library.Device) bool {
|
||||||
|
|||||||
@@ -947,19 +947,25 @@ pub const Controller = struct {
|
|||||||
/// reset the port, read the speed, and setup+address the downstream 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
|
/// (route string + TT). Returns the addressed device for the bus to enumerate
|
||||||
/// and register, or null (empty port, disconnect, or a failure).
|
/// 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 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_connection, port), &.{}, false);
|
||||||
_ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_reset, port), &.{}, false);
|
_ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_reset, port), &.{}, false);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
if (!connected) {
|
pub fn hubPortConnected(status: u32) bool {
|
||||||
// Disconnect (or an empty port) — teardown is B4c.
|
return status & hubreq.status_connection != 0;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
if (self.deviceOnHubPort(hub, port) != null) return null; // already up
|
/// 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.
|
// Reset the port if not yet enabled, then wait (bounded) for enable.
|
||||||
if (status & hubreq.status_enable == 0) {
|
if (status & hubreq.status_enable == 0) {
|
||||||
@@ -982,7 +988,7 @@ pub const Controller = struct {
|
|||||||
return self.setupDeviceBehindHub(hub, port, downstream_speed);
|
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| {
|
for (&self.devices) |*device| {
|
||||||
if (device.used and device.parent_slot == hub.slot_id and device.parent_port == port) return 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;
|
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,
|
/// Tear a device down after unplug: cancel its interrupt subscriptions,
|
||||||
/// Disable Slot (frees the controller's slot state), clear its context-array
|
/// Disable Slot (frees the controller's slot state), clear its context-array
|
||||||
/// entry, and release the tracking slot. DMA regions leak (as elsewhere) —
|
/// entry, and release the tracking slot. DMA regions leak (as elsewhere) —
|
||||||
|
|||||||
@@ -651,6 +651,34 @@ CASES = [
|
|||||||
# cannot exercise the path. The hot-plug code — port-change queue, teardown
|
# cannot exercise the path. The hot-plug code — port-change queue, teardown
|
||||||
# via Disable Slot, ChildRemoved reporting — is validated on real hardware,
|
# via Disable Slot, ChildRemoved reporting — is validated on real hardware,
|
||||||
# flagged for the user. The qmp_sequence harness support it added remains.)
|
# 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 —
|
# 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,
|
# path resolution, node status/read (an ELF magic), and directory listing,
|
||||||
# asserted kernel-side.
|
# asserted kernel-side.
|
||||||
|
|||||||
Reference in New Issue
Block a user