diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index 3ce0485..63d532d 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -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 "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| { + 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, diff --git a/system/drivers/usb-xhci-bus/usb-xhci-library.zig b/system/drivers/usb-xhci-bus/usb-xhci-library.zig index 1ebe594..ab00970 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-library.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-library.zig @@ -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; diff --git a/test/qemu_test.py b/test/qemu_test.py index 7d7b66c..fbd7e9b 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -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