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:
Daniel Samson
2026-07-21 22:16:42 +01:00
parent 9da1a9899c
commit e52ae242fc
4 changed files with 115 additions and 27 deletions
@@ -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) —