Files
danos/system/drivers/usb-xhci-bus/usb-xhci-bus.zig
T
Daniel Samson ea470afe84 runtime: consolidate the device-manager hello into runtime.device_manager
Every supervised driver owes the device manager a hello at startup, and
the retry-lookup-call-check for it had been copied into five drivers:
usb.helloManager (misfiled in the USB client) plus hand-rolled twins in
display, virtio-gpu, pci-bus, and usb-xhci-bus.

Extract it once as a runtime client, runtime.device_manager.hello(role,
device_id) ?Handle — returns the manager endpoint (bus drivers keep it to
report children through), null when there is no manager or it refused the
handshake, and logs the outcome itself so each call site is one line.

Also correct two roles while collapsing their calls: virtio-gpu and the
display driver each claim one PCI function and report no children, so they
are Role.device, not Role.bus. The manager ignores role today, so this is
cosmetic, but it matches the protocol's own definition (bus = reports
children via child_added).

Behavior-preserving otherwise: pci-bus and usb-xhci-bus move their hello
logging from raw serial writes to std.log, which the kernel renders with
the same "<path>: " prefix, so driver-restart still matches
"usb-xhci-bus: hello acknowledged". zig build clean; 8 QEMU cases pass
(driver-restart, pci-scan, usb-hid, usb-storage, virtio-gpu,
display-reattach, device-list, device-manager).
2026-07-22 19:27:20 +01:00

575 lines
29 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: bus — we enumerate USB ports and report the devices
// behind them), inside the manager's hello deadline. Keep the handle: the
// tick's hot-plug dispatch reports through it.
const handle = runtime.device_manager.hello(.bus, controller_id) orelse return false;
manager_handle = handle;
scanPorts(handle);
// 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;
// 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");
return;
};
std.log.info("{d} root-hub ports", .{engine.max_ports});
var port: u32 = 1;
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");
engine.dumpPortTopology(); // help diagnose an empty scan: the xECP map + raw PORTSC
}
}
/// 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;
}
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,
});
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);
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);
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;
}
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,
});
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}: {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,
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();
// 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;
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,
});
}