usb: readable PORTSC decode, link-state names, and device string descriptors

Make the USB diagnostic logs scannable by eye:
- usb-ids gains speedName + linkStateName (USB3 PORTSC link states: U0,
  RxDetect, Polling, ...), host-tested with usb-ids.
- The PORTSC dump decodes the register instead of printing hex flags:
    PORTSC[3] 0x00021203: connected, enabled, link=U0, power=on, SuperSpeed
    PORTSC[5] 0x00020ee1: connected, disabled, link=Polling, power=on, High-speed
  (the raw word stays for reference). A USB2 device reads connected+
  disabled+Polling until reset — so the user's next log shows at a glance
  whether the companion port ever reaches that state.
- The library reads STRING descriptors (readString, UTF-16LE -> ASCII,
  English langid), and the device line now names the maker + product:
    port 5 device: Hub "Genesys Logic USB3.0 Hub" (0x05e3:0x0626), 1 interface(s)
  instead of a bare vendor/product id pair.

Full USB case set green (hub regexes follow the new 'device: <class>
"<maker> <product>"' format). Branch diagnostics for the SuperSpeed
compound-hub investigation.
This commit is contained in:
Daniel Samson
2026-07-21 23:19:18 +01:00
parent 4ea4a040d2
commit ab732dc455
4 changed files with 85 additions and 6 deletions
+32
View File
@@ -103,6 +103,38 @@ pub fn className(class: u8) []const u8 {
};
}
/// 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",
};
}
/// A fuller name for a HID interface's (subclass, protocol): "HID boot keyboard"
/// / "HID boot mouse", else just the class name. Used to make input devices
/// recognizable in logs.
+14 -2
View File
@@ -230,9 +230,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: {s} 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,
@@ -280,9 +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: {s} 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,
@@ -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;
@@ -511,12 +512,46 @@ pub const Controller = struct {
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} ccs={d} ped={d} pls={d} pp={d} speed={d}", .{
port, portsc, portsc & 1, (portsc >> 1) & 1, (portsc >> 5) & 0xF, (portsc >> 9) & 1, (portsc >> 10) & 0xF,
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));
}