Files
danos/library/device/usb/usb.zig
T
Daniel Samson a44b397bed protocols: attach gets its reverse — block detach, usb-transfer dma_detach
The kernel was always symmetric (dma_bind 51 / dma_unbind 52); the two
protocols that forward an attachment up the stack were one-way, so a live
client could grant a device reach into its buffer but never revoke it while
alive — exactly the one-way lifecycle the storage architecture's enforcement
section forbids. Death stays the mechanical backstop; detach is the living
process's path.

Both verbs are appended, so every existing number holds. The shape mirrors
attach precisely: the same region capability rides the cap slot again — the
kernel matches the region, so no layer retains anything between the calls
(the bus never kept the handle; now it never needs to).

fat's bring-up does attach -> detach -> attach, exercising both verbs
through the whole chain (fat -> storage -> bus -> kernel) on every boot: a
broken detach fails every fat case instead of lying dormant until the first
buffer replacement. Honest scope: the round trip proves the plumbing; unbind
semantics are the kernel iommu tests' (map/unmap/translationOf); the full
composition (detach then DMA faults) is a future iommu-fault extension.
2026-08-09 15:18:21 +01:00

205 lines
10 KiB
Zig

//! USB class-driver client: the helper a keyboard, mouse, or mass-storage driver
//! uses to reach its device through the xHCI bus driver, so it never hand-rolls
//! the transfer-protocol IPC. Layered over `ipc` and the shared
//! `usb-transfer-protocol` wire format, the way `input.zig` layers over the input
//! service and `device.zig` over the raw device calls.
//!
//! A class driver, spawned with its interface's assigned device id as argv[1]:
//! const bus = device_manager.helloForChannel(.device, id) orelse return;
//! var device = usb.open(bus, id) orelse return; // open + get its endpoints
//! _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors
//! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously
//! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop
//!
//! The bus channel arrives from the device manager's hello — routed to THIS
//! device's controller by lineage — never from a registry name; several bus
//! processes provide this contract on a multi-controller machine.
//!
//! Reports are delivered to `device.endpoint` as asynchronous `interrupt_report`
//! event packets, decoded with `reportOf` (the class driver runs a bare `replyWait`
//! loop to read them, because the service harness drops buffered-message payloads
//! — see service.zig).
//!
//! Every packet this file lays down is an envelope packet: the verb and the
//! device token in the folded `Header`, the transfer's own fields after it, and
//! a control transfer's data stage in the tail.
const std = @import("std");
const envelope = @import("envelope");
const ipc = @import("ipc");
const usb_transfer_protocol = @import("usb-transfer-protocol");
const Protocol = usb_transfer_protocol.Protocol;
/// The USB chapter-9 wire ABI and the class taxonomy, re-exported so a class driver reaches
/// the whole USB domain through its one `usb` import (`usb.abi.getDescriptor`, `usb.ids.Class`).
pub const abi = @import("usb-abi");
pub const ids = @import("usb-ids");
pub const Endpoint = usb_transfer_protocol.Endpoint;
pub const InterruptReport = usb_transfer_protocol.InterruptReport;
pub const max_report_data = usb_transfer_protocol.max_report_data;
// Endpoint transfer types (EndpointDescriptor attributes), for `findEndpoint`.
pub const transfer_type_bulk: u8 = 2;
pub const transfer_type_interrupt: u8 = 3;
/// An opened USB device: the bus endpoint to send requests to, this driver's own
/// endpoint that reports arrive on, the device token, and the interface's
/// endpoints (so a driver need not re-read the configuration descriptor).
pub const Device = struct {
bus: ipc.Handle,
endpoint: ipc.Handle,
token: u64,
class: u8,
subclass: u8,
protocol_code: u8,
interface_number: u8,
endpoint_count: usize = 0,
endpoints: [usb_transfer_protocol.max_reported_endpoints]Endpoint = undefined,
/// The interface's first endpoint of the given transfer type and direction
/// (`transfer_type_bulk` / `transfer_type_interrupt`), or null.
pub fn findEndpoint(self: *const Device, transfer_type: u8, direction_in: bool) ?Endpoint {
for (self.endpoints[0..self.endpoint_count]) |endpoint| {
if (endpoint.transfer_type == transfer_type and (endpoint.address & 0x80 != 0) == direction_in) return endpoint;
}
return null;
}
/// One request at the bus driver, addressing this device by its token — the
/// packet's `Header.target`, so no request body ever names the device again.
/// Null covers both a failed transport and a refusal: a class driver has the
/// same recourse either way.
fn call(
self: *Device,
comptime operation: Protocol.Operation,
request: Protocol.RequestOf(operation),
tail: []const u8,
capability: ?ipc.Handle,
reply: []u8,
) ?[]u8 {
var packet: [usb_transfer_protocol.message_maximum]u8 = undefined;
const framed = Protocol.encodeRequest(operation, self.token, request, tail, &packet) orelse return null;
const answer = ipc.callCap(self.bus, framed, reply, capability) catch return null;
const status = envelope.statusOf(reply[0..answer.len]) orelse return null;
if (status.status != 0) return null;
return reply[0..answer.len];
}
/// The data stage rides the tail in both directions, so the answer's length
/// *is* the transferred length — `Status.len`, which the envelope stamps.
fn controlTransfer(self: *Device, setup: [8]u8, direction_in: bool, data: []u8) ?usize {
if (data.len > usb_transfer_protocol.max_inline_data) return null;
const outgoing: []const u8 = if (direction_in) &.{} else data;
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
const answered = self.call(.control, .{
.setup = setup,
.direction_in = @intFromBool(direction_in),
.data_length = @intCast(data.len),
}, outgoing, null, &reply) orelse return null;
const returned = Protocol.replyTail(.control, answered);
const actual = @min(returned.len, data.len);
if (direction_in and actual > 0) @memcpy(data[0..actual], returned[0..actual]);
return actual;
}
/// A control transfer with no data stage (SET_PROTOCOL, SET_IDLE, ...). The
/// `setup` is a bit-cast `usb_abi.Request`.
pub fn controlOut(self: *Device, setup: [8]u8) bool {
return self.controlTransfer(setup, false, &.{}) != null;
}
/// A device-to-host control transfer, returning the bytes read into `out`.
pub fn controlIn(self: *Device, setup: [8]u8, out: []u8) ?usize {
return self.controlTransfer(setup, true, out);
}
/// Begin periodic IN polling of an interrupt endpoint; reports flow back to
/// `self.endpoint` as asynchronous `InterruptReport` messages.
pub fn subscribeInterrupt(self: *Device, endpoint_address: u8, max_length: u16) bool {
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
return self.call(.interrupt_subscribe, .{
.endpoint_address = endpoint_address,
.max_length = max_length,
}, &.{}, null, &reply) != null;
}
/// Hand the controller a DMA-region capability (`handle` — from a `shareable`
/// dma_alloc, or forwarded from another process) so it binds that buffer into its
/// IOMMU domain. Must be called for every buffer whose physical address this device
/// will name in a `bulk` transfer, before the transfer. Harmless (and a no-op
/// success) when no IOMMU is enforcing. Returns false on failure.
pub fn attachDma(self: *Device, handle: ipc.Handle) bool {
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
return self.call(.dma_attach, {}, &.{}, handle, &reply) != null;
}
/// The reverse of `attachDma`: unbind the buffer from the controller's IOMMU
/// domain. The same region capability rides again — the kernel matches the
/// region, so neither side kept state between the two calls. Do not name the
/// buffer's physical address in any transfer after this.
pub fn detachDma(self: *Device, handle: ipc.Handle) bool {
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
return self.call(.dma_detach, {}, &.{}, handle, &reply) != null;
}
/// One bulk transfer (IN or OUT per `endpoint_address`'s direction bit) to or
/// from the caller's own DMA buffer at `physical`. Returns the bytes moved.
pub fn bulk(self: *Device, endpoint_address: u8, physical: u64, length: u32) ?u32 {
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
const answered = self.call(.bulk, .{
.physical_address = physical,
.length = length,
.endpoint_address = endpoint_address,
}, &.{}, null, &reply) orelse return null;
return (Protocol.decodeReply(.bulk, answered) orelse return null).actual_length;
}
};
/// Decode one asynchronous interrupt report out of a packet that arrived on the
/// class driver's own endpoint. Null when it is not one — a stray message, or a
/// packet too short to carry the report it names. The device it came from is the
/// packet's `Header.target`, which a single-device class driver never has to read.
pub fn reportOf(packet: []const u8) ?InterruptReport {
const event = Protocol.eventOf(packet) orelse return null;
if (event != .interrupt_report) return null;
return Protocol.decodeEvent(.interrupt_report, packet);
}
/// On `bus` — the channel to this device's own controller, handed to the class
/// driver by the device manager's hello (establishment by lineage,
/// communication.md "Establishment: two planes") — open the device with the
/// assigned id, handing over a freshly created endpoint for asynchronous
/// interrupt reports. The channel is never found by name: one machine carries
/// several controllers, several processes provide this contract, and only the
/// manager knows which one reported this device.
pub fn open(bus: ipc.Handle, device_id: u64) ?Device {
const endpoint = ipc.createIpcEndpoint() orelse return null;
// The assigned device id is the target: it is what the caller has before a
// token exists, and the token the reply hands back addresses every packet
// after this one.
var packet: [usb_transfer_protocol.message_maximum]u8 = undefined;
const framed = Protocol.encodeRequest(.open, device_id, {}, &.{}, &packet) orelse return null;
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
const result = ipc.callCap(bus, framed, &reply, endpoint) catch return null;
const answered = reply[0..result.len];
const status = envelope.statusOf(answered) orelse return null;
if (status.status != 0) return null;
const open_reply = Protocol.decodeReply(.open, answered) orelse return null;
var device = Device{
.bus = bus,
.endpoint = endpoint,
.token = open_reply.device_token,
.class = open_reply.interface_class,
.subclass = open_reply.interface_subclass,
.protocol_code = open_reply.interface_protocol,
.interface_number = open_reply.interface_number,
.endpoint_count = @min(open_reply.endpoint_count, usb_transfer_protocol.max_reported_endpoints),
};
for (0..device.endpoint_count) |index| device.endpoints[index] = open_reply.endpoints[index];
return device;
}