682 lines
35 KiB
Zig
682 lines
35 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 device = @import("driver");
|
|
const ipc = @import("ipc");
|
|
const process = @import("process");
|
|
const service = @import("service");
|
|
const time = @import("time");
|
|
const input = @import("input-client");
|
|
const device_manager = @import("driver");
|
|
const memory = @import("memory");
|
|
const logging = @import("logging");
|
|
const device_manager_protocol = @import("device-manager-protocol");
|
|
const usb_ids = @import("usb-ids");
|
|
const usb_abi = @import("usb-abi");
|
|
const usb_transfer_protocol = @import("usb-transfer-protocol");
|
|
const library = @import("usb-xhci-library.zig");
|
|
const pci = @import("pci");
|
|
|
|
/// 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, MSI notifications, and the poll/reconcile timer all arrive.
|
|
var service_endpoint: ipc.Handle = 0;
|
|
|
|
/// How often the driver drains the event ring for interrupt reports (~125 Hz) when
|
|
/// polling, re-armed each tick. Frequent enough for responsive input.
|
|
const poll_interval_ms: u64 = 8;
|
|
|
|
/// The timer interval in MSI mode: the ring is drained at interrupt time, and the tick
|
|
/// only reconciles root ports (real hardware delivers late USB2 companion-hub debounce
|
|
/// with no reliable port-change event — see onNotification) and un-wedges a lost MSI
|
|
/// edge (edge-triggered, no kernel mask/ack: a missed IP clear stalls, never storms).
|
|
const reconcile_interval_ms: u64 = 250;
|
|
|
|
/// The controller's own descriptor, kept at file scope because `pci.Function` holds a
|
|
/// pointer to it for the whole bring-up.
|
|
var controller_descriptor: device.DeviceDescriptor = undefined;
|
|
|
|
/// Non-null iff MSI mode is active: the vector whose notification badge means "the
|
|
/// controller interrupted". Null means the 8 ms polling fallback is running.
|
|
var msi_vector: ?u32 = null;
|
|
|
|
fn timerInterval() u64 {
|
|
return if (msi_vector != null) reconcile_interval_ms else poll_interval_ms;
|
|
}
|
|
|
|
/// 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 = device_manager_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: 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 = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
|
_ = logging.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;
|
|
};
|
|
controller_descriptor = descriptor;
|
|
|
|
// 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 {
|
|
_ = logging.write("/system/drivers/usb-xhci-bus: mmio_map failed\n");
|
|
return false;
|
|
};
|
|
|
|
// Message-signalled interrupt setup comes BEFORE the controller bring-up, not
|
|
// after: Controller.init writes IMAN.IE, and QEMU's xhci only registers the MSI-X
|
|
// vector as in-use when that write happens with MSI-X already enabled (its
|
|
// intr_update callback early-outs on !msix_enabled, and msix_notify silently
|
|
// drops interrupts for an unused vector). Real hardware does not care about the
|
|
// order; QEMU requires it.
|
|
setupMsi();
|
|
|
|
// 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 {
|
|
_ = logging.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()) {
|
|
_ = logging.write("/system/drivers/usb-xhci-bus: command ring running (no-op ok)\n");
|
|
} else {
|
|
_ = logging.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 = device_manager.hello(.bus, controller_id) orelse return false;
|
|
manager_handle = handle;
|
|
|
|
scanPorts(handle);
|
|
|
|
// Arm the timer: in polling mode it drains the event ring; in MSI mode it is the
|
|
// slower port-reconcile/safety-net tick. Re-armed on each tick in onNotification.
|
|
_ = time.timerOnce(service_endpoint, timerInterval());
|
|
return true;
|
|
}
|
|
|
|
/// Switch the event ring from timer polling to message-signalled interrupts, if the
|
|
/// whole path is available: map the function's config space, bind a vector, program
|
|
/// the MSI capability — or, on an MSI-X-only function (QEMU's qemu-xhci is one: it
|
|
/// advertises MSI-X and PCIe but no plain MSI), entry 0 of the MSI-X table, which
|
|
/// takes the same kernel (address, data) pair (xHCI interrupter 0 raises vector 0).
|
|
/// Any step failing leaves `msi_vector` null and the 8 ms polling path exactly as it
|
|
/// was. The controller side needs nothing extra — IMAN.IE and USBCMD.INTE are already
|
|
/// set (see Controller.init: QEMU only writes runtime events with the interrupter
|
|
/// enabled).
|
|
///
|
|
/// After a supervised kill, the kernel drops the vector binding but the device still
|
|
/// has the interrupt enabled and fires the stale vector; the kernel EOIs it
|
|
/// harmlessly, and the respawned driver re-runs this with its fresh vector.
|
|
fn setupMsi() void {
|
|
var function = pci.Function.map(controller_id, &controller_descriptor) orelse {
|
|
std.log.info("config-space map failed; polling at {d} ms", .{poll_interval_ms});
|
|
return;
|
|
};
|
|
function.enableMemoryAndBusMaster();
|
|
const message = device.msiBind(controller_id, service_endpoint) orelse {
|
|
std.log.info("msi_bind unavailable; polling at {d} ms", .{poll_interval_ms});
|
|
return;
|
|
};
|
|
if (function.programMsi(message)) {
|
|
msi_vector = message.data;
|
|
std.log.info("msi active (vector {d}); reconcile tick at {d} ms", .{ message.data, reconcile_interval_ms });
|
|
return;
|
|
}
|
|
if (function.msix()) |table| {
|
|
if (table.programEntry(0, message) and table.unmaskEntry(0)) {
|
|
table.enable();
|
|
function.setInterruptDisable();
|
|
msi_vector = message.data;
|
|
std.log.info("msix active (vector {d}); reconcile tick at {d} ms", .{ message.data, reconcile_interval_ms });
|
|
return;
|
|
}
|
|
}
|
|
std.log.info("no msi/msi-x capability; polling at {d} ms", .{poll_interval_ms});
|
|
}
|
|
|
|
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: ?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: ipc.Handle) void {
|
|
const engine = if (controller) |*c| c else {
|
|
_ = logging.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) {
|
|
_ = logging.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: 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: 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: 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 = device_manager_protocol.ChildRemoved{
|
|
.parent = controller_id,
|
|
.bus_address = (@as(u64, key) << 8) | interface.number,
|
|
};
|
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
|
_ = 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: 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 = device_manager_protocol.ChildRemoved{
|
|
.parent = controller_id,
|
|
.bus_address = (@as(u64, port) << 8) | interface.number,
|
|
};
|
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
|
_ = 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: 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 = device_manager_protocol.ChildAdded{
|
|
.bus = @intFromEnum(device_manager_protocol.BusKind.usb),
|
|
.parent = controller_id,
|
|
.bus_address = (@as(u64, port) << 8) | interface.number,
|
|
.identity = identity,
|
|
.device_id = registered,
|
|
};
|
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
|
_ = 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;
|
|
};
|
|
// The devices.csv columns (bus=usb, and the class triple as base/class/prog_if)
|
|
// then the human-readable interface name — a would-be /system/configuration/devices.csv row read
|
|
// straight off the boot log.
|
|
std.log.info("port {d} interface {d} bus=usb base={X:0>2} class={X:0>2} prog_if={X:0>2} — {s} registered as device {d}", .{
|
|
port,
|
|
interface.number,
|
|
interface.class,
|
|
interface.subclass,
|
|
interface.protocol,
|
|
usb_ids.interfaceName(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: ?ipc.Handle) usize {
|
|
_ = sender;
|
|
if (message.len < 4) return 0;
|
|
const operation = std.mem.readInt(u32, message[0..4], .little);
|
|
return switch (operation) {
|
|
@intFromEnum(usb_transfer_protocol.Operation.open) => handleOpen(message, reply, capability),
|
|
@intFromEnum(usb_transfer_protocol.Operation.control) => handleControl(message, reply),
|
|
@intFromEnum(usb_transfer_protocol.Operation.interrupt_subscribe) => handleSubscribe(message, reply),
|
|
@intFromEnum(usb_transfer_protocol.Operation.bulk) => handleBulk(message, reply),
|
|
@intFromEnum(usb_transfer_protocol.Operation.dma_attach) => handleDmaAttach(message, reply, capability),
|
|
else => 0,
|
|
};
|
|
}
|
|
|
|
/// dma_attach: bind the class driver's DMA-region capability into the controller's IOMMU
|
|
/// domain, so the controller may DMA to the physical addresses inside that buffer. The
|
|
/// binding holds its own kernel reference, so the forwarded capability is closed here.
|
|
fn handleDmaAttach(message: []const u8, reply: []u8, capability: ?ipc.Handle) usize {
|
|
if (message.len < @sizeOf(usb_transfer_protocol.DmaAttachRequest)) return writeReply(reply, usb_transfer_protocol.DmaAttachReply{ .status = -1 });
|
|
const handle = capability orelse return writeReply(reply, usb_transfer_protocol.DmaAttachReply{ .status = -1 });
|
|
const ok = device.dmaBind(controller_id, handle);
|
|
_ = ipc.close(handle);
|
|
return writeReply(reply, usb_transfer_protocol.DmaAttachReply{ .status = if (ok) 0 else -1 });
|
|
}
|
|
|
|
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: ?ipc.Handle) usize {
|
|
if (message.len < @sizeOf(usb_transfer_protocol.OpenRequest)) return writeReply(reply, usb_transfer_protocol.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(usb_transfer_protocol.OpenRequest, message[0..@sizeOf(usb_transfer_protocol.OpenRequest)]);
|
|
const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.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, usb_transfer_protocol.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 = usb_transfer_protocol.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, usb_transfer_protocol.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(usb_transfer_protocol.ControlRequest)) return writeReply(reply, usb_transfer_protocol.ControlReply{ .status = -1, .actual_length = 0 });
|
|
const request = std.mem.bytesToValue(usb_transfer_protocol.ControlRequest, message[0..@sizeOf(usb_transfer_protocol.ControlRequest)]);
|
|
const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.ControlReply{ .status = -1, .actual_length = 0 });
|
|
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.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, usb_transfer_protocol.max_inline_data);
|
|
var data: [usb_transfer_protocol.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 = usb_transfer_protocol.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(usb_transfer_protocol.InterruptSubscribeRequest)) return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 });
|
|
const request = std.mem.bytesToValue(usb_transfer_protocol.InterruptSubscribeRequest, message[0..@sizeOf(usb_transfer_protocol.InterruptSubscribeRequest)]);
|
|
const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 });
|
|
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 });
|
|
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 });
|
|
const report_endpoint = reportEndpointFor(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 });
|
|
const ok = engine.subscribeInterrupt(found.device, endpoint, request.device_token, report_endpoint);
|
|
return writeReply(reply, usb_transfer_protocol.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(usb_transfer_protocol.BulkRequest)) return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 });
|
|
const request = std.mem.bytesToValue(usb_transfer_protocol.BulkRequest, message[0..@sizeOf(usb_transfer_protocol.BulkRequest)]);
|
|
const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 });
|
|
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 });
|
|
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 });
|
|
const transferred = engine.bulkTransfer(found.device, endpoint, request.physical_address, request.length);
|
|
return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = if (transferred != null) 0 else -1, .actual_length = transferred orelse 0 });
|
|
}
|
|
|
|
/// A timer tick or an MSI landed: drain the event ring, reconcile ports, and fan out.
|
|
/// The timer arm re-arms itself (8 ms drain when polling, 250 ms reconcile under MSI);
|
|
/// the MSI arm clears the interrupter's pending bit FIRST, then drains — so an event
|
|
/// arriving after the drain takes IP 0→1 and fires a fresh edge instead of being
|
|
/// swallowed until the reconcile tick.
|
|
fn onNotification(badge: u64) void {
|
|
if (badge & ipc.notify_timer_bit != 0) {
|
|
serviceController();
|
|
_ = time.timerOnce(service_endpoint, timerInterval());
|
|
return;
|
|
}
|
|
const vector = msi_vector orelse return;
|
|
if (badge & ~ipc.notify_badge_bit != vector) return;
|
|
if (controller) |*engine| engine.acknowledgeInterrupt();
|
|
serviceController();
|
|
}
|
|
|
|
/// Everything one servicing pass does, shared verbatim by the poll/reconcile tick and
|
|
/// the MSI notification: drain the event ring, reconcile root ports, service hub
|
|
/// changes, and push interrupt reports to their class drivers.
|
|
fn serviceController() void {
|
|
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 = usb_transfer_protocol.InterruptReport{
|
|
.device_token = report.device_token,
|
|
.endpoint_address = report.endpoint_address,
|
|
.length = @intCast(@min(report.length, usb_transfer_protocol.max_report_data)),
|
|
};
|
|
const n = @min(report.length, usb_transfer_protocol.max_report_data);
|
|
@memcpy(message.data[0..n], report.data[0..n]);
|
|
_ = ipc.send(report.report_endpoint, std.mem.asBytes(&message));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn main(init: process.Init) void {
|
|
const argument = init.arguments.get(1) orelse {
|
|
_ = logging.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;
|
|
};
|
|
service.run(usb_transfer_protocol.message_maximum, .{
|
|
.service = .usb_bus,
|
|
.init = initialise,
|
|
.on_message = onMessage,
|
|
.on_notification = onNotification,
|
|
});
|
|
}
|