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
|
// Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the
|
||||||
// protocol distinguishes the hub's transaction-translator arrangement.
|
// protocol distinguishes the hub's transaction-translator arrangement.
|
||||||
pub const hub = struct {
|
pub const hub = struct {
|
||||||
@@ -84,6 +182,16 @@ pub const hub = struct {
|
|||||||
super_speed = 0x03,
|
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.
|
// Subclass and protocol codes qualified by Class.hid.
|
||||||
@@ -104,6 +212,23 @@ pub const hid = struct {
|
|||||||
mouse = 0x02,
|
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
|
// 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,
|
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
|
// Subclass and protocol codes qualified by Class.communications (CDC). The protocol codes
|
||||||
@@ -182,6 +334,25 @@ pub const communications = struct {
|
|||||||
network_control = 0x0D,
|
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.
|
// Subclass and protocol codes qualified by Class.wireless_controller.
|
||||||
@@ -204,6 +375,23 @@ pub const wireless_controller = struct {
|
|||||||
bluetooth_amp = 0x04,
|
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.
|
// Subclass and protocol codes qualified by Class.miscellaneous.
|
||||||
@@ -221,6 +409,20 @@ pub const miscellaneous = struct {
|
|||||||
interface_association = 0x01,
|
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.
|
// Subclass and protocol codes qualified by Class.application_specific.
|
||||||
@@ -234,6 +436,15 @@ pub const application_specific = struct {
|
|||||||
test_and_measurement = 0x03,
|
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
|
/// 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;
|
_ = 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" {
|
test "packTriple / unpackTriple round-trip the identity a bus driver reports" {
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const expectEqual = std.testing.expectEqual;
|
const expectEqual = std.testing.expectEqual;
|
||||||
|
|||||||
@@ -189,6 +189,10 @@ fn speedName(speed: u32) []const u8 {
|
|||||||
/// class driver against.
|
/// class driver against.
|
||||||
var manager_handle: ?runtime.ipc.Handle = null;
|
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 {
|
fn scanPorts(manager: runtime.ipc.Handle) void {
|
||||||
const engine = if (controller) |*c| c else {
|
const engine = if (controller) |*c| c else {
|
||||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: controller not initialised\n");
|
_ = 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;
|
var connected: u32 = 0;
|
||||||
while (port <= engine.max_ports) : (port += 1) {
|
while (port <= engine.max_ports) : (port += 1) {
|
||||||
if (!engine.portConnected(port)) continue;
|
if (!engine.portConnected(port)) continue;
|
||||||
|
if (port < prev_connected.len) prev_connected[port] = true; // don't re-fire the poll for these
|
||||||
connected += 1;
|
connected += 1;
|
||||||
bringUpPort(manager, engine, port);
|
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
|
/// 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});
|
std.log.info("port {d} enumeration failed", .{port});
|
||||||
return;
|
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,
|
port,
|
||||||
|
usb_ids.className(usb_device.device_descriptor.device_class),
|
||||||
|
maker,
|
||||||
|
product,
|
||||||
usb_device.device_descriptor.vendor_id,
|
usb_device.device_descriptor.vendor_id,
|
||||||
usb_device.device_descriptor.product_id,
|
usb_device.device_descriptor.product_id,
|
||||||
usb_device.interface_count,
|
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 {
|
fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hub: *library.Device, port: u16) void {
|
||||||
const status = engine.hubPortStatusAck(hub, port) orelse return;
|
const status = engine.hubPortStatusAck(hub, port) orelse return;
|
||||||
const connected = library.Controller.hubPortConnected(status);
|
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);
|
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 (!connected) {
|
||||||
if (existing) |dev| tearDownHubDevice(manager, engine, dev);
|
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 });
|
std.log.info("hub slot {d} port {d}: enumeration failed", .{ hub.slot_id, port });
|
||||||
return;
|
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,
|
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.vendor_id,
|
||||||
usb_device.device_descriptor.product_id,
|
usb_device.device_descriptor.product_id,
|
||||||
usb_device.interface_count,
|
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 });
|
std.log.info("child report for port {d} interface {d} failed", .{ port, interface.number });
|
||||||
return null;
|
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,
|
port,
|
||||||
interface.number,
|
interface.number,
|
||||||
|
usb_ids.interfaceName(interface.class, interface.subclass, interface.protocol),
|
||||||
interface.class,
|
interface.class,
|
||||||
interface.subclass,
|
interface.subclass,
|
||||||
interface.protocol,
|
interface.protocol,
|
||||||
@@ -491,9 +515,34 @@ fn onNotification(badge: u64) void {
|
|||||||
if (badge & runtime.ipc.notify_timer_bit == 0) return;
|
if (badge & runtime.ipc.notify_timer_bit == 0) return;
|
||||||
if (controller) |*engine| {
|
if (controller) |*engine| {
|
||||||
engine.pump();
|
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| {
|
while (engine.takePortChange()) |port| {
|
||||||
const manager = manager_handle orelse break;
|
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);
|
if (engine.deviceOnPort(port) == null) bringUpPort(manager, engine, port);
|
||||||
} else {
|
} else {
|
||||||
tearDownPort(manager, engine, port);
|
tearDownPort(manager, engine, port);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const std = @import("std");
|
|||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
const mmio = @import("mmio");
|
const mmio = @import("mmio");
|
||||||
const usb_abi = @import("usb-abi");
|
const usb_abi = @import("usb-abi");
|
||||||
|
const usb_ids = @import("usb-ids");
|
||||||
const dma = runtime.dma;
|
const dma = runtime.dma;
|
||||||
const system = runtime.system;
|
const system = runtime.system;
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ pub const TrbType = enum(u6) {
|
|||||||
pub const CompletionCode = enum(u8) {
|
pub const CompletionCode = enum(u8) {
|
||||||
invalid = 0,
|
invalid = 0,
|
||||||
success = 1,
|
success = 1,
|
||||||
|
usb_transaction_error = 4,
|
||||||
short_packet = 13,
|
short_packet = 13,
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
@@ -484,6 +486,73 @@ pub const Controller = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PORTSC for 1-based port `port`.
|
// 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 {
|
pub fn portStatus(self: *const Controller, port: u32) u32 {
|
||||||
return read32(self.op_base + op_portsc_base + op_portsc_stride * (port - 1));
|
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 {
|
fn addressDeviceCommand(self: *Controller, device: *Device) bool {
|
||||||
const physical = self.submitCommand(.{
|
// Retry on a USB Transaction Error (code 4): a freshly-reset device can
|
||||||
.parameter = device.input_context.physical,
|
// miss the first SET_ADDRESS; re-reset the port and try again (xHCI
|
||||||
.control = trbControl(.address_device, @as(u32, device.slot_id) << 24),
|
// 4.6.5). Up to 3 attempts.
|
||||||
});
|
var attempt: u32 = 0;
|
||||||
const code = self.awaitCommand(physical) orelse {
|
while (attempt < 3) : (attempt += 1) {
|
||||||
std.log.info("port {d} setup: Address Device timed out", .{device.port});
|
const physical = self.submitCommand(.{
|
||||||
return false;
|
.parameter = device.input_context.physical,
|
||||||
};
|
.control = trbControl(.address_device, @as(u32, device.slot_id) << 24),
|
||||||
if (code != @intFromEnum(CompletionCode.success)) {
|
});
|
||||||
std.log.info("port {d} setup: Address Device completion code {d}", .{ device.port, code });
|
const code = self.awaitCommand(physical) orelse {
|
||||||
return false;
|
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
|
/// 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).
|
// (pre-reset reads misreport on real controllers; M20).
|
||||||
effective_speed = (self.portStatus(port) >> 10) & 0xF;
|
effective_speed = (self.portStatus(port) >> 10) & 0xF;
|
||||||
if (effective_speed == 0) effective_speed = speed; // defensive: keep the caller's read
|
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 {
|
const slot_id = self.enableSlot() orelse {
|
||||||
std.log.info("port {d} setup: Enable Slot failed", .{port});
|
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
|
# B4a: the hub powers its ports. B4b: the keyboard behind it enumerates
|
||||||
# (route string + TT) and binds usb-hid-keyboard.
|
# (route string + TT) and binds usb-hid-keyboard.
|
||||||
"expect": r"(?s)(?=.*hub slot \d+: \d+ downstream ports powered)"
|
"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)",
|
r"(?=.*usb-hid-keyboard: ok \(device 3)",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# USB mass storage end to end: the boot usb-storage device (the FAT32 image,
|
# 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",
|
||||||
"-device", "usb-hub,bus=xhci2.0,port=1.1",
|
"-device", "usb-hub,bus=xhci2.0,port=1.1",
|
||||||
"-device", "usb-kbd,bus=xhci2.0,port=1.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)",
|
r"(?=.*usb-hid-keyboard: ok \(device 3)",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# Hub-downstream disconnect (B4c): device_del the keyboard behind the hub;
|
# Hub-downstream disconnect (B4c): device_del the keyboard behind the hub;
|
||||||
|
|||||||
Reference in New Issue
Block a user