Merge usb-superspeed-hub: hubs work on real hardware (M22)
USB hub support, root-caused and validated on the user's real machine: a Keychron keyboard and ROG mouse now enumerate through a Genesys compound USB3 hub (its USB2 companion + transaction translators) and type on danos. Fixes found by reading logs off the stick, each invisible to QEMU: - SuperSpeed hubs have change bits a USB2 hub lacks; not clearing them spun the driver. Clear them (gated on speed) + a per-tick service cap. - The boot scan runs ~3 ms after the controller reset, too early for a USB2 connection to debounce; the SuperSpeed devices train instantly and hid the problem. Poll the root ports on the tick (edge-triggered). - Full-speed devices failed Address Device with a USB Transaction Error: USB 2.0 requires a 10 ms reset-recovery interval before SET_ADDRESS. Wait it out, and retry Address Device on a transaction error. Plus richer logs: readable PORTSC decode, USB link-state and speed names, device string descriptors (maker/product), and a readable name for every class/subclass/protocol enum in usb-ids. Full suite 92/92. Open follow-up: 'port 11 enumeration failed' — a third device that did not come up; to be investigated separately.
This commit is contained in:
@@ -70,6 +70,104 @@ pub const Class = enum(u8) {
|
||||
_,
|
||||
};
|
||||
|
||||
/// A human-readable name for a device/interface class code, for logs. Unknown
|
||||
/// codes fall through to "class 0xNN".
|
||||
pub fn className(class: u8) []const u8 {
|
||||
return switch (@as(Class, @enumFromInt(class))) {
|
||||
.per_interface => "per-interface",
|
||||
.audio => "Audio",
|
||||
.communications => "Communications",
|
||||
.hid => "HID",
|
||||
.physical => "Physical",
|
||||
.image => "Image",
|
||||
.printer => "Printer",
|
||||
.mass_storage => "Mass Storage",
|
||||
.hub => "Hub",
|
||||
.cdc_data => "CDC Data",
|
||||
.smart_card => "Smart Card",
|
||||
.content_security => "Content Security",
|
||||
.video => "Video",
|
||||
.personal_healthcare => "Personal Healthcare",
|
||||
.audio_video => "Audio/Video",
|
||||
.billboard => "Billboard",
|
||||
.type_c_bridge => "Type-C Bridge",
|
||||
.bulk_display => "Bulk Display",
|
||||
.mctp => "MCTP",
|
||||
.i3c => "I3C",
|
||||
.diagnostic => "Diagnostic",
|
||||
.wireless_controller => "Wireless Controller",
|
||||
.miscellaneous => "Miscellaneous",
|
||||
.application_specific => "Application-specific",
|
||||
.vendor_specific => "Vendor-specific",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
|
||||
/// The USB speed class (as xHCI reports it in PORTSC/slot contexts) named.
|
||||
pub fn speedName(speed: u32) []const u8 {
|
||||
return switch (speed) {
|
||||
1 => "Full-speed",
|
||||
2 => "Low-speed",
|
||||
3 => "High-speed",
|
||||
4 => "SuperSpeed",
|
||||
5 => "SuperSpeedPlus",
|
||||
else => "unknown-speed",
|
||||
};
|
||||
}
|
||||
|
||||
/// A USB3 Port Link State (xHCI PORTSC PLS field) named.
|
||||
pub fn linkStateName(pls: u32) []const u8 {
|
||||
return switch (pls) {
|
||||
0 => "U0",
|
||||
1 => "U1",
|
||||
2 => "U2",
|
||||
3 => "U3-suspended",
|
||||
4 => "Disabled",
|
||||
5 => "RxDetect",
|
||||
6 => "Inactive",
|
||||
7 => "Polling",
|
||||
8 => "Recovery",
|
||||
9 => "HotReset",
|
||||
10 => "Compliance",
|
||||
11 => "Test",
|
||||
15 => "Resume",
|
||||
else => "reserved",
|
||||
};
|
||||
}
|
||||
|
||||
/// The most useful readable name for an interface's (class, subclass, protocol)
|
||||
/// triple, decoding the well-known combinations recognizable in a log — e.g.
|
||||
/// "HID boot keyboard", "Mass Storage SCSI Bulk-Only", "Bluetooth". Falls back
|
||||
/// to the class name (and then "Unknown") for codes without a spelled-out combo.
|
||||
pub fn interfaceName(class: u8, subclass: u8, protocol: u8) []const u8 {
|
||||
return switch (@as(Class, @enumFromInt(class))) {
|
||||
.hid => if (subclass == @intFromEnum(hid.SubClass.boot)) switch (@as(hid.Protocol, @enumFromInt(protocol))) {
|
||||
.keyboard => "HID boot keyboard",
|
||||
.mouse => "HID boot mouse",
|
||||
else => "HID boot device",
|
||||
} else "HID",
|
||||
.mass_storage => switch (@as(mass_storage.Protocol, @enumFromInt(protocol))) {
|
||||
.bulk_only => "Mass Storage (Bulk-Only)",
|
||||
.uas => "Mass Storage (UAS)",
|
||||
else => "Mass Storage",
|
||||
},
|
||||
.hub => switch (@as(hub.Protocol, @enumFromInt(protocol))) {
|
||||
.super_speed => "Hub (SuperSpeed)",
|
||||
.hi_speed_multi_tt => "Hub (Hi-Speed multi-TT)",
|
||||
.hi_speed_single_tt => "Hub (Hi-Speed single-TT)",
|
||||
else => "Hub",
|
||||
},
|
||||
.wireless_controller => if (subclass == @intFromEnum(wireless_controller.SubClass.radio_frequency))
|
||||
wireless_controller.protocolName(protocol)
|
||||
else
|
||||
"Wireless Controller",
|
||||
.communications => communications.subclassName(subclass),
|
||||
.application_specific => application_specific.subclassName(subclass),
|
||||
.miscellaneous => "Miscellaneous",
|
||||
else => className(class),
|
||||
};
|
||||
}
|
||||
|
||||
// Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the
|
||||
// protocol distinguishes the hub's transaction-translator arrangement.
|
||||
pub const hub = struct {
|
||||
@@ -84,6 +182,16 @@ pub const hub = struct {
|
||||
super_speed = 0x03,
|
||||
_,
|
||||
};
|
||||
|
||||
pub fn protocolName(protocol: u8) []const u8 {
|
||||
return switch (@as(Protocol, @enumFromInt(protocol))) {
|
||||
.full_speed => "full-speed",
|
||||
.hi_speed_single_tt => "Hi-Speed single-TT",
|
||||
.hi_speed_multi_tt => "Hi-Speed multi-TT",
|
||||
.super_speed => "SuperSpeed",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.hid.
|
||||
@@ -104,6 +212,23 @@ pub const hid = struct {
|
||||
mouse = 0x02,
|
||||
_,
|
||||
};
|
||||
|
||||
pub fn subclassName(subclass: u8) []const u8 {
|
||||
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
||||
.none => "none",
|
||||
.boot => "boot",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn protocolName(protocol: u8) []const u8 {
|
||||
return switch (@as(Protocol, @enumFromInt(protocol))) {
|
||||
.none => "none",
|
||||
.keyboard => "keyboard",
|
||||
.mouse => "mouse",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.mass_storage. The subclass identifies the
|
||||
@@ -147,6 +272,33 @@ pub const mass_storage = struct {
|
||||
vendor_specific = 0xFF,
|
||||
_,
|
||||
};
|
||||
|
||||
pub fn subclassName(subclass: u8) []const u8 {
|
||||
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
||||
.not_reported => "SCSI (not reported)",
|
||||
.rbc => "RBC",
|
||||
.atapi => "ATAPI",
|
||||
.qic_157 => "QIC-157",
|
||||
.ufi => "UFI",
|
||||
.sff_8070i => "SFF-8070i",
|
||||
.scsi => "SCSI",
|
||||
.lsd_fs => "LSD FS",
|
||||
.ieee_1667 => "IEEE 1667",
|
||||
.vendor_specific => "vendor-specific",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn protocolName(protocol: u8) []const u8 {
|
||||
return switch (@as(Protocol, @enumFromInt(protocol))) {
|
||||
.cbi_completion_interrupt => "CBI",
|
||||
.cbi => "CBI (no completion IRQ)",
|
||||
.bulk_only => "Bulk-Only",
|
||||
.uas => "UAS",
|
||||
.vendor_specific => "vendor-specific",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.communications (CDC). The protocol codes
|
||||
@@ -182,6 +334,25 @@ pub const communications = struct {
|
||||
network_control = 0x0D,
|
||||
_,
|
||||
};
|
||||
|
||||
pub fn subclassName(subclass: u8) []const u8 {
|
||||
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
||||
.direct_line => "Direct Line",
|
||||
.abstract_control => "Abstract Control (modem/serial)",
|
||||
.telephone => "Telephone",
|
||||
.multi_channel => "Multi-Channel",
|
||||
.capi => "CAPI",
|
||||
.ethernet => "Ethernet",
|
||||
.atm => "ATM",
|
||||
.wireless_handset => "Wireless Handset",
|
||||
.device_management => "Device Management",
|
||||
.mobile_direct_line => "Mobile Direct Line",
|
||||
.obex => "OBEX",
|
||||
.ethernet_emulation => "Ethernet Emulation",
|
||||
.network_control => "Network Control",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.wireless_controller.
|
||||
@@ -204,6 +375,23 @@ pub const wireless_controller = struct {
|
||||
bluetooth_amp = 0x04,
|
||||
_,
|
||||
};
|
||||
|
||||
pub fn subclassName(subclass: u8) []const u8 {
|
||||
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
||||
.radio_frequency => "RF",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn protocolName(protocol: u8) []const u8 {
|
||||
return switch (@as(Protocol, @enumFromInt(protocol))) {
|
||||
.bluetooth => "Bluetooth",
|
||||
.ultra_wideband => "Ultra-Wideband",
|
||||
.remote_ndis => "Remote NDIS",
|
||||
.bluetooth_amp => "Bluetooth AMP",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.miscellaneous.
|
||||
@@ -221,6 +409,20 @@ pub const miscellaneous = struct {
|
||||
interface_association = 0x01,
|
||||
_,
|
||||
};
|
||||
|
||||
pub fn subclassName(subclass: u8) []const u8 {
|
||||
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
||||
.common => "common",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn protocolName(protocol: u8) []const u8 {
|
||||
return switch (@as(Protocol, @enumFromInt(protocol))) {
|
||||
.interface_association => "Interface Association",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.application_specific.
|
||||
@@ -234,6 +436,15 @@ pub const application_specific = struct {
|
||||
test_and_measurement = 0x03,
|
||||
_,
|
||||
};
|
||||
|
||||
pub fn subclassName(subclass: u8) []const u8 {
|
||||
return switch (@as(SubClass, @enumFromInt(subclass))) {
|
||||
.firmware_upgrade => "Device Firmware Upgrade",
|
||||
.irda_bridge => "IrDA Bridge",
|
||||
.test_and_measurement => "Test & Measurement",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Pack a (class, subclass, protocol) triple into one 0xCCSSPP value — the
|
||||
@@ -280,6 +491,30 @@ test "class codes match the USB-IF assignments" {
|
||||
_ = application_specific.SubClass.firmware_upgrade;
|
||||
}
|
||||
|
||||
test "readable names decode the well-known triples" {
|
||||
const std = @import("std");
|
||||
const eql = std.testing.expectEqualStrings;
|
||||
|
||||
try eql("Hub", className(0x09));
|
||||
try eql("Unknown", className(0x42));
|
||||
|
||||
// interfaceName decodes the combos we log.
|
||||
try eql("HID boot keyboard", interfaceName(0x03, 0x01, 0x01));
|
||||
try eql("HID boot mouse", interfaceName(0x03, 0x01, 0x02));
|
||||
try eql("Mass Storage (Bulk-Only)", interfaceName(0x08, 0x06, 0x50));
|
||||
try eql("Hub (SuperSpeed)", interfaceName(0x09, 0x00, 0x03));
|
||||
try eql("Bluetooth", interfaceName(0xE0, 0x01, 0x01));
|
||||
|
||||
// The per-enum name functions.
|
||||
try eql("Bulk-Only", mass_storage.protocolName(0x50));
|
||||
try eql("SCSI", mass_storage.subclassName(0x06));
|
||||
try eql("Bluetooth", wireless_controller.protocolName(0x01));
|
||||
try eql("SuperSpeed", hub.protocolName(0x03));
|
||||
try eql("keyboard", hid.protocolName(0x01));
|
||||
try eql("SuperSpeed", speedName(4));
|
||||
try eql("Polling", linkStateName(7));
|
||||
}
|
||||
|
||||
test "packTriple / unpackTriple round-trip the identity a bus driver reports" {
|
||||
const std = @import("std");
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
|
||||
@@ -189,6 +189,10 @@ fn speedName(speed: u32) []const u8 {
|
||||
/// class driver against.
|
||||
var manager_handle: ?runtime.ipc.Handle = null;
|
||||
|
||||
// Per-root-port connected state from the previous tick, so the poll acts on
|
||||
// empty->connected transitions (edge), never re-attempting a level every tick.
|
||||
var prev_connected: [64]bool = [_]bool{false} ** 64;
|
||||
|
||||
fn scanPorts(manager: runtime.ipc.Handle) void {
|
||||
const engine = if (controller) |*c| c else {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: controller not initialised\n");
|
||||
@@ -200,10 +204,14 @@ fn scanPorts(manager: runtime.ipc.Handle) void {
|
||||
var connected: u32 = 0;
|
||||
while (port <= engine.max_ports) : (port += 1) {
|
||||
if (!engine.portConnected(port)) continue;
|
||||
if (port < prev_connected.len) prev_connected[port] = true; // don't re-fire the poll for these
|
||||
connected += 1;
|
||||
bringUpPort(manager, engine, port);
|
||||
}
|
||||
if (connected == 0) _ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n");
|
||||
if (connected == 0) {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n");
|
||||
engine.dumpPortTopology(); // help diagnose an empty scan: the xECP map + raw PORTSC
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring up whatever is on `port`: setup + enumerate + register/report one child
|
||||
@@ -221,8 +229,15 @@ fn bringUpPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u
|
||||
std.log.info("port {d} enumeration failed", .{port});
|
||||
return;
|
||||
}
|
||||
std.log.info("port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)", .{
|
||||
var maker_buffer: [64]u8 = undefined;
|
||||
var product_buffer: [64]u8 = undefined;
|
||||
const maker = engine.readString(usb_device, @intFromEnum(usb_device.device_descriptor.manufacturer_index), &maker_buffer) orelse "?";
|
||||
const product = engine.readString(usb_device, @intFromEnum(usb_device.device_descriptor.product_index), &product_buffer) orelse "?";
|
||||
std.log.info("port {d} device: {s} \"{s} {s}\" (0x{x:0>4}:0x{x:0>4}), {d} interface(s)", .{
|
||||
port,
|
||||
usb_ids.className(usb_device.device_descriptor.device_class),
|
||||
maker,
|
||||
product,
|
||||
usb_device.device_descriptor.vendor_id,
|
||||
usb_device.device_descriptor.product_id,
|
||||
usb_device.interface_count,
|
||||
@@ -256,8 +271,9 @@ fn hubPortKey(hub_slot: u8, port: u16) u32 {
|
||||
fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hub: *library.Device, port: u16) void {
|
||||
const status = engine.hubPortStatusAck(hub, port) orelse return;
|
||||
const connected = library.Controller.hubPortConnected(status);
|
||||
std.log.info("hub slot {d} port {d} status 0x{x:0>8} ({s})", .{ hub.slot_id, port, status, if (connected) "connected" else "empty" });
|
||||
const existing = engine.deviceOnHubPort(hub, port);
|
||||
if (connected != (existing != null)) // only when a device appears or leaves — not empty seed-sweep ports
|
||||
std.log.info("hub slot {d} port {d}: {s} (status 0x{x:0>4})", .{ hub.slot_id, port, if (connected) "device connected" else "device removed", status & 0xFFFF });
|
||||
|
||||
if (!connected) {
|
||||
if (existing) |dev| tearDownHubDevice(manager, engine, dev);
|
||||
@@ -270,8 +286,15 @@ fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hu
|
||||
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)", .{
|
||||
var maker_buffer: [64]u8 = undefined;
|
||||
var product_buffer: [64]u8 = undefined;
|
||||
const maker = engine.readString(usb_device, @intFromEnum(usb_device.device_descriptor.manufacturer_index), &maker_buffer) orelse "?";
|
||||
const product = engine.readString(usb_device, @intFromEnum(usb_device.device_descriptor.product_index), &product_buffer) orelse "?";
|
||||
std.log.info("hub slot {d} port {d} device: {s} \"{s} {s}\" (0x{x:0>4}:0x{x:0>4}), {d} interface(s)", .{
|
||||
hub.slot_id, port,
|
||||
usb_ids.className(usb_device.device_descriptor.device_class),
|
||||
maker,
|
||||
product,
|
||||
usb_device.device_descriptor.vendor_id,
|
||||
usb_device.device_descriptor.product_id,
|
||||
usb_device.interface_count,
|
||||
@@ -378,9 +401,10 @@ fn reportInterface(manager: runtime.ipc.Handle, port: u32, interface: library.In
|
||||
std.log.info("child report for port {d} interface {d} failed", .{ port, interface.number });
|
||||
return null;
|
||||
};
|
||||
std.log.info("port {d} interface {d} class {d}/{d}/{d} registered as device {d}", .{
|
||||
std.log.info("port {d} interface {d}: {s} ({d}/{d}/{d}) registered as device {d}", .{
|
||||
port,
|
||||
interface.number,
|
||||
usb_ids.interfaceName(interface.class, interface.subclass, interface.protocol),
|
||||
interface.class,
|
||||
interface.subclass,
|
||||
interface.protocol,
|
||||
@@ -491,9 +515,34 @@ fn onNotification(badge: u64) void {
|
||||
if (badge & runtime.ipc.notify_timer_bit == 0) return;
|
||||
if (controller) |*engine| {
|
||||
engine.pump();
|
||||
// Poll every root port and reconcile — a device present but not yet
|
||||
// enumerated is brought up; a device gone is torn down. This does NOT
|
||||
// depend on a Port Status Change EVENT firing: the boot scan runs ~3 ms
|
||||
// after the controller reset, far too early for a USB2 connection to
|
||||
// debounce (~100 ms), and the SuperSpeed devices that DO show up early
|
||||
// proved the event path unreliable for the late USB2 companion hub on
|
||||
// real hardware. Polling catches it on the next tick regardless.
|
||||
if (manager_handle) |manager| {
|
||||
var port: u32 = 1;
|
||||
while (port <= engine.max_ports and port <= prev_connected.len) : (port += 1) {
|
||||
const connected = engine.portConnected(port);
|
||||
const was = prev_connected[port];
|
||||
prev_connected[port] = connected;
|
||||
if (connected and !was and engine.deviceOnPort(port) == null) {
|
||||
// Rising edge the boot scan missed (it ran before the USB2
|
||||
// connection debounced): bring the device up now.
|
||||
std.log.info("root port {d}: device appeared (PORTSC 0x{x:0>8})", .{ port, engine.portStatus(port) });
|
||||
bringUpPort(manager, engine, port);
|
||||
} else if (!connected and was and engine.deviceOnPort(port) != null) {
|
||||
tearDownPort(manager, engine, port);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (engine.takePortChange()) |port| {
|
||||
const manager = manager_handle orelse break;
|
||||
if (engine.portConnected(port)) {
|
||||
const connected = engine.portConnected(port);
|
||||
std.log.info("root port {d} change: {s} (PORTSC 0x{x:0>8})", .{ port, if (connected) "connected" else "empty", engine.portStatus(port) });
|
||||
if (connected) {
|
||||
if (engine.deviceOnPort(port) == null) bringUpPort(manager, engine, port);
|
||||
} else {
|
||||
tearDownPort(manager, engine, port);
|
||||
|
||||
@@ -22,6 +22,7 @@ const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const mmio = @import("mmio");
|
||||
const usb_abi = @import("usb-abi");
|
||||
const usb_ids = @import("usb-ids");
|
||||
const dma = runtime.dma;
|
||||
const system = runtime.system;
|
||||
|
||||
@@ -100,6 +101,7 @@ pub const TrbType = enum(u6) {
|
||||
pub const CompletionCode = enum(u8) {
|
||||
invalid = 0,
|
||||
success = 1,
|
||||
usb_transaction_error = 4,
|
||||
short_packet = 13,
|
||||
_,
|
||||
};
|
||||
@@ -484,6 +486,73 @@ pub const Controller = struct {
|
||||
}
|
||||
|
||||
// PORTSC for 1-based port `port`.
|
||||
/// Diagnostic: walk the xECP list and log each Supported Protocol capability
|
||||
/// (USB 2.0 vs 3.x, the compatible root-port range), then dump every port's
|
||||
/// raw PORTSC. Reveals where the USB2 root ports are and their state — for
|
||||
/// finding a USB2 companion hub that isn't presenting a connection.
|
||||
pub fn dumpPortTopology(self: *const Controller) void {
|
||||
const hccparams1 = read32(self.register_base + cap_hccparams1);
|
||||
var offset: usize = (hccparams1 >> 16) & 0xFFFF; // xECP: dword offset from register_base
|
||||
var guard: u32 = 0;
|
||||
while (offset != 0 and guard < 64) : (guard += 1) {
|
||||
const cap_base = self.register_base + offset * 4;
|
||||
const dw0 = read32(cap_base);
|
||||
const id = dw0 & 0xFF;
|
||||
if (id == 2) { // Supported Protocol
|
||||
const dw2 = read32(cap_base + 8);
|
||||
const major = (dw0 >> 24) & 0xFF;
|
||||
const minor = (dw0 >> 16) & 0xFF;
|
||||
const port_offset = dw2 & 0xFF;
|
||||
const port_count = (dw2 >> 8) & 0xFF;
|
||||
std.log.info("xECP USB {d}.{d}: root ports {d}..{d}", .{ major, minor, port_offset, port_offset + port_count - 1 });
|
||||
}
|
||||
const next = (dw0 >> 8) & 0xFF;
|
||||
if (next == 0) break;
|
||||
offset += next;
|
||||
}
|
||||
var port: u32 = 1;
|
||||
while (port <= self.max_ports) : (port += 1) {
|
||||
const portsc = self.portStatus(port);
|
||||
std.log.info("PORTSC[{d}] 0x{x:0>8}: {s}, {s}, link={s}, power={s}, {s}", .{
|
||||
port,
|
||||
portsc,
|
||||
if (portsc & 1 != 0) "connected" else "empty",
|
||||
if (portsc & 2 != 0) "enabled" else "disabled",
|
||||
usb_ids.linkStateName((portsc >> 5) & 0xF),
|
||||
if (portsc & (1 << 9) != 0) "on" else "off",
|
||||
usb_ids.speedName((portsc >> 10) & 0xF),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Read USB STRING descriptor `index` (English, langid 0x0409) into `out` as
|
||||
/// ASCII, returning the slice — for logging manufacturer/product names.
|
||||
/// Null for index 0 (no string) or a failed transfer. Non-ASCII code units
|
||||
/// become '?'.
|
||||
pub fn readString(self: *Controller, device: *Device, index: u8, out: []u8) ?[]const u8 {
|
||||
if (index == 0) return null;
|
||||
var raw: [256]u8 = undefined;
|
||||
const request = usb_abi.Request{
|
||||
.request_type = .{ .recipient = .device, .kind = .standard, .direction = .device_to_host },
|
||||
.request_code = .get_descriptor,
|
||||
.value = (@as(u16, 3) << 8) | index, // STRING descriptor
|
||||
.index = 0x0409, // English (US)
|
||||
.length = raw.len,
|
||||
};
|
||||
if (!self.controlTransfer(device, request, raw[0..], true)) return null;
|
||||
const length = raw[0]; // bLength; the UTF-16LE payload is bytes 2..length
|
||||
if (length < 2) return null;
|
||||
const chars = (@min(length, raw.len) - 2) / 2;
|
||||
var n: usize = 0;
|
||||
var i: usize = 0;
|
||||
while (i < chars and n < out.len) : (i += 1) {
|
||||
const unit = @as(u16, raw[2 + i * 2]) | (@as(u16, raw[2 + i * 2 + 1]) << 8);
|
||||
out[n] = if (unit >= 0x20 and unit < 0x7F) @intCast(unit) else '?';
|
||||
n += 1;
|
||||
}
|
||||
return out[0..n];
|
||||
}
|
||||
|
||||
pub fn portStatus(self: *const Controller, port: u32) u32 {
|
||||
return read32(self.op_base + op_portsc_base + op_portsc_stride * (port - 1));
|
||||
}
|
||||
@@ -768,19 +837,31 @@ pub const Controller = struct {
|
||||
}
|
||||
|
||||
fn addressDeviceCommand(self: *Controller, device: *Device) bool {
|
||||
const physical = self.submitCommand(.{
|
||||
.parameter = device.input_context.physical,
|
||||
.control = trbControl(.address_device, @as(u32, device.slot_id) << 24),
|
||||
});
|
||||
const code = self.awaitCommand(physical) orelse {
|
||||
std.log.info("port {d} setup: Address Device timed out", .{device.port});
|
||||
return false;
|
||||
};
|
||||
if (code != @intFromEnum(CompletionCode.success)) {
|
||||
std.log.info("port {d} setup: Address Device completion code {d}", .{ device.port, code });
|
||||
return false;
|
||||
// Retry on a USB Transaction Error (code 4): a freshly-reset device can
|
||||
// miss the first SET_ADDRESS; re-reset the port and try again (xHCI
|
||||
// 4.6.5). Up to 3 attempts.
|
||||
var attempt: u32 = 0;
|
||||
while (attempt < 3) : (attempt += 1) {
|
||||
const physical = self.submitCommand(.{
|
||||
.parameter = device.input_context.physical,
|
||||
.control = trbControl(.address_device, @as(u32, device.slot_id) << 24),
|
||||
});
|
||||
const code = self.awaitCommand(physical) orelse {
|
||||
std.log.info("port {d} setup: Address Device timed out (attempt {d})", .{ device.port, attempt + 1 });
|
||||
return false;
|
||||
};
|
||||
if (code == @intFromEnum(CompletionCode.success)) return true;
|
||||
std.log.info("port {d} setup: Address Device completion code {d} (attempt {d})", .{ device.port, code, attempt + 1 });
|
||||
if (code != @intFromEnum(CompletionCode.usb_transaction_error)) return false;
|
||||
// Re-reset a root-port device and wait the recovery interval before
|
||||
// retrying. (A device behind a hub is reset through the hub — not
|
||||
// retried here; its port was reset in serviceHubPort.)
|
||||
if (device.parent_slot == 0) {
|
||||
if (!self.resetPort(device.port)) return false;
|
||||
system.sleep(10);
|
||||
} else return false;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Reset the port, enable a slot, and address the device on it: after this the
|
||||
@@ -806,6 +887,11 @@ pub const Controller = struct {
|
||||
// (pre-reset reads misreport on real controllers; M20).
|
||||
effective_speed = (self.portStatus(port) >> 10) & 0xF;
|
||||
if (effective_speed == 0) effective_speed = speed; // defensive: keep the caller's read
|
||||
// USB 2.0 spec 7.1.7.5: a device needs a reset-recovery interval
|
||||
// (TRSTRCY, 10 ms) after reset before it answers SET_ADDRESS.
|
||||
// Addressing immediately gives a USB Transaction Error (code 4) on
|
||||
// real full-speed devices; QEMU tolerates the omission.
|
||||
system.sleep(10);
|
||||
}
|
||||
const slot_id = self.enableSlot() orelse {
|
||||
std.log.info("port {d} setup: Enable Slot failed", .{port});
|
||||
|
||||
+2
-2
@@ -494,7 +494,7 @@ CASES = [
|
||||
# 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"(?=.*hub slot \d+ port \d+ device:.*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,
|
||||
@@ -676,7 +676,7 @@ CASES = [
|
||||
"-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)"
|
||||
"expect": r"(?s)(?=.*hub slot \d+ port \d+ device:.*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;
|
||||
|
||||
Reference in New Issue
Block a user