usb: enumerate devices behind a hub — route strings + transaction translators (B4b)
The core of hub support (docs/usb-hub.md): a device on a hub's downstream port now reaches its class driver exactly like one on a root port. The hub's interrupt status-change endpoint is armed with an in-process subscription — completions set the hub's pending-port mask instead of queuing a class-driver report — and setupHub seeds every downstream port pending so a STATIC topology (a device present at power-on) enumerates without waiting on the initial interrupt edge. On the bus tick, each pending hub port is serviced: GET_STATUS + clear the change bits, and on a connect reset the port, read the speed, then enable a slot and Address Device with the composed route string ((parent_route<<4)|port), the inherited root port, and the parent hub's slot/port as the TRANSACTION TRANSLATOR — so the controller routes a full/low-speed device's split transactions through the hub's TT. The device then enumerates and registers its interfaces through the normal path (a compact topology-unique port key keeps the id tag within its 8-byte cap), recursing setupHub if it is itself a hub. Verified in QEMU (a keyboard behind a USB2 hub on a second controller): 'hub slot 1 port 1 device vendor 0x0627 ... usb-hid-keyboard: ok (device 35)'. Full suite 89/89. The user's SuperSpeed Genesys hub + full-speed keyboard/mouse on its USB2 companion is the real-hardware target, flagged separately.
This commit is contained in:
@@ -242,6 +242,33 @@ 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.
|
||||
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
|
||||
if (!engine.enumerate(usb_device)) {
|
||||
std.log.info("hub slot {d} port {d}: enumeration failed", .{ hub.slot_id, port });
|
||||
return;
|
||||
}
|
||||
std.log.info("hub slot {d} port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)", .{
|
||||
hub.slot_id, port,
|
||||
usb_device.device_descriptor.vendor_id,
|
||||
usb_device.device_descriptor.product_id,
|
||||
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 "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;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
@@ -434,6 +461,13 @@ fn onNotification(badge: u64) void {
|
||||
tearDownPort(manager, engine, port);
|
||||
}
|
||||
}
|
||||
// Downstream hub-port changes (docs/usb-hub.md): a device connected on a
|
||||
// hub's downstream port is enumerated and registered here, so a keyboard
|
||||
// behind a hub reaches its class driver like one on a root port.
|
||||
while (engine.takeHubChange()) |change| {
|
||||
const manager = manager_handle orelse break;
|
||||
bringUpBehindHub(manager, engine, change.hub, change.port);
|
||||
}
|
||||
while (engine.takeReport()) |report| {
|
||||
var message = transfer.InterruptReport{
|
||||
.device_token = report.device_token,
|
||||
|
||||
@@ -292,6 +292,9 @@ pub const Device = struct {
|
||||
is_hub: bool = false,
|
||||
hub_ports: u8 = 0, // downstream port count from the hub descriptor
|
||||
hub_multi_tt: bool = false,
|
||||
// Downstream ports with a pending change to service (bit P = port P), set
|
||||
// by the status-change endpoint (and by an initial sweep in setupHub).
|
||||
hub_change_mask: u32 = 0,
|
||||
};
|
||||
|
||||
// A standing interrupt-IN subscription: the endpoint's ring is kept armed with a
|
||||
@@ -310,6 +313,9 @@ const Subscription = struct {
|
||||
// device token and the endpoint handle its reports are sent to.
|
||||
device_token: u64 = 0,
|
||||
report_endpoint: usize = 0,
|
||||
// When set, this is an IN-PROCESS hub status-change subscription: completions
|
||||
// set the hub's pending-change mask instead of queuing a class-driver report.
|
||||
hub: ?*Device = null,
|
||||
};
|
||||
|
||||
// One interrupt report waiting for the bus layer to push it to a subscriber.
|
||||
@@ -333,6 +339,14 @@ const report_queue_capacity = 16;
|
||||
// route strings and slot contexts a downstream device needs only exist here.
|
||||
// These are the class-specific control requests to a hub device.
|
||||
|
||||
// A USB2 hub port's wPortStatus speed bits (bit 9 = low-speed, bit 10 =
|
||||
// high-speed; neither = full-speed) mapped to the xHCI speed id.
|
||||
fn mapHubPortSpeed(port_speed_bits: u32) u32 {
|
||||
if (port_speed_bits & 0x1 != 0) return 2; // low-speed (wPortStatus bit 9)
|
||||
if (port_speed_bits & 0x2 != 0) return 3; // high-speed
|
||||
return 1; // full-speed
|
||||
}
|
||||
|
||||
fn routeDepth(route: u32) u16 {
|
||||
// Tiers used by a route string: each nonzero 4-bit nibble is one tier.
|
||||
var depth: u16 = 0;
|
||||
@@ -343,7 +357,7 @@ fn routeDepth(route: u32) u16 {
|
||||
return depth;
|
||||
}
|
||||
|
||||
const hub = struct {
|
||||
const hubreq = struct {
|
||||
// Hub descriptor types (GET_DESCRIPTOR value high byte).
|
||||
const descriptor_usb2: u8 = 0x29;
|
||||
const descriptor_usb3: u8 = 0x2A;
|
||||
@@ -832,21 +846,21 @@ pub const Controller = struct {
|
||||
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)) {
|
||||
const kind: u8 = if (is_usb3) hubreq.descriptor_usb3 else hubreq.descriptor_usb2;
|
||||
if (!self.controlTransfer(device, hubreq.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);
|
||||
device.hub_multi_tt = !is_usb3 and (characteristics & hubreq.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);
|
||||
_ = self.controlTransfer(device, hubreq.setHubDepth(depth), &.{}, false);
|
||||
}
|
||||
|
||||
// Tell the controller the slot is a hub — Hub bit, Number of Ports, and
|
||||
@@ -860,8 +874,17 @@ pub const Controller = struct {
|
||||
// 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);
|
||||
_ = self.controlTransfer(device, hubreq.setPortFeature(hubreq.feature_port_power, port), &.{}, false);
|
||||
}
|
||||
// Seed every downstream port as pending: the bus tick GET_STATUSes each
|
||||
// and enumerates the connected ones. This makes a STATIC topology (a
|
||||
// device present at power-on) work without relying on the initial
|
||||
// status-change interrupt edge; the interrupt then handles later plugs.
|
||||
device.hub_change_mask = if (device.hub_ports >= 31) 0xFFFF_FFFE else (@as(u32, 1) << @intCast(device.hub_ports + 1)) - 2;
|
||||
|
||||
// Arm the status-change interrupt endpoint (in-process) for hot-plug.
|
||||
self.armHubStatus(device);
|
||||
|
||||
std.log.info("hub slot {d}: {d} downstream ports powered ({s})", .{
|
||||
device.slot_id,
|
||||
device.hub_ports,
|
||||
@@ -870,6 +893,137 @@ pub const Controller = struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Arm the hub's interrupt-IN status-change endpoint with an in-process
|
||||
/// subscription: completions set the hub's pending-change mask (serviced on
|
||||
/// the bus tick). Best-effort — a hub with no interrupt endpoint (shouldn't
|
||||
/// happen) just relies on the initial sweep.
|
||||
fn armHubStatus(self: *Controller, device: *Device) void {
|
||||
for (device.interfaces[0..device.interface_count]) |interface| {
|
||||
for (interface.endpoints[0..interface.endpoint_count]) |endpoint| {
|
||||
const is_interrupt = endpoint.transfer_type == 3;
|
||||
const is_in = endpoint.address & 0x80 != 0;
|
||||
if (!is_interrupt or !is_in) continue;
|
||||
const ring = self.getOrConfigureEndpoint(device, endpoint) orelse return;
|
||||
const subscription = self.allocateSubscription() orelse return;
|
||||
const buffer = dma.alloc(page_size, dma.coherent) orelse return;
|
||||
const number: u8 = endpoint.address & 0x0F;
|
||||
subscription.* = .{
|
||||
.active = true,
|
||||
.slot_id = device.slot_id,
|
||||
.dci = doorbellContextIndex(number, true),
|
||||
.endpoint_address = endpoint.address,
|
||||
.ring = ring,
|
||||
.buffer = buffer,
|
||||
.max_length = endpoint.max_packet_size,
|
||||
.hub = device,
|
||||
};
|
||||
self.armInterrupt(subscription);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The next pending (hub, downstream-port) change to service, or null. Clears
|
||||
/// the returned port's bit. Called on the bus tick.
|
||||
pub fn takeHubChange(self: *Controller) ?struct { hub: *Device, port: u16 } {
|
||||
for (&self.devices) |*device| {
|
||||
if (!device.used or !device.is_hub or device.hub_change_mask == 0) continue;
|
||||
const bit: u5 = @intCast(@ctz(device.hub_change_mask));
|
||||
device.hub_change_mask &= ~(@as(u32, 1) << bit);
|
||||
if (bit == 0) continue; // bit 0 is the hub itself, not a downstream port
|
||||
return .{ .hub = device, .port = bit };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn readHubPortStatus(self: *Controller, hub: *Device, port: u16) ?u32 {
|
||||
var buffer: [4]u8 = undefined;
|
||||
if (!self.controlTransfer(hub, hubreq.getPortStatus(port), buffer[0..], true)) return null;
|
||||
return @as(u32, buffer[0]) | (@as(u32, buffer[1]) << 8) | (@as(u32, buffer[2]) << 16) | (@as(u32, buffer[3]) << 24);
|
||||
}
|
||||
|
||||
/// Bring up (or note the disconnect of) a device on hub downstream `port`:
|
||||
/// read the port status, acknowledge the change bits, and on a fresh connect
|
||||
/// 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 {
|
||||
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);
|
||||
|
||||
if (!connected) {
|
||||
// Disconnect (or an empty port) — teardown is B4c.
|
||||
return null;
|
||||
}
|
||||
if (self.deviceOnHubPort(hub, port) != null) return null; // already up
|
||||
|
||||
// Reset the port if not yet enabled, then wait (bounded) for enable.
|
||||
if (status & hubreq.status_enable == 0) {
|
||||
_ = self.controlTransfer(hub, hubreq.setPortFeature(hubreq.feature_port_reset, port), &.{}, false);
|
||||
var tries: u32 = 0;
|
||||
while (tries < 200) : (tries += 1) {
|
||||
system.sleep(5);
|
||||
const s = self.readHubPortStatus(hub, port) orelse return null;
|
||||
if (s & hubreq.status_enable != 0) break;
|
||||
}
|
||||
_ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_reset, port), &.{}, false);
|
||||
}
|
||||
const enabled = self.readHubPortStatus(hub, port) orelse return null;
|
||||
if (enabled & hubreq.status_enable == 0) {
|
||||
std.log.info("hub slot {d} port {d}: reset did not enable", .{ hub.slot_id, port });
|
||||
return null;
|
||||
}
|
||||
const downstream_speed = (enabled >> 9) & 0x3; // wPortStatus: bit9 low-speed, bit10 high-speed
|
||||
|
||||
return self.setupDeviceBehindHub(hub, port, downstream_speed);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Enable a slot and Address a device behind `hub` on downstream `port`, with
|
||||
/// the composed route string, inherited root port, and TT fields (so the
|
||||
/// controller routes split transactions through this hub's TT for a
|
||||
/// full/low-speed device). Mirrors setupDevice for a root-port device.
|
||||
fn setupDeviceBehindHub(self: *Controller, hub: *Device, port: u16, speed: u32) ?*Device {
|
||||
const slot_id = self.enableSlot() orelse {
|
||||
std.log.info("hub slot {d} port {d}: Enable Slot failed", .{ hub.slot_id, port });
|
||||
return null;
|
||||
};
|
||||
const device = self.allocateDevice() orelse return null;
|
||||
const child_speed = mapHubPortSpeed(speed);
|
||||
device.* = .{
|
||||
.used = true,
|
||||
.slot_id = slot_id,
|
||||
.port = hub.root_port,
|
||||
.speed = child_speed,
|
||||
.max_packet_size_0 = defaultMaxPacketSize0(child_speed),
|
||||
.route = (hub.route << 4) | (port & 0xF),
|
||||
.root_port = hub.root_port,
|
||||
.parent_slot = hub.slot_id,
|
||||
.parent_port = @intCast(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);
|
||||
device.ep0_ring = .{ .region = dma.alloc(page_size, dma.coherent) orelse return self.abandon(device) };
|
||||
device.ep0_ring.installLink();
|
||||
device.control_buffer = dma.alloc(page_size, dma.coherent) orelse return self.abandon(device);
|
||||
|
||||
self.buildAddressInputContext(device);
|
||||
const array: [*]volatile u64 = @ptrFromInt(self.device_context_array.virtual);
|
||||
array[device.slot_id] = device.device_context.physical;
|
||||
if (!self.addressDeviceCommand(device)) return self.abandon(device);
|
||||
return device;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -1264,9 +1418,20 @@ pub const Controller = struct {
|
||||
if (!subscription.active or subscription.armed_trb_physical != trb_pointer) continue;
|
||||
const code = completionCode(event.status);
|
||||
if (code == @intFromEnum(CompletionCode.success) or code == @intFromEnum(CompletionCode.short_packet)) {
|
||||
const residual = event.status & 0xFFFFFF;
|
||||
const transferred: u16 = if (residual >= subscription.max_length) 0 else @intCast(subscription.max_length - residual);
|
||||
self.enqueueReport(subscription, transferred);
|
||||
if (subscription.hub) |hub_device| {
|
||||
// Hub status-change report: OR the changed-port bitmap into
|
||||
// the hub's pending mask (bit 0 = the hub itself, ignored;
|
||||
// bit P = downstream port P). The control transfers to
|
||||
// service it run on the bus tick, not here.
|
||||
const bytes: [*]const u8 = @ptrFromInt(subscription.buffer.virtual);
|
||||
var i: usize = 0;
|
||||
while (i < subscription.max_length and i < 4) : (i += 1)
|
||||
hub_device.hub_change_mask |= @as(u32, bytes[i]) << @intCast(i * 8);
|
||||
} else {
|
||||
const residual = event.status & 0xFFFFFF;
|
||||
const transferred: u16 = if (residual >= subscription.max_length) 0 else @intCast(subscription.max_length - residual);
|
||||
self.enqueueReport(subscription, transferred);
|
||||
}
|
||||
}
|
||||
self.armInterrupt(subscription); // keep polling
|
||||
return true;
|
||||
|
||||
+5
-1
@@ -476,7 +476,11 @@ CASES = [
|
||||
"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",
|
||||
# B4a: the hub powers its ports. B4b: the keyboard behind it enumerates
|
||||
# (route string + TT) and binds usb-hid-keyboard.
|
||||
"expect": r"(?s)(?=.*hub slot \d+: \d+ downstream ports powered)"
|
||||
r"(?=.*hub slot \d+ port \d+ device vendor 0x0627)"
|
||||
r"(?=.*usb-hid-keyboard: ok \(device 3)",
|
||||
"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
|
||||
|
||||
Reference in New Issue
Block a user