Files
danos/system/drivers/usb-xhci-bus/usb-xhci-bus.zig
T
Daniel Samson 18408b0666 usb: [diagnostic] dump all root-port PORTSC + log port-change events
Branch-only debugging for the real SuperSpeed compound hub: the spin fix
(27f87cb) stopped the hang — the SS hub's 4 SuperSpeed ports now enumerate
(all empty, correct: the full-speed keyboard isn't a SuperSpeed device) —
but the USB2 COMPANION hub, where the keyboard actually lives, never
appears. Only root ports 19 (SS hub) and 21 (SS storage) connect.

Dump every root port's raw PORTSC at scan (connect/enable/link-state/
speed) and log every root port-change event, so the next boot shows
whether the companion is connected on a USB2 port we misread, connects
late as a port-change event, or is simply absent. Diagnostic logging —
to be removed once the companion path works.
2026-07-21 22:50:11 +01:00

559 lines
28 KiB
Zig

//! /system/drivers/usb-xhci-bus — the xHCI (USB 3) host-controller bus driver.
//! The device manager spawns **one instance per controller** it discovers (a
//! machine can carry several), passing the controller's device-tree id as
//! argv[1]; this instance claims that device and no other, so multiple
//! instances never fight over hardware.
//!
//! M18.2 (this increment): after the hello, real hardware — map the xHC's
//! register window (the first memory BAR; resource 0 is the ECAM config
//! space), read the capability registers, and walk the root-hub ports: one
//! `child_added` report to the manager per connected port, carrying the port
//! number and the PORTSC speed class as identity. No transfer rings yet —
//! descriptors and USB class matching are the USB track; the connect bit and
//! speed come straight from PORTSC, which reflects hardware state whether or
//! not the controller is running.
const std = @import("std");
const runtime = @import("runtime");
const protocol = runtime.device_manager_protocol;
const device = runtime.device;
const usb_ids = @import("usb-ids");
const usb_abi = @import("usb-abi");
const transfer = @import("usb-transfer-protocol");
const library = @import("usb-xhci-library.zig");
/// The controller engine (reset, rings, transfers), stood up in `initialise`.
var controller: ?library.Controller = null;
/// This driver's service endpoint (registered as `.usb_bus`), where class-driver
/// requests, signals, and the interrupt-poll timer all arrive.
var service_endpoint: runtime.ipc.Handle = 0;
/// How often the driver drains the event ring for interrupt reports (~125 Hz),
/// re-armed each tick. Frequent enough for responsive input.
const poll_interval_ms: u64 = 8;
/// The class driver endpoints that opened each device, so interrupt reports can
/// be pushed back to them. Keyed by the device token (the interface's device id).
const Open = struct {
used: bool = false,
device_token: u64 = 0,
report_endpoint: usize = 0,
};
var opens = [_]Open{.{}} ** 16;
fn recordOpen(device_token: u64, report_endpoint: usize) void {
for (&opens) |*open| {
if (open.used and open.device_token == device_token) {
open.report_endpoint = report_endpoint;
return;
}
}
for (&opens) |*open| {
if (!open.used) {
open.* = .{ .used = true, .device_token = device_token, .report_endpoint = report_endpoint };
return;
}
}
}
fn reportEndpointFor(device_token: u64) ?usize {
for (&opens) |*open| {
if (open.used and open.device_token == device_token) return open.report_endpoint;
}
return null;
}
var controller_id: u64 = protocol.no_device;
/// Claim the assigned controller, find its register window, and hello the
/// manager. Any failure returns false: the process exits cleanly, which the
/// manager reads as "meant to stop" — a missing assignment is not a crash loop.
fn initialise(endpoint: runtime.ipc.Handle) bool {
service_endpoint = endpoint;
if (!device.claim(controller_id)) {
std.log.info("unable to claim controller device {d}", .{controller_id});
return false;
}
// Fetch our own descriptor back for the controller's resources.
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: out of memory\n");
return false;
};
const total = device.enumerate(buffer);
const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
if (d.id == controller_id) break d;
} else {
std.log.info("device {d} not in the device tree", .{controller_id});
return false;
};
// The xHC's registers live behind the first memory BAR. Resource 0 is the
// function's ECAM configuration space (M15), so the walk starts at 1.
var register_index: u64 = 0;
const register_window = for (descriptor.resources[1..@intCast(descriptor.resource_count)], 1..) |resource, index| {
if (resource.kind == @intFromEnum(device.ResourceKind.memory)) {
register_index = index;
break resource;
}
} else {
std.log.info("controller device {d} has no register BAR", .{controller_id});
return false;
};
std.log.info("claimed controller device {d} (registers at 0x{x}, {d} bytes)", .{
controller_id,
register_window.start,
register_window.len,
});
register_base = device.mmioMap(controller_id, register_index) orelse {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: mmio_map failed\n");
return false;
};
// Bring the controller up: reset it, stand up the command and event rings,
// and start it running (the hardware half lives in usb-xhci-library.zig).
controller = library.Controller.init(register_base) orelse {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: controller reset/bring-up failed\n");
return false;
};
std.log.info("controller running ({d} slots, {d}-byte contexts)", .{
controller.?.max_slots,
controller.?.context_size,
});
// The proof of life: a No-Op command round-trips the command ring, the event
// ring, the doorbell, and the cycle-bit bookkeeping. If this completes, the
// engine is sound; transfers build on exactly this machinery.
if (controller.?.noOpCommand()) {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: command ring running (no-op ok)\n");
} else {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: no-op command did not complete\n");
return false;
}
// The handshake: role, protocol version, assignment — inside the manager's
// deadline (the lookup retries cover the manager still registering).
var manager: ?runtime.ipc.Handle = null;
var tries: u32 = 0;
while (manager == null and tries < 100) : (tries += 1) {
manager = runtime.ipc.lookup(.device_manager);
if (manager == null) runtime.system.sleep(20);
}
const h = manager orelse {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: no device manager to hello\n");
return false;
};
manager_handle = h; // the tick's hot-plug dispatch reports through this
const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = controller_id };
var reply: [protocol.message_maximum]u8 = undefined;
const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello call failed\n");
return false;
};
if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello refused\n");
return false;
}
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello acknowledged\n");
scanPorts(h);
// Arm the poll timer that drains interrupt reports from the event ring. It is
// re-armed on each tick in onNotification; class drivers subscribe later.
_ = runtime.system.timerOnce(service_endpoint, poll_interval_ms);
return true;
}
var register_base: usize = 0;
/// The xHCI default Protocol Speed IDs (the PORTSC port-speed field, bits 13:10)
/// decoded to human names — the boot-log breadcrumb for what actually enumerated on
/// a port, the USB analog of the pci-bus class-code line. A controller may redefine
/// these through its Supported Protocol capability, but the defaults cover every
/// speed QEMU and real hardware report at this (pre-descriptor) stage.
fn speedName(speed: u32) []const u8 {
return switch (speed) {
1 => "Full-speed (USB 2.0, 12 Mb/s)",
2 => "Low-speed (USB 2.0, 1.5 Mb/s)",
3 => "High-speed (USB 2.0, 480 Mb/s)",
4 => "SuperSpeed (USB 3.0, 5 Gb/s)",
5 => "SuperSpeedPlus (USB 3.1, 10 Gb/s)",
else => "unknown speed",
};
}
/// The root-hub scan and enumeration: for each connected port, bring the device
/// up (reset → enable slot → address), read its descriptors, and register +
/// report one child per interface — carrying the interface's (class, subclass,
/// protocol) triple as identity, which is what the device manager matches a
/// class driver against.
var manager_handle: ?runtime.ipc.Handle = null;
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");
return;
};
std.log.info("{d} root-hub ports", .{engine.max_ports});
// Diagnostic: dump every root port's raw PORTSC (CCS = connect, PED =
// enabled, PLS = link state, speed) so a USB2 companion hub that isn't
// enumerating is visible — is it connected on a port we misread, or absent?
{
var p: u32 = 1;
while (p <= engine.max_ports) : (p += 1) {
const portsc = engine.portStatus(p);
if (portsc & 1 != 0 or (portsc >> 5) & 0xF != 5) // connected, or not in the disabled/disconnected link state
std.log.info("PORTSC[{d}] = 0x{x:0>8} ccs={d} ped={d} pls={d} speed={d}", .{ p, portsc, portsc & 1, (portsc >> 1) & 1, (portsc >> 5) & 0xF, (portsc >> 10) & 0xF });
}
}
var port: u32 = 1;
var connected: u32 = 0;
while (port <= engine.max_ports) : (port += 1) {
if (!engine.portConnected(port)) continue;
connected += 1;
bringUpPort(manager, engine, port);
}
if (connected == 0) _ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n");
}
/// Bring up whatever is on `port`: setup + enumerate + register/report one child
/// per interface. Shared by the boot scan and hot-plug (a port-change event with
/// the port now connected).
fn bringUpPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u32) void {
const speed = (engine.portStatus(port) >> 10) & 0xF; // the PORTSC port-speed class
std.log.info("port {d} connected — {s} (speed class {d})", .{ port, speedName(speed), speed });
const usb_device = engine.setupDevice(port, speed) orelse {
std.log.info("port {d} device setup failed", .{port});
return;
};
if (!engine.enumerate(usb_device)) {
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)", .{
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| {
// Record the id each interface was registered as, so a class driver
// opening the interface (by that id) resolves to it.
if (reportInterface(manager, port, interface.*)) |registered| {
interface.registered_device_id = registered;
}
}
// A hub (class 9) is bus infrastructure the bus drives itself: configure it
// and power its downstream ports (docs/usb-hub.md). Its interfaces are still
// reported above, but no external class driver binds it.
if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
}
// A compact topology-unique port key for a hub downstream port: 1000 + slot*100
// + port. Stays a few digits (the id tag "P<key>I<iface>" has an 8-byte cap)
// while never colliding with a root port (1..N) or another (hub, port).
fn hubPortKey(hub_slot: u8, port: u16) u32 {
return 1000 + @as(u32, hub_slot) * 100 + port;
}
/// Service a change on hub downstream `port`: dispatch a connect (enumerate the
/// new device) or a disconnect (tear the old one down). Recurses for a hub
/// behind a hub — a nested hub is set up on connect and its downstream devices
/// torn down first on disconnect.
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) {
if (existing) |dev| tearDownHubDevice(manager, engine, dev);
return;
}
if (existing != null) return; // already up
const usb_device = engine.serviceHubPort(hub, port) orelse return;
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| {
if (reportInterface(manager, hubPortKey(hub.slot_id, port), interface.*)) |registered| {
interface.registered_device_id = registered;
}
}
if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
}
/// Tear down a device that disconnected from a hub: recursively tear down its
/// own downstream devices first if it is a hub, report each interface removed,
/// then Disable Slot. Mirrors tearDownPort for a hub-attached device.
fn tearDownHubDevice(manager: runtime.ipc.Handle, engine: *library.Controller, dev: *library.Device) void {
// A hub that left takes its whole subtree with it — tear children down first.
if (dev.is_hub) {
while (engine.nextChildOf(dev.slot_id, 0)) |child| tearDownHubDevice(manager, engine, child);
}
std.log.info("hub device slot {d} disconnected", .{dev.slot_id});
const key = hubPortKey(dev.parent_slot, dev.parent_port);
for (dev.interfaces[0..dev.interface_count]) |*interface| {
if (interface.registered_device_id == 0) continue;
const event = protocol.ChildRemoved{
.parent = controller_id,
.bus_address = (@as(u64, key) << 8) | interface.number,
};
var reply: [protocol.message_maximum]u8 = undefined;
_ = runtime.ipc.call(manager, std.mem.asBytes(&event), &reply) catch {};
interface.registered_device_id = 0;
}
engine.tearDownDevice(dev);
}
/// 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 {
if (usb_device.device_descriptor.device_class == @intFromEnum(usb_ids.Class.hub)) return true;
for (usb_device.interfaces[0..usb_device.interface_count]) |interface| {
if (interface.class == @intFromEnum(usb_ids.Class.hub)) return true;
}
return false;
}
/// Tear down whatever was on `port` after an unplug: report each registered
/// interface as removed (the manager prunes the node, notifies watchers, and
/// stops the class driver's world honestly), then release the controller-side
/// device state (Disable Slot).
fn tearDownPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u32) void {
const usb_device = engine.deviceOnPort(port) orelse return;
std.log.info("port {d} disconnected", .{port});
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
if (interface.registered_device_id == 0) continue;
const event = protocol.ChildRemoved{
.parent = controller_id,
.bus_address = (@as(u64, port) << 8) | interface.number,
};
var reply: [protocol.message_maximum]u8 = undefined;
_ = runtime.ipc.call(manager, std.mem.asBytes(&event), &reply) catch {
std.log.info("child-removed report for port {d} interface {d} failed", .{ port, interface.number });
};
interface.registered_device_id = 0;
}
engine.tearDownDevice(usb_device);
}
/// Register one interface as a resource-less child of the controller and report
/// it to the device manager. The identity is the packed USB class triple, so the
/// manager can match a class driver (HID keyboard, mouse, mass storage); the
/// registered device id becomes that driver's argv[1] assignment. Returns the
/// registered device id, or null if registration or the report failed.
fn reportInterface(manager: runtime.ipc.Handle, port: u32, interface: library.InterfaceInfo) ?u64 {
const identity = usb_ids.packTriple(interface.class, interface.subclass, interface.protocol);
// A USB device is reached through its controller, not by MMIO, so the child
// carries no resources; register() allows that. Its bus-local identity — the
// (port, interface) address, written as a short "P<port>I<interface>" tag in
// the hid field — makes each interface a distinct kernel node (the register
// dedup keys on class/pci_class/hid/resources, all otherwise identical here)
// and keeps re-registration idempotent across a bus restart: the same port
// and interface always map back to the same device id.
var descriptor = std.mem.zeroes(device.DeviceDescriptor);
descriptor.class = @intFromEnum(device.DeviceClass.usb_device);
descriptor.pci_class = device.no_pci_class;
descriptor.resource_count = 0;
var hid_buffer: [8]u8 = undefined;
const hid_text = std.fmt.bufPrint(&hid_buffer, "P{d}I{d}", .{ port, interface.number }) catch "";
descriptor.hid_len = hid_text.len;
@memcpy(descriptor.hid[0..hid_text.len], hid_text);
const registered = device.register(controller_id, &descriptor) orelse {
std.log.info("register refused for port {d} interface {d}", .{ port, interface.number });
return null;
};
const report = protocol.ChildAdded{
.parent = controller_id,
.bus_address = (@as(u64, port) << 8) | interface.number,
.identity = identity,
.device_id = registered,
};
var reply: [protocol.message_maximum]u8 = undefined;
_ = runtime.ipc.call(manager, std.mem.asBytes(&report), &reply) catch {
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}", .{
port,
interface.number,
interface.class,
interface.subclass,
interface.protocol,
registered,
});
return registered;
}
/// Serve the USB transfer protocol: a class driver opens its device, then issues
/// control / interrupt-subscribe / bulk requests against it.
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
_ = sender;
if (message.len < 4) return 0;
const operation = std.mem.readInt(u32, message[0..4], .little);
return switch (operation) {
@intFromEnum(transfer.Operation.open) => handleOpen(message, reply, capability),
@intFromEnum(transfer.Operation.control) => handleControl(message, reply),
@intFromEnum(transfer.Operation.interrupt_subscribe) => handleSubscribe(message, reply),
@intFromEnum(transfer.Operation.bulk) => handleBulk(message, reply),
else => 0,
};
}
fn writeReply(reply: []u8, value: anytype) usize {
const bytes = std.mem.asBytes(&value);
@memcpy(reply[0..bytes.len], bytes);
return bytes.len;
}
/// open: resolve the assigned device id to an interface, remember the caller's
/// endpoint (for interrupt reports), and answer with a device token + the
/// interface's endpoints so the class driver need not re-read the config.
fn handleOpen(message: []const u8, reply: []u8, capability: ?runtime.ipc.Handle) usize {
if (message.len < @sizeOf(transfer.OpenRequest)) return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
const request = std.mem.bytesToValue(transfer.OpenRequest, message[0..@sizeOf(transfer.OpenRequest)]);
const engine = if (controller) |*c| c else return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
const found = engine.findInterface(request.device_id) orelse return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
if (capability) |endpoint| recordOpen(request.device_id, endpoint);
var open_reply = transfer.OpenReply{
.status = 0,
.endpoint_count = found.interface.endpoint_count,
.device_token = request.device_id,
.interface_class = found.interface.class,
.interface_subclass = found.interface.subclass,
.interface_protocol = found.interface.protocol,
.interface_number = found.interface.number,
};
const count = @min(found.interface.endpoint_count, transfer.max_reported_endpoints);
for (found.interface.endpoints[0..count], 0..) |endpoint, index| {
open_reply.endpoints[index] = .{
.address = endpoint.address,
.transfer_type = endpoint.transfer_type,
.max_packet_size = endpoint.max_packet_size,
.interval = endpoint.interval,
};
}
return writeReply(reply, open_reply);
}
/// control: one EP0 control transfer, small data inline both ways.
fn handleControl(message: []const u8, reply: []u8) usize {
if (message.len < @sizeOf(transfer.ControlRequest)) return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
const request = std.mem.bytesToValue(transfer.ControlRequest, message[0..@sizeOf(transfer.ControlRequest)]);
const engine = if (controller) |*c| c else return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
const setup = std.mem.bytesToValue(usb_abi.Request, &request.setup);
const direction_in = request.direction_in != 0;
const data_length = @min(request.data_length, transfer.max_inline_data);
var data: [transfer.max_inline_data]u8 = undefined;
if (!direction_in) @memcpy(data[0..data_length], request.data[0..data_length]);
const ok = engine.controlTransfer(found.device, setup, data[0..data_length], direction_in);
var control_reply = transfer.ControlReply{ .status = if (ok) 0 else -1, .actual_length = if (ok) data_length else 0 };
if (ok and direction_in) @memcpy(control_reply.data[0..data_length], data[0..data_length]);
return writeReply(reply, control_reply);
}
/// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously.
fn handleSubscribe(message: []const u8, reply: []u8) usize {
if (message.len < @sizeOf(transfer.InterruptSubscribeRequest)) return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
const request = std.mem.bytesToValue(transfer.InterruptSubscribeRequest, message[0..@sizeOf(transfer.InterruptSubscribeRequest)]);
const engine = if (controller) |*c| c else return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
const report_endpoint = reportEndpointFor(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
const ok = engine.subscribeInterrupt(found.device, endpoint, request.device_token, report_endpoint);
return writeReply(reply, transfer.InterruptSubscribeReply{ .status = if (ok) 0 else -1 });
}
/// bulk: one bulk transfer to/from the class driver's own DMA buffer (by physical
/// address), so sector-sized data never crosses IPC.
fn handleBulk(message: []const u8, reply: []u8) usize {
if (message.len < @sizeOf(transfer.BulkRequest)) return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
const request = std.mem.bytesToValue(transfer.BulkRequest, message[0..@sizeOf(transfer.BulkRequest)]);
const engine = if (controller) |*c| c else return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
const transferred = engine.bulkTransfer(found.device, endpoint, request.physical_address, request.length);
return writeReply(reply, transfer.BulkReply{ .status = if (transferred != null) 0 else -1, .actual_length = transferred orelse 0 });
}
/// The poll timer landed: drain any interrupt reports off the event ring and push
/// each to the class driver that subscribed, then re-arm the timer.
fn onNotification(badge: u64) void {
if (badge & runtime.ipc.notify_timer_bit == 0) return;
if (controller) |*engine| {
engine.pump();
while (engine.takePortChange()) |port| {
const manager = manager_handle orelse break;
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);
}
}
// 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.
// Cap per tick: even if a hub's change bits refuse to clear, the driver
// must not spin here — it services a bounded batch and yields to the
// next tick (and to storage, input, everything else).
var serviced: u32 = 0;
while (engine.takeHubChange()) |change| {
const manager = manager_handle orelse break;
bringUpBehindHub(manager, engine, change.hub, change.port);
serviced += 1;
if (serviced >= 32) break;
}
while (engine.takeReport()) |report| {
var message = transfer.InterruptReport{
.device_token = report.device_token,
.endpoint_address = report.endpoint_address,
.length = @intCast(@min(report.length, transfer.max_report_data)),
};
const n = @min(report.length, transfer.max_report_data);
@memcpy(message.data[0..n], report.data[0..n]);
_ = runtime.ipc.send(report.report_endpoint, std.mem.asBytes(&message));
}
}
_ = runtime.system.timerOnce(service_endpoint, poll_interval_ms);
}
pub fn main(init: runtime.process.Init) void {
const argument = init.arguments.get(1) orelse {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: missing controller device id (argv[1])\n");
return;
};
controller_id = std.fmt.parseInt(u64, argument, 10) catch {
std.log.info("malformed controller device id '{s}'", .{argument});
return;
};
runtime.service.run(transfer.message_maximum, .{
.service = .usb_bus,
.init = initialise,
.on_message = onMessage,
.on_notification = onNotification,
});
}