219 lines
11 KiB
Zig
219 lines
11 KiB
Zig
//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or
|
||
//! mass-storage driver) says to the xHCI bus driver over `/protocol/usb-transfer`
|
||
//! to drive its device. The class driver owns no hardware — it reaches its device
|
||
//! entirely through these packets, the way a PS/2 keyboard driver reaches the
|
||
//! 8042 through the ps2-bus.
|
||
//!
|
||
//! Defined through the envelope (docs/os-development/protocol-namespace.md), so
|
||
//! every packet begins with the folded `Header`. **`Header.target` is the device
|
||
//! token** — the per-open handle the bus driver hands back, which every request
|
||
//! but `open` addressed through a `device_token` field of its own before the
|
||
//! rebase. `open` itself addresses the *assigned device id*, because that is what
|
||
//! the caller has before there is a token.
|
||
//!
|
||
//! The shape:
|
||
//! - **open** (a capability-passing call): the class driver hands over its own
|
||
//! endpoint (for asynchronous interrupt reports); the target is its assigned
|
||
//! device id, and the reply carries a `device_token` plus its interface's
|
||
//! endpoints.
|
||
//! - **control / bulk** (synchronous calls): one transfer, answered when it
|
||
//! completes. Control data travels **in the packet's tail** in both
|
||
//! directions (descriptors, HID/MSC class requests are all small), so the
|
||
//! fixed parts stay tiny and `Status.len` is the transferred length — the
|
||
//! envelope's own field for "how many bytes follow", which is precisely what
|
||
//! the old `actual_length` said. Bulk data travels by **physical address** —
|
||
//! the class driver's own `dma_alloc`'d buffer — so a 512-byte sector never
|
||
//! has to cross the packet floor.
|
||
//! - **interrupt_subscribe** (synchronous): arm periodic IN polling of an
|
||
//! interrupt endpoint; each report the device produces is then pushed to the
|
||
//! class driver's endpoint as an asynchronous `interrupt_report` event.
|
||
//! It stays one of **this protocol's own verbs**, not the reserved
|
||
//! `subscribe`: the reserved verb means "push me this provider's events" and
|
||
//! carries the subscriber's endpoint, while this names one endpoint address
|
||
//! on one device and a poll length, and the endpoint it pushes to was handed
|
||
//! over at `open`. Same word, different contract.
|
||
//! - **dma_attach**: a class driver hands the controller a DMA-region
|
||
//! capability (riding the call's cap slot) so the controller binds that
|
||
//! buffer into its IOMMU domain and may then DMA to the physical addresses
|
||
//! inside it. Needed once per buffer the class driver will name in a `bulk`
|
||
//! transfer (its own, or one forwarded to it).
|
||
//!
|
||
//! Single controller assumption: one provider serves QEMU's one xHCI. A
|
||
//! multi-controller machine would need the controller in the target (or the
|
||
//! spawner wiring each class driver its own channel); noted, not built.
|
||
|
||
const std = @import("std");
|
||
const envelope = @import("envelope");
|
||
|
||
/// The largest control-transfer data stage. It rides the packet's tail, so the
|
||
/// bound is the call floor less the header and the fixed request part — derived
|
||
/// rather than declared, which is what keeps it honest when a field moves.
|
||
pub const max_inline_data: usize = envelope.packet_maximum - envelope.prefix_size - @sizeOf(Control);
|
||
|
||
/// The largest interrupt report pushed asynchronously. An event packet is the
|
||
/// header plus the payload within 64 bytes, so this is what is left after the
|
||
/// report's own four bytes of framing: boot keyboard reports are 8 bytes, boot
|
||
/// mouse reports 3–4, and the whole HID boot vocabulary fits many times over.
|
||
/// A device that produces more has its report truncated, never split.
|
||
pub const max_report_data: usize = 40;
|
||
|
||
/// Endpoints per interface reported back in an open reply (a boot HID interface
|
||
/// has one interrupt endpoint, a mass-storage interface two bulk endpoints).
|
||
pub const max_reported_endpoints: usize = 4;
|
||
|
||
/// The endpoint facts a class driver needs, lifted from the endpoint descriptor
|
||
/// the bus driver already parsed during enumeration.
|
||
pub const Endpoint = extern struct {
|
||
/// EndpointDescriptor address: direction in bit 7, number in bits 3:0.
|
||
address: u8,
|
||
/// 0 control, 1 isochronous, 2 bulk, 3 interrupt.
|
||
transfer_type: u8,
|
||
max_packet_size: u16,
|
||
interval: u8,
|
||
reserved: [3]u8 = .{ 0, 0, 0 },
|
||
};
|
||
|
||
// --- the per-operation request and reply parts ------------------------------
|
||
//
|
||
// Each names the bytes AFTER the prefix. Nothing here carries an operation or a
|
||
// device token: those are the packet header's, folded in once. No reply carries
|
||
// a status either — that is the `Status` every reply begins with.
|
||
|
||
/// The answer to `open`: the token every later packet puts in `Header.target`,
|
||
/// the interface's class triple (a sanity check), and its endpoints.
|
||
pub const Opened = extern struct {
|
||
device_token: u64,
|
||
endpoint_count: u32,
|
||
interface_class: u8,
|
||
interface_subclass: u8,
|
||
interface_protocol: u8,
|
||
interface_number: u8,
|
||
endpoints: [max_reported_endpoints]Endpoint = [_]Endpoint{.{ .address = 0, .transfer_type = 0, .max_packet_size = 0, .interval = 0 }} ** max_reported_endpoints,
|
||
};
|
||
|
||
/// `control`: one EP0 control transfer on `Header.target`. `setup` is a bit-cast
|
||
/// `usb_abi.Request`. For an OUT transfer the data stage is the request's tail;
|
||
/// for an IN transfer it comes back as the reply's tail, and `Status.len` is how
|
||
/// much of it arrived.
|
||
pub const Control = extern struct {
|
||
setup: [8]u8,
|
||
/// 1 = device-to-host (IN), 0 = host-to-device (OUT).
|
||
direction_in: u8,
|
||
_padding: u8 = 0,
|
||
/// Bytes of data stage: what an IN transfer asks for, and what an OUT
|
||
/// transfer's tail carries.
|
||
data_length: u16,
|
||
_padding2: u32 = 0,
|
||
};
|
||
|
||
/// `interrupt_subscribe`: begin periodic IN polling of an interrupt endpoint of
|
||
/// `Header.target`. Each report the device returns is pushed to the endpoint the
|
||
/// caller handed over at `open`, as an `interrupt_report` event.
|
||
pub const InterruptSubscribe = extern struct {
|
||
endpoint_address: u8,
|
||
_padding: u8 = 0,
|
||
/// Bytes to request per poll (the endpoint's max packet size).
|
||
max_length: u16,
|
||
};
|
||
|
||
/// `bulk`: one bulk IN or OUT transfer on `Header.target`. `physical_address` is
|
||
/// the class driver's own `dma_alloc`'d buffer — the controller DMAs straight
|
||
/// to/from it, so the bulk data never crosses IPC. `endpoint_address`'s bit 7
|
||
/// selects IN vs OUT.
|
||
pub const Bulk = extern struct {
|
||
physical_address: u64,
|
||
length: u32,
|
||
endpoint_address: u8,
|
||
_padding: u8 = 0,
|
||
_padding2: u16 = 0,
|
||
};
|
||
|
||
/// How many bytes a bulk transfer actually moved. It cannot ride `Status.len`
|
||
/// the way a control transfer's does: nothing follows a bulk reply, because the
|
||
/// data went to the caller's DMA buffer rather than into the packet.
|
||
pub const Transferred = extern struct { actual_length: u32 };
|
||
|
||
/// One asynchronous interrupt report, pushed to the endpoint the class driver
|
||
/// handed over at `open`. The device it came from is `Header.target`.
|
||
pub const InterruptReport = extern struct {
|
||
endpoint_address: u8,
|
||
length: u8,
|
||
_padding: u16 = 0,
|
||
data: [max_report_data]u8 = [_]u8{0} ** max_report_data,
|
||
};
|
||
|
||
pub const Protocol = envelope.Define(.{
|
||
.name = "usb-transfer",
|
||
.version = 1,
|
||
.operations = &.{
|
||
// open: the target is the interface's assigned device id (its argv[1]),
|
||
// and the class driver's receive endpoint rides as the capability.
|
||
.{ .name = "open", .reply = Opened },
|
||
.{ .name = "control", .request = Control },
|
||
.{ .name = "interrupt_subscribe", .request = InterruptSubscribe },
|
||
.{ .name = "bulk", .request = Bulk, .reply = Transferred },
|
||
// dma_attach: the region capability rides the call's cap slot; the
|
||
// target says which caller's device is attaching, so there is nothing
|
||
// left for a body to carry.
|
||
.{ .name = "dma_attach" },
|
||
},
|
||
.events = &.{
|
||
.{ .name = "interrupt_report", .payload = InterruptReport },
|
||
},
|
||
});
|
||
|
||
pub const Operation = Protocol.Operation;
|
||
pub const Event = Protocol.Event;
|
||
|
||
/// What both sides size their buffers to — the call floor, as every protocol does.
|
||
pub const message_maximum: usize = Protocol.message_maximum;
|
||
|
||
test "the budgets, re-verified by Define rather than by hand" {
|
||
// What the hand-rolled comptime asserts used to say, now said by `Define`
|
||
// — and counting the header, which the old checks did not.
|
||
try std.testing.expectEqual(@as(usize, 224), max_inline_data);
|
||
try std.testing.expect(Protocol.request_maximum <= envelope.packet_maximum);
|
||
try std.testing.expect(Protocol.reply_maximum <= envelope.packet_maximum);
|
||
// The report was 48 bytes of data in a 64-byte struct that had no room left
|
||
// for a header. Folding the device token into the target and trimming the
|
||
// data to 40 leaves the whole packet at 60 of the 64-byte push floor.
|
||
try std.testing.expectEqual(@as(usize, 44), @sizeOf(InterruptReport));
|
||
try std.testing.expectEqual(@as(usize, 60), Protocol.event_maximum);
|
||
try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum);
|
||
}
|
||
|
||
test "the verb numbering, and the device token in the header" {
|
||
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.open));
|
||
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.control));
|
||
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.interrupt_subscribe));
|
||
try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Operation.bulk));
|
||
try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Operation.dma_attach));
|
||
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.interrupt_report));
|
||
|
||
var buffer: [message_maximum]u8 = undefined;
|
||
const packet = Protocol.encodeRequest(.control, 9, .{
|
||
.setup = .{ 0, 6, 0, 1, 0, 0, 18, 0 },
|
||
.direction_in = 1,
|
||
.data_length = 18,
|
||
}, &.{}, &buffer).?;
|
||
try std.testing.expectEqual(@as(u64, 9), envelope.headerOf(packet).?.target);
|
||
try std.testing.expectEqual(@as(u16, 18), Protocol.decodeRequest(.control, packet).?.data_length);
|
||
}
|
||
|
||
test "a control OUT carries its data stage as the packet's tail" {
|
||
var buffer: [message_maximum]u8 = undefined;
|
||
const payload = [_]u8{ 1, 2, 3, 4 };
|
||
const packet = Protocol.encodeRequest(.control, 5, .{
|
||
.setup = .{ 0x21, 11, 0, 0, 0, 0, 4, 0 },
|
||
.direction_in = 0,
|
||
.data_length = payload.len,
|
||
}, &payload, &buffer).?;
|
||
try std.testing.expectEqualSlices(u8, &payload, Protocol.requestTail(.control, packet));
|
||
|
||
// And the answer to an IN: the bytes follow the (empty) fixed reply part,
|
||
// with `Status.len` counting exactly them.
|
||
const answered = Protocol.encodeReply(.control, 0, {}, &payload, &buffer).?;
|
||
try std.testing.expectEqual(@as(u32, payload.len), envelope.statusOf(answered).?.len);
|
||
try std.testing.expectEqualSlices(u8, &payload, Protocol.replyTail(.control, answered));
|
||
}
|