Flash 419 lines
// usb_descriptors: byte-exact USB descriptor set + SETUP-packet decode
// for the DWC2 gadget. Pure data + pure functions — no MMIO, no extern —
// so it host-unit-tests with no hardware (mirrors src/mailbox.zig and
// src/sdhci_cmd.zig). The rpi4b driver (src/board/rpi4b/usb.zig) imports
// this as the named module "usb_descriptors"; the host-test build runs
// the tests at the bottom.
//
// CDC-ACM: the descriptor set below describes a CDC-ACM serial
// function — a Communications interface (class 2 / subclass ACM 2) carrying
// the Header / Call-Management / ACM / Union functional descriptors + an
// interrupt-IN notification endpoint, and a Data interface (class 0x0A) with
// bulk IN + bulk OUT. macOS binds AppleUSBCDCACM by interface class and
// creates /dev/tty.usbmodem<serial>. The bulk/interrupt endpoints are
// declared here but only hardware-configured by the data-path code in
// usb.zig; this module covers enumeration + the EP0 class requests.
const std = #import("std")
// --- Descriptor type codes (USB 2.0 §9.4, table 9-5). ---
pub const DESC_DEVICE u8 = 1
pub const DESC_CONFIG u8 = 2
pub const DESC_STRING u8 = 3
pub const DESC_INTERFACE u8 = 4
pub const DESC_ENDPOINT u8 = 5
pub const DESC_DEVICE_QUALIFIER u8 = 6
pub const DESC_OTHER_SPEED u8 = 7
// CDC class-specific descriptor type (USB CDC 1.1 §5.2.3): the functional
// descriptors carried inside the Communications interface.
pub const CS_INTERFACE u8 = 0x24
// --- Standard device requests (USB 2.0 §9.4, table 9-4). ---
pub const REQ_GET_STATUS u8 = 0x00
pub const REQ_CLEAR_FEATURE u8 = 0x01
pub const REQ_SET_FEATURE u8 = 0x03
pub const REQ_SET_ADDRESS u8 = 0x05
pub const REQ_GET_DESCRIPTOR u8 = 0x06
pub const REQ_SET_DESCRIPTOR u8 = 0x07
pub const REQ_GET_CONFIGURATION u8 = 0x08
pub const REQ_SET_CONFIGURATION u8 = 0x09
// --- CDC-ACM class-specific requests (USB CDC PSTN 1.2 §6.3). macOS issues
// these when an app opens the tty; the device must ACK them. ---
pub const REQ_SET_LINE_CODING u8 = 0x20
pub const REQ_GET_LINE_CODING u8 = 0x21
pub const REQ_SET_CONTROL_LINE_STATE u8 = 0x22
pub const REQ_SEND_BREAK u8 = 0x23
// Pinned identity: pid.codes vendor 0x1209, a throwaway test product id.
// macOS binds by class, so the exact pair is irrelevant for enumeration;
// any non-colliding pair works for the spike.
pub const VID u16 = 0x1209
pub const PID u16 = 0x0001
// Full-Speed EP0 max packet.
pub const EP0_MPS u16 = 64
// CDC line coding (USB CDC PSTN §6.3.11): dwDTERate (LE u32) + bCharFormat
// (stop bits) + bParityType + bDataBits. Default 115200 8N1 — cosmetic over
// USB (there is no real UART), but macOS reads it back via GET_LINE_CODING.
pub const line_coding_default = [7]u8{ 0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x08 }
// Device descriptor (18 bytes). bcdUSB 0x0200; CDC (Communications) class at
// the device level so macOS binds AppleUSBCDCACM via the comms interface;
// FS EP0 = 64.
pub const device_descriptor = [18]u8{
0x12, DESC_DEVICE,
0x00, 0x02, // bcdUSB = 0x0200
0x02, 0x00, // bDeviceClass = CDC (Communications); bDeviceSubClass = 0
0x00, 0x40, // bDeviceProtocol = 0; bMaxPacketSize0 = 64
0x09, 0x12, // idVendor = 0x1209 (little-endian)
0x01, 0x00, // idProduct = 0x0001 (little-endian)
0x00, 0x01, // bcdDevice = 0x0100
0x01, 0x02,
0x03, 0x01, // iManufacturer/iProduct/iSerial = 1/2/3; bNumConfigurations = 1
}
// CDC-ACM configuration block (67 bytes): config (9) + comms interface (9) +
// Header/Call-Mgmt/ACM/Union functional descriptors (5+5+4+5) + notify EP (7)
// + data interface (9) + bulk OUT (7) + bulk IN (7). Two interfaces; bus-
// powered, 100 mA. Endpoint map: EP1 IN = interrupt notify, EP2 IN/OUT = bulk.
pub const config_descriptor = [67]u8{
// configuration descriptor (9)
0x09, DESC_CONFIG,
0x43, 0x00, // wTotalLength = 67 (little-endian)
0x02, // bNumInterfaces = comms + data
0x01, // bConfigurationValue
0x00, // iConfiguration
0x80, // bmAttributes = bus-powered, no remote wakeup
0x32, // bMaxPower = 0x32 * 2 mA = 100 mA
// interface 0: Communications class (9)
0x09, DESC_INTERFACE,
0x00, // bInterfaceNumber = 0
0x00, // bAlternateSetting
0x01, // bNumEndpoints = 1 (interrupt-IN notification)
0x02, // bInterfaceClass = CDC (Communications)
0x02, // bInterfaceSubClass = Abstract Control Model
0x01, // bInterfaceProtocol = AT/V.25ter (0x00 also binds on macOS)
0x00, // iInterface
// CDC Header functional descriptor (5)
0x05, CS_INTERFACE,
0x00, // bDescriptorSubtype = Header
0x10, 0x01, // bcdCDC = 0x0110 (little-endian)
// CDC Call-Management functional descriptor (5)
0x05, CS_INTERFACE,
0x01, // bDescriptorSubtype = Call Management
0x00, // bmCapabilities = device does not handle call management
0x01, // bDataInterface = 1
// CDC Abstract-Control-Management functional descriptor (4)
0x04, CS_INTERFACE,
0x02, // bDescriptorSubtype = ACM
0x02, // bmCapabilities = Set/Get_Line_Coding + Set_Control_Line_State
// CDC Union functional descriptor (5)
0x05, CS_INTERFACE,
0x06, // bDescriptorSubtype = Union
0x00, // bControlInterface = 0 (comms)
0x01, // bSubordinateInterface0 = 1 (data)
// endpoint: notification (interrupt IN, EP1) (7)
0x07, DESC_ENDPOINT,
0x81, // bEndpointAddress = EP1 IN
0x03, // bmAttributes = interrupt
0x10, 0x00, // wMaxPacketSize = 16 (little-endian)
0x10, // bInterval = 16 (FS frames)
// interface 1: Data class (9)
0x09, DESC_INTERFACE,
0x01, // bInterfaceNumber = 1
0x00, // bAlternateSetting
0x02, // bNumEndpoints = 2 (bulk IN + bulk OUT)
0x0A, // bInterfaceClass = CDC Data
0x00, // bInterfaceSubClass
0x00, // bInterfaceProtocol
0x00, // iInterface
// endpoint: bulk OUT (EP2) (7)
0x07, DESC_ENDPOINT,
0x02, // bEndpointAddress = EP2 OUT
0x02, // bmAttributes = bulk
0x40, 0x00, // wMaxPacketSize = 64 (little-endian)
0x00, // bInterval
// endpoint: bulk IN (EP2) (7)
0x07, DESC_ENDPOINT,
0x82, // bEndpointAddress = EP2 IN
0x02, // bmAttributes = bulk
0x40, 0x00, // wMaxPacketSize = 64 (little-endian)
0x00, // bInterval
}
// String index 0 = supported-LANGID list. macOS requests index 0 during
// enumeration; a missing index 0 trips the whole enum. Single LANGID =
// 0x0409 (en-US).
pub const str_langid = [4]u8{ 0x04, DESC_STRING, 0x09, 0x04 }
// Build a UTF-16LE string descriptor at comptime from an ASCII literal.
fn utf16leStringDesc(comptime s []u8) [2 + 2 * s.len]u8 {
var buf [2 + 2 * s.len]u8 = undefined
buf[0] = #intCast(buf.len) // bLength
buf[1] = DESC_STRING
inline for c, i in s {
buf[2 + i * 2] = c
buf[2 + i * 2 + 1] = 0x00
}
return buf
}
pub const str_manufacturer = utf16leStringDesc("FlashOS")
pub const str_product = utf16leStringDesc("FlashOS Serial")
// Fixed serial → a deterministic /dev/tty.usbmodem node once CDC lands.
pub const str_serial = utf16leStringDesc("0001")
// Resolve a GET_DESCRIPTOR request to its byte slice, or null when the
// device should STALL EP0 (unknown type, unknown string index, and the
// HS-only DEVICE_QUALIFIER / OTHER_SPEED descriptors a Full-Speed-only
// device must reject).
pub fn getDescriptor(desc_type u8, index u8) ?[]u8 {
return switch desc_type {
DESC_DEVICE => &device_descriptor,
DESC_CONFIG => &config_descriptor,
DESC_STRING => switch index {
0 => &str_langid,
1 => &str_manufacturer,
2 => &str_product,
3 => &str_serial,
else => null,
},
else => null,
}
}
// Decoded 8-byte SETUP packet (USB 2.0 §9.3, all multi-byte fields LE).
pub const Setup = struct {
bmRequestType u8,
bRequest u8,
wValue u16,
wIndex u16,
wLength u16,
// GET_DESCRIPTOR packs type in the high byte, index in the low byte.
pub fn descType(self Setup) u8 {
return #intCast(self.wValue >> 8)
}
pub fn descIndex(self Setup) u8 {
return #intCast(self.wValue & 0x00FF)
}
// SET_ADDRESS carries a 7-bit address in wValue.
pub fn address(self Setup) u8 {
return #intCast(self.wValue & 0x007F)
}
}
pub fn decodeSetup(raw *[8]u8) Setup {
return .{
.bmRequestType = raw[0],
.bRequest = raw[1],
.wValue = #as(u16, raw[2]) | (#as(u16, raw[3]) << 8),
.wIndex = #as(u16, raw[4]) | (#as(u16, raw[5]) << 8),
.wLength = #as(u16, raw[6]) | (#as(u16, raw[7]) << 8),
}
}
// --- Host tests ---
const testing = std.testing
test "device descriptor is byte-exact (CDC class at device level)" {
try testing.expectEqual(#as(usize, 18), device_descriptor.len)
try testing.expectEqual(#as(u8, 18), device_descriptor[0]) // bLength
try testing.expectEqual(DESC_DEVICE, device_descriptor[1])
try testing.expectEqual(#as(u8, 0x02), device_descriptor[4]) // bDeviceClass = CDC
try testing.expectEqual(#as(u8, 0x00), device_descriptor[5]) // bDeviceSubClass
try testing.expectEqual(#as(u8, 0x00), device_descriptor[6]) // bDeviceProtocol
try testing.expectEqual(#as(u8, 0x40), device_descriptor[7]) // bMaxPacketSize0 = 64
// idVendor 0x1209 / idProduct 0x0001, little-endian.
try testing.expectEqual(#as(u8, 0x09), device_descriptor[8])
try testing.expectEqual(#as(u8, 0x12), device_descriptor[9])
try testing.expectEqual(#as(u8, 0x01), device_descriptor[10])
try testing.expectEqual(#as(u8, 0x00), device_descriptor[11])
try testing.expectEqual(#as(u8, 1), device_descriptor[17]) // bNumConfigurations
}
test "config descriptor: CDC-ACM two-interface layout is byte-exact" {
try testing.expectEqual(#as(usize, 67), config_descriptor.len)
// configuration descriptor
try testing.expectEqual(#as(u8, 9), config_descriptor[0])
try testing.expectEqual(DESC_CONFIG, config_descriptor[1])
try testing.expectEqual(#as(u8, 0x43), config_descriptor[2]) // wTotalLength = 67 lo
try testing.expectEqual(#as(u8, 0x00), config_descriptor[3]) // wTotalLength hi
try testing.expectEqual(#as(u8, 2), config_descriptor[4]) // bNumInterfaces
try testing.expectEqual(#as(u8, 1), config_descriptor[5]) // bConfigurationValue
// interface 0: Communications class (subclass ACM, 1 notify EP)
try testing.expectEqual(DESC_INTERFACE, config_descriptor[10])
try testing.expectEqual(#as(u8, 0), config_descriptor[11]) // bInterfaceNumber
try testing.expectEqual(#as(u8, 1), config_descriptor[13]) // bNumEndpoints
try testing.expectEqual(#as(u8, 0x02), config_descriptor[14]) // CDC
try testing.expectEqual(#as(u8, 0x02), config_descriptor[15]) // ACM
// notification endpoint (interrupt IN, EP1)
try testing.expectEqual(DESC_ENDPOINT, config_descriptor[38])
try testing.expectEqual(#as(u8, 0x81), config_descriptor[39])
try testing.expectEqual(#as(u8, 0x03), config_descriptor[40]) // interrupt
// interface 1: Data class + bulk endpoints
try testing.expectEqual(#as(u8, 0x0A), config_descriptor[49]) // CDC Data
try testing.expectEqual(#as(u8, 0x02), config_descriptor[55]) // bulk OUT addr
try testing.expectEqual(#as(u8, 0x40), config_descriptor[57]) // bulk OUT MPS = 64
try testing.expectEqual(#as(u8, 0x82), config_descriptor[62]) // bulk IN addr
try testing.expectEqual(#as(u8, 0x40), config_descriptor[64]) // bulk IN MPS = 64
}
test "CDC Union functional descriptor links comms->data (macOS binds on this)" {
try testing.expectEqual(#as(u8, 5), config_descriptor[32]) // bFunctionLength
try testing.expectEqual(CS_INTERFACE, config_descriptor[33])
try testing.expectEqual(#as(u8, 0x06), config_descriptor[34]) // Union subtype
try testing.expectEqual(#as(u8, 0), config_descriptor[35]) // bControlInterface = comms
try testing.expectEqual(#as(u8, 1), config_descriptor[36]) // bSubordinateInterface0 = data
// Call-Management functional descriptor points at the data interface.
try testing.expectEqual(#as(u8, 0x01), config_descriptor[25]) // Call-Mgmt subtype
try testing.expectEqual(#as(u8, 1), config_descriptor[27]) // bDataInterface = 1
}
test "LANGID string descriptor (index 0) is mandatory and exact" {
// A missing/wrong index-0 LANGID is the classic macOS enum-abort cause.
try testing.expectEqualSlices(u8, &[_]u8{ 0x04, 0x03, 0x09, 0x04 }, &str_langid)
}
test "getDescriptor resolves the known descriptors and STALLs the rest" {
try testing.expect(getDescriptor(DESC_DEVICE, 0) != null)
try testing.expectEqual(#as(usize, 67), getDescriptor(DESC_CONFIG, 0).?.len)
try testing.expectEqual(#as(usize, 4), getDescriptor(DESC_STRING, 0).?.len)
try testing.expect(getDescriptor(DESC_STRING, 1) != null)
try testing.expect(getDescriptor(DESC_STRING, 9) == null) // unknown string → STALL
try testing.expect(getDescriptor(DESC_DEVICE_QUALIFIER, 0) == null) // FS-only → STALL
try testing.expect(getDescriptor(DESC_OTHER_SPEED, 0) == null) // FS-only → STALL
}
test "string descriptors carry UTF-16LE bodies with correct bLength" {
// "FlashOS" = 7 chars → bLength = 2 + 2*7 = 16.
try testing.expectEqual(#as(u8, 16), str_manufacturer[0])
try testing.expectEqual(DESC_STRING, str_manufacturer[1])
try testing.expectEqual(#as(u8, 'F'), str_manufacturer[2])
try testing.expectEqual(#as(u8, 0x00), str_manufacturer[3])
try testing.expectEqual(#as(u8, 'l'), str_manufacturer[4])
// "0001" = 4 chars → bLength = 10.
try testing.expectEqual(#as(u8, 10), str_serial[0])
}
test "line coding default is 115200 8N1" {
// dwDTERate = 115200 = 0x0001C200, little-endian.
try testing.expectEqual(#as(u8, 0x00), line_coding_default[0])
try testing.expectEqual(#as(u8, 0xC2), line_coding_default[1])
try testing.expectEqual(#as(u8, 0x01), line_coding_default[2])
try testing.expectEqual(#as(u8, 0x00), line_coding_default[3])
try testing.expectEqual(#as(u8, 0x00), line_coding_default[4]) // 1 stop bit
try testing.expectEqual(#as(u8, 0x00), line_coding_default[5]) // no parity
try testing.expectEqual(#as(u8, 0x08), line_coding_default[6]) // 8 data bits
}
test "decodeSetup unpacks a GET_DESCRIPTOR(device) request" {
// bmRT=0x80 (D2H,std,device) bReq=0x06 wValue=0x0100 (DEVICE,idx0) wLen=64.
const raw = [8]u8{ 0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x40, 0x00 }
const s = decodeSetup(&raw)
try testing.expectEqual(#as(u8, 0x80), s.bmRequestType)
try testing.expectEqual(REQ_GET_DESCRIPTOR, s.bRequest)
try testing.expectEqual(#as(u16, 0x0100), s.wValue)
try testing.expectEqual(DESC_DEVICE, s.descType())
try testing.expectEqual(#as(u8, 0), s.descIndex())
try testing.expectEqual(#as(u16, 64), s.wLength)
}
test "decodeSetup masks the SET_ADDRESS 7-bit address" {
// SET_ADDRESS to 0x6B; high bit of wValue must be masked off.
const raw = [8]u8{ 0x00, 0x05, 0xEB, 0x00, 0x00, 0x00, 0x00, 0x00 }
const s = decodeSetup(&raw)
try testing.expectEqual(REQ_SET_ADDRESS, s.bRequest)
try testing.expectEqual(#as(u8, 0x6B), s.address()) // 0xEB & 0x7F
}
test "decodeSetup unpacks a SET_CONFIGURATION request" {
const raw = [8]u8{ 0x00, 0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00 }
const s = decodeSetup(&raw)
try testing.expectEqual(REQ_SET_CONFIGURATION, s.bRequest)
try testing.expectEqual(#as(u16, 1), s.wValue)
}
test "decodeSetup unpacks a CDC SET_LINE_CODING request" {
// bmRT=0x21 (H2D, class, interface) bReq=0x20 wIndex=0 wLength=7.
const raw = [8]u8{ 0x21, 0x20, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00 }
const s = decodeSetup(&raw)
try testing.expectEqual(#as(u8, 0x21), s.bmRequestType)
try testing.expectEqual(REQ_SET_LINE_CODING, s.bRequest)
try testing.expectEqual(#as(u16, 7), s.wLength)
}
test "config descriptor sub-descriptor bLength chain sums to wTotalLength" {
// Walk the descriptor chain by bLength; it must land exactly on the
// buffer end. Catches an edit that resizes a functional descriptor but
// forgets wTotalLength — a silent enumeration-corrupting bug macOS
// surfaces only as an unhelpful "device not configured".
var i usize = 0
while i < config_descriptor.len {
try testing.expect(config_descriptor[i] != 0) // bLength 0 = malformed (would never terminate on hw)
i += config_descriptor[i]
}
try testing.expectEqual(#as(usize, 67), i) // chain ends exactly at the buffer end
const wtotal = #as(u16, config_descriptor[2]) | (#as(u16, config_descriptor[3]) << 8)
try testing.expectEqual(#as(u16, config_descriptor.len), wtotal)
}
test "getDescriptor(CONFIG) serves exactly wTotalLength bytes" {
// The served slice length must match the descriptor's own wTotalLength
// field, or the host's two-stage config fetch (9-byte header, then full)
// reads a truncated/over-long body.
const d = getDescriptor(DESC_CONFIG, 0).?
const wtotal = #as(u16, d[2]) | (#as(u16, d[3]) << 8)
try testing.expectEqual(#as(usize, 67), d.len)
try testing.expectEqual(#as(u16, #intCast(d.len)), wtotal)
}
test "descType/descIndex split CONFIG and STRING wValue correctly" {
// GET_DESCRIPTOR(CONFIG, 0) → wValue 0x0200; (STRING, 3 = serial) → 0x0303.
const cfg_raw = [8]u8{ 0x80, 0x06, 0x00, 0x02, 0x00, 0x00, 0xFF, 0x00 }
const cfg = decodeSetup(&cfg_raw)
try testing.expectEqual(DESC_CONFIG, cfg.descType())
try testing.expectEqual(#as(u8, 0), cfg.descIndex())
const str_raw = [8]u8{ 0x80, 0x06, 0x03, 0x03, 0x09, 0x04, 0xFF, 0x00 }
const str = decodeSetup(&str_raw)
try testing.expectEqual(DESC_STRING, str.descType())
try testing.expectEqual(#as(u8, 3), str.descIndex())
}
test "decodeSetup unpacks the CDC SET_CONTROL_LINE_STATE request" {
// macOS sends this on tty open: bmRT=0x21, bReq=0x22, wValue bit0=DTR
// bit1=RTS, wLength=0 (no data stage — dispatchSetup ZLP-acks it).
const raw = [8]u8{ 0x21, 0x22, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }
const s = decodeSetup(&raw)
try testing.expectEqual(#as(u8, 0x21), s.bmRequestType)
try testing.expectEqual(REQ_SET_CONTROL_LINE_STATE, s.bRequest)
try testing.expectEqual(#as(u16, 0x0003), s.wValue) // DTR|RTS asserted
try testing.expectEqual(#as(u16, 0), s.wLength)
}
test "decodeSetup unpacks GET_CONFIGURATION / GET_STATUS / CLEAR_FEATURE" {
// The remaining standard requests dispatchSetup answers — round-trip
// each so every arm of that switch has decode coverage.
const getcfg = [8]u8{ 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00 }
try testing.expectEqual(REQ_GET_CONFIGURATION, decodeSetup(&getcfg).bRequest)
const getsts = [8]u8{ 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00 }
try testing.expectEqual(REQ_GET_STATUS, decodeSetup(&getsts).bRequest)
// CLEAR_FEATURE(ENDPOINT_HALT) on EP2 IN: bmRT=0x02 (endpoint), wIndex=0x82.
const clrf = [8]u8{ 0x02, 0x01, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00 }
const s = decodeSetup(&clrf)
try testing.expectEqual(REQ_CLEAR_FEATURE, s.bRequest)
try testing.expectEqual(#as(u16, 0x0082), s.wIndex)
}