//! 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]: //! if (device_manager.hello(.device, id) == null) return; // meet the spawn deadline //! var device = usb.open(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 //! //! 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 channel = @import("channel"); const envelope = @import("envelope"); const ipc = @import("ipc"); const time = @import("time"); 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; } /// 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); } /// Open `/protocol/usb-transfer` and, on that channel, open the device with the /// assigned id, handing over a freshly created endpoint for asynchronous interrupt /// reports. Retries while the bus is still coming up (a class driver races the bus /// driver at boot). Two opens, deliberately: the first names the contract, the /// second names an object within it. pub fn open(device_id: u64) ?Device { var attempts: usize = 0; const bus = while (attempts < 100) : (attempts += 1) { if (channel.openEndpoint("usb-transfer")) |handle| break handle; time.sleepMillis(20); } else return null; 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; }