From d2725893fdf697b65ffdf29d810d82524df32441 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 10 Jan 2026 14:40:46 +0100 Subject: [PATCH 01/38] neccessary -> necessary and fix linter bot issue --- examples/raspberrypi/rp2xxx/src/custom_clock_config.zig | 2 +- port/raspberrypi/rp2xxx/src/hal/clocks/common.zig | 2 +- port/raspberrypi/rp2xxx/src/hal/usb.zig | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/raspberrypi/rp2xxx/src/custom_clock_config.zig b/examples/raspberrypi/rp2xxx/src/custom_clock_config.zig index 7fe3c7968..950df80c3 100644 --- a/examples/raspberrypi/rp2xxx/src/custom_clock_config.zig +++ b/examples/raspberrypi/rp2xxx/src/custom_clock_config.zig @@ -73,7 +73,7 @@ const system_clock_cfg: GlobalConfig = val: { .integer_divisor = 1, }, - // Change peri to also run off XOSC, not neccessarily reccomended, but interesting! + // Change peri to also run off XOSC, not necessarily reccomended, but interesting! .peri = .{ .input = .{ .source = .src_xosc, diff --git a/port/raspberrypi/rp2xxx/src/hal/clocks/common.zig b/port/raspberrypi/rp2xxx/src/hal/clocks/common.zig index 903e9c359..00b6e3219 100644 --- a/port/raspberrypi/rp2xxx/src/hal/clocks/common.zig +++ b/port/raspberrypi/rp2xxx/src/hal/clocks/common.zig @@ -7,7 +7,7 @@ const CLOCKS = peripherals.CLOCKS; const XOSC = peripherals.XOSC; /// The current HAL requires XOSC configuration with the RP2xxx chip, although this isn't -/// strictly neccessary as the system could be driven from an external clock. +/// strictly necessary as the system could be driven from an external clock. /// TODO: Find a way to allow this to be "null" as it's not explicitly required as long /// as a user isn't using XOSC functionality in their clock setup. pub const xosc_freq = microzig.board.xosc_freq; diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 20bcc5136..5b533f7fb 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -149,9 +149,9 @@ pub fn Polled( // the buffer is ours again. This is indicated by the hw // _clearing_ the AVAILABLE bit. // - // This ensures that we can return a shared reference to - // the databuffer contents without races. - // TODO: if ((bc & (1 << 10)) == 1) return EPBError.NotAvailable; + // It seems the hardware sets the AVAILABLE bit _after_ setting BUFF_STATUS + // So we wait for it just to be sure. + while (buffer_control[epnum].get(ep.dir).read().AVAILABLE_0 != 0) {} // Cool. Checks out. @@ -288,7 +288,7 @@ pub fn Polled( const bufctrl_ptr = &buffer_control[@intFromEnum(ep_num)].in; const ep = self.hardware_endpoint_get_by_address(.in(ep_num)); // Wait for controller to give processor ownership of the buffer before writing it. - // This is technically not neccessary, but the usb cdc driver is bugged. + // This is technically not necessary, but the usb cdc driver is bugged. while (bufctrl_ptr.read().AVAILABLE_0 == 1) {} const len = buffer.len; From 3e3b6de712113f757a7c95d40105c1c27062c58c Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 10 Jan 2026 15:47:44 +0100 Subject: [PATCH 02/38] scan unhandled buffer linearly --- core/src/core/usb/drivers/cdc.zig | 6 ++- port/raspberrypi/rp2xxx/src/hal/usb.zig | 68 ++++++++++++------------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 30b80ff7b..72cc3e53e 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -118,8 +118,8 @@ pub fn CdcClassDriver(options: Options) type { ep_out: types.Endpoint.Num, line_coding: LineCoding align(4), - rx: FIFO = .empty, - tx: FIFO = .empty, + rx: FIFO, + tx: FIFO, epin_buf: [options.max_packet_size]u8 = undefined, @@ -177,6 +177,8 @@ pub fn CdcClassDriver(options: Options) type { .parity = 0, .data_bits = 8, }, + .rx = .empty, + .tx = .empty, }; } diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 5b533f7fb..bb4eb280c 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -113,36 +113,22 @@ pub fn Polled( self.controller.on_setup_req(&self.interface, &setup); } + var buff_status: u32 = 0; // Events on one or more buffers? (In practice, always one.) if (ints.BUFF_STATUS != 0) { const bufbits_init = peripherals.USB.BUFF_STATUS.raw; - var bufbits = bufbits_init; - - while (true) { - // Who's still outstanding? Find their bit index by counting how - // many LSBs are zero. - const lowbit_index = std.math.cast(u5, @ctz(bufbits)) orelse break; - // Remove their bit from our set. - bufbits ^= @as(u32, @intCast(1)) << lowbit_index; - - // Here we exploit knowledge of the ordering of buffer control - // registers in the peripheral. Each endpoint has a pair of - // registers, so we can determine the endpoint number by: - const epnum = @as(u4, @intCast(lowbit_index >> 1)); - // Of the pair, the IN endpoint comes first, followed by OUT, so - // we can get the direction by: - const dir: usb.types.Dir = if (lowbit_index & 1 == 0) .In else .Out; - - const ep: usb.types.Endpoint = .{ .num = @enumFromInt(epnum), .dir = dir }; - // Process the buffer-done event. - - // Process the buffer-done event. - // - // Scan the device table to figure out which endpoint struct - // corresponds to this address. We could use a smarter - // method here, but in practice, the number of endpoints is - // small so a linear scan doesn't kill us. + buff_status |= bufbits_init; + peripherals.USB.BUFF_STATUS.write_raw(bufbits_init); + } + // Here we exploit knowledge of the ordering of buffer control + // registers in the peripheral. Each endpoint has a pair of + // registers, IN being first + for (0..16) |ep_num| { + const shift: u5 = @intCast(2 * ep_num); + + if (buff_status & (@as(u32, 1) << shift) != 0) { + const ep: usb.types.Endpoint = .in(@enumFromInt(ep_num)); const ep_hard = self.hardware_endpoint_get_by_address(ep); // We should only get here if we've been notified that @@ -151,22 +137,36 @@ pub fn Polled( // // It seems the hardware sets the AVAILABLE bit _after_ setting BUFF_STATUS // So we wait for it just to be sure. - while (buffer_control[epnum].get(ep.dir).read().AVAILABLE_0 != 0) {} + while (buffer_control[ep_num].in.read().AVAILABLE_0 != 0) {} - // Cool. Checks out. + // Get the actual length of the data, which may be less + // than the buffer size. + const len = buffer_control[@intFromEnum(ep.num)].in.read().LENGTH_0; + + self.controller.on_buffer(&self.interface, ep, ep_hard.data_buffer[0..len]); + } + + if (buff_status & (@as(u32, 2) << shift) != 0) { + const ep: usb.types.Endpoint = .out(@enumFromInt(ep_num)); + const ep_hard = self.hardware_endpoint_get_by_address(ep); + + // We should only get here if we've been notified that + // the buffer is ours again. This is indicated by the hw + // _clearing_ the AVAILABLE bit. + // + // It seems the hardware sets the AVAILABLE bit _after_ setting BUFF_STATUS + // So we wait for it just to be sure. + while (buffer_control[ep_num].out.read().AVAILABLE_0 != 0) {} // Get the actual length of the data, which may be less // than the buffer size. - const len = buffer_control[@intFromEnum(ep.num)].get(ep.dir).read().LENGTH_0; + const len = buffer_control[@intFromEnum(ep.num)].out.read().LENGTH_0; self.controller.on_buffer(&self.interface, ep, ep_hard.data_buffer[0..len]); - if (ep.dir == .Out) - ep_hard.awaiting_rx = false; + ep_hard.awaiting_rx = false; } - - peripherals.USB.BUFF_STATUS.write_raw(bufbits_init); - } // <-- END of buf status handling + } // Has the host signaled a bus reset? if (ints.BUS_RESET != 0) { From b587b46e861feb688e2299cd62a53c6a11be38eb Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 10 Jan 2026 17:42:23 +0100 Subject: [PATCH 03/38] redo endpoint handling --- core/src/core/usb.zig | 105 ++++++++++++++---------- core/src/core/usb/drivers/cdc.zig | 31 ++++--- core/src/core/usb/drivers/hid.zig | 7 +- port/raspberrypi/rp2xxx/src/hal/usb.zig | 42 +++------- 4 files changed, 100 insertions(+), 85 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index d5e04641b..b038fe619 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const assert = std.debug.assert; const log = std.log.scoped(.usb); pub const descriptor = @import("usb/descriptor.zig"); @@ -66,6 +67,11 @@ pub const Config = struct { configurations: []const Configuration, }; +const Handler = struct { + driver: []const u8, + function: []const u8, +}; + /// USB device controller /// /// This code handles usb enumeration and configuration and routes packets to drivers. @@ -147,6 +153,31 @@ pub fn DeviceController(config: Config) type { } }){}; }; + const handlers = blk: { + var ret: struct { In: [16]Handler, Out: [16]Handler } = .{ + .In = @splat(.{ .driver = "", .function = "" }), + .Out = @splat(.{ .driver = "", .function = "" }), + }; + for (driver_fields) |fld_drv| { + const cfg = @field(config_descriptor, fld_drv.name); + const fields = @typeInfo(@TypeOf(cfg)).@"struct".fields; + for (fields) |fld| { + if (fld.type != descriptor.Endpoint) continue; + const desc: descriptor.Endpoint = @field(cfg, fld.name); + const handler = &@field(ret, @tagName(desc.endpoint.dir))[@intFromEnum(desc.endpoint.num)]; + // assert(handler.driver.len == 0 and handler.function.len == 0); + handler.* = .{ + .driver = fld_drv.name, + .function = switch (desc.endpoint.dir) { + .In => "on_rx", + .Out => "on_tx_ready", + }, + }; + } + } + break :blk ret; + }; + /// When the host sets the device address, the acknowledgement /// step must use the _old_ address. new_address: ?u8, @@ -213,61 +244,49 @@ pub fn DeviceController(config: Config) type { } /// Called by the device implementation when a packet has been sent or received. - pub fn on_buffer(self: *@This(), device_itf: *DeviceInterface, ep: types.Endpoint, buffer: []u8) void { + pub fn on_buffer(self: *@This(), device_itf: *DeviceInterface, comptime ep: types.Endpoint, buffer: []u8) void { if (config.debug) log.info("buff status", .{}); - if (config.debug) log.info(" data: {any}", .{buffer}); - // Perform any required action on the data. For OUT, the `data` - // will be whatever was sent by the host. For IN, it's a copy of - // whatever we sent. - switch (ep.num) { - .ep0 => if (ep.dir == .In) { - if (config.debug) log.info(" EP0_IN_ADDR", .{}); - - // We use this opportunity to finish the delayed - // SetAddress request, if there is one: - if (self.new_address) |addr| { - // Change our address: - device_itf.set_address(@intCast(addr)); - self.new_address = null; - } + const handler = comptime @field(handlers, @tagName(ep.dir))[@intFromEnum(ep.num)]; - if (buffer.len > 0 and self.tx_slice.len > 0) { - self.tx_slice = self.tx_slice[buffer.len..]; + if (comptime ep == types.Endpoint.in(.ep0)) { + if (config.debug) log.info(" EP0_IN_ADDR", .{}); - const next_data_chunk = self.tx_slice[0..@min(64, self.tx_slice.len)]; - if (next_data_chunk.len > 0) { - device_itf.start_tx(.ep0, next_data_chunk); - } else { - device_itf.start_rx(.ep0, 0); + // We use this opportunity to finish the delayed + // SetAddress request, if there is one: + if (self.new_address) |addr| { + // Change our address: + device_itf.set_address(@intCast(addr)); + self.new_address = null; + } - if (self.driver_last) |drv| - self.driver_class_control(device_itf, drv, .Ack, &self.setup_packet); - } + if (buffer.len > 0 and self.tx_slice.len > 0) { + self.tx_slice = self.tx_slice[buffer.len..]; + + const next_data_chunk = self.tx_slice[0..@min(64, self.tx_slice.len)]; + if (next_data_chunk.len > 0) { + device_itf.start_tx(.ep0, next_data_chunk); } else { - // Otherwise, we've just finished sending - // something to the host. We expect an ensuing - // status phase where the host sends us (via EP0 - // OUT) a zero-byte DATA packet, so, set that - // up: device_itf.start_rx(.ep0, 0); if (self.driver_last) |drv| self.driver_class_control(device_itf, drv, .Ack, &self.setup_packet); } - }, - inline else => |ep_num| inline for (driver_fields) |fld_drv| { - const cfg = @field(config_descriptor, fld_drv.name); - const fields = @typeInfo(@TypeOf(cfg)).@"struct".fields; - inline for (fields) |fld| { - const desc = @field(cfg, fld.name); - if (comptime fld.type == descriptor.Endpoint and desc.endpoint.num == ep_num) { - if (ep.dir == desc.endpoint.dir) - @field(self.driver_data.?, fld_drv.name).transfer(ep, buffer); - } - } - }, + } else { + // Otherwise, we've just finished sending + // something to the host. We expect an ensuing + // status phase where the host sends us (via EP0 + // OUT) a zero-byte DATA packet, so, set that + // up: + device_itf.start_rx(.ep0, 0); + + if (self.driver_last) |drv| + self.driver_class_control(device_itf, drv, .Ack, &self.setup_packet); + } + } else if (comptime handler.driver.len != 0) { + const drv = &@field(self.driver_data.?, handler.driver); + @field(@FieldType(config0.Drivers, handler.driver), handler.function)(drv, ep.num, buffer); } } diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 72cc3e53e..7fd9362b8 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -121,6 +121,8 @@ pub fn CdcClassDriver(options: Options) type { rx: FIFO, tx: FIFO, + last_len: u11, + epin_buf: [options.max_packet_size]u8 = undefined, pub fn available(self: *@This()) usize { @@ -155,6 +157,7 @@ pub fn CdcClassDriver(options: Options) type { } const len = self.tx.read(&self.epin_buf); self.device.start_tx(self.ep_in, self.epin_buf[0..len]); + self.last_len = @intCast(len); return len; } @@ -179,6 +182,7 @@ pub fn CdcClassDriver(options: Options) type { }, .rx = .empty, .tx = .empty, + .last_len = 0, }; } @@ -200,19 +204,22 @@ pub fn CdcClassDriver(options: Options) type { return usb.nak; } - pub fn transfer(self: *@This(), ep: types.Endpoint, data: []u8) void { - if (ep == types.Endpoint.out(self.ep_out)) { - self.rx.write(data) catch {}; - self.prep_out_transaction(); - } + pub fn on_tx_ready(self: *@This(), ep_num: types.Endpoint.Num, data: []u8) void { + if (ep_num != self.ep_out) return; + + self.rx.write(data) catch {}; + self.prep_out_transaction(); + } + + pub fn on_rx(self: *@This(), ep_num: types.Endpoint.Num, _: []u8) void { + if (ep_num != self.ep_in) return; - if (ep == types.Endpoint.in(self.ep_in)) { - if (self.write_flush() == 0) { - // If there is no data left, a empty packet should be sent if - // data len is multiple of EP Packet size and not zero - if (self.tx.get_readable_len() == 0 and data.len > 0 and data.len == options.max_packet_size) { - self.device.start_tx(self.ep_in, &.{}); - } + if (self.write_flush() == 0) { + // If there is no data left, a empty packet should be sent if + // data len is multiple of EP Packet size and not zero + if (self.tx.get_readable_len() == 0 and self.last_len == options.max_packet_size) { + self.device.start_tx(self.ep_in, usb.ack); + self.last_len = 0; } } } diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index a12f4f64e..c525c1b3c 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -123,10 +123,15 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { return usb.nak; } - pub fn transfer(self: *@This(), ep: types.Endpoint, data: []u8) void { + pub fn on_tx_ready(self: *@This(), ep: types.Endpoint.Num, data: []u8) void { _ = self; _ = ep; _ = data; } + + pub fn on_rx(self: *@This(), ep: types.Endpoint.Num, _: []u8) void { + _ = self; + _ = ep; + } }; } diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index bb4eb280c..cbebe1bd2 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -99,7 +99,6 @@ pub fn Polled( pub fn poll(self: *@This()) void { // Check which interrupt flags are set. - const ints = peripherals.USB.INTS.read(); // Setup request received? @@ -121,33 +120,17 @@ pub fn Polled( peripherals.USB.BUFF_STATUS.write_raw(bufbits_init); } - // Here we exploit knowledge of the ordering of buffer control - // registers in the peripheral. Each endpoint has a pair of - // registers, IN being first - for (0..16) |ep_num| { - const shift: u5 = @intCast(2 * ep_num); - + inline for (0..2 * config.max_endpoints_count) |shift| { if (buff_status & (@as(u32, 1) << shift) != 0) { - const ep: usb.types.Endpoint = .in(@enumFromInt(ep_num)); - const ep_hard = self.hardware_endpoint_get_by_address(ep); - - // We should only get here if we've been notified that - // the buffer is ours again. This is indicated by the hw - // _clearing_ the AVAILABLE bit. - // - // It seems the hardware sets the AVAILABLE bit _after_ setting BUFF_STATUS - // So we wait for it just to be sure. - while (buffer_control[ep_num].in.read().AVAILABLE_0 != 0) {} - - // Get the actual length of the data, which may be less - // than the buffer size. - const len = buffer_control[@intFromEnum(ep.num)].in.read().LENGTH_0; - - self.controller.on_buffer(&self.interface, ep, ep_hard.data_buffer[0..len]); - } + // Here we exploit knowledge of the ordering of buffer control + // registers in the peripheral. Each endpoint has a pair of + // registers, IN being first + const ep_num = shift / 2; + const ep: usb.types.Endpoint = comptime .{ + .num = @enumFromInt(ep_num), + .dir = if (shift & 1 == 0) .In else .Out, + }; - if (buff_status & (@as(u32, 2) << shift) != 0) { - const ep: usb.types.Endpoint = .out(@enumFromInt(ep_num)); const ep_hard = self.hardware_endpoint_get_by_address(ep); // We should only get here if we've been notified that @@ -156,15 +139,16 @@ pub fn Polled( // // It seems the hardware sets the AVAILABLE bit _after_ setting BUFF_STATUS // So we wait for it just to be sure. - while (buffer_control[ep_num].out.read().AVAILABLE_0 != 0) {} + while (buffer_control[ep_num].get(ep.dir).read().AVAILABLE_0 != 0) {} // Get the actual length of the data, which may be less // than the buffer size. - const len = buffer_control[@intFromEnum(ep.num)].out.read().LENGTH_0; + const len = buffer_control[ep_num].get(ep.dir).read().LENGTH_0; self.controller.on_buffer(&self.interface, ep, ep_hard.data_buffer[0..len]); - ep_hard.awaiting_rx = false; + if (ep.dir == .Out) + ep_hard.awaiting_rx = false; } } From a6537115a5d818d2146a261420417c700955bd74 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 10 Jan 2026 18:33:05 +0100 Subject: [PATCH 04/38] separate getting usb rx data and requesting more --- core/src/core/usb.zig | 20 ++++++++---- core/src/core/usb/drivers/cdc.zig | 12 ++++--- core/src/core/usb/drivers/hid.zig | 5 ++- port/raspberrypi/rp2xxx/src/hal/usb.zig | 43 +++++++++++++++++-------- 4 files changed, 53 insertions(+), 27 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index b038fe619..b69b98d12 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -17,7 +17,8 @@ pub const nak: ?[]const u8 = null; pub const DeviceInterface = struct { pub const VTable = struct { start_tx: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, buffer: []const u8) void, - start_rx: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, len: usize) void, + ep_readv: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, data: []const []u8) usize, + ep_listen: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, len: usize) void, endpoint_open: *const fn (self: *DeviceInterface, desc: *const descriptor.Endpoint) void, set_address: *const fn (self: *DeviceInterface, addr: u7) void, }; @@ -31,10 +32,15 @@ pub const DeviceInterface = struct { return self.vtable.start_tx(self, ep_num, buffer); } - /// Called by drivers to report readiness to receive up to `len` bytes. + /// Called by drivers to retrieve a received packet. /// Must be called exactly once before each packet. - pub fn start_rx(self: *@This(), ep_num: types.Endpoint.Num, len: usize) void { - return self.vtable.start_rx(self, ep_num, len); + /// Buffers in `data` must collectively be long enough to fit the whole packet. + pub fn ep_readv(self: *@This(), ep_num: types.Endpoint.Num, data: []const []u8) usize { + return self.vtable.ep_readv(self, ep_num, data); + } + + pub fn ep_listen(self: *@This(), ep_num: types.Endpoint.Num, len: usize) void { + return self.vtable.ep_listen(self, ep_num, len); } /// Opens an endpoint according to the descriptor. Note that if the endpoint @@ -268,7 +274,7 @@ pub fn DeviceController(config: Config) type { if (next_data_chunk.len > 0) { device_itf.start_tx(.ep0, next_data_chunk); } else { - device_itf.start_rx(.ep0, 0); + device_itf.ep_listen(.ep0, 0); if (self.driver_last) |drv| self.driver_class_control(device_itf, drv, .Ack, &self.setup_packet); @@ -279,14 +285,14 @@ pub fn DeviceController(config: Config) type { // status phase where the host sends us (via EP0 // OUT) a zero-byte DATA packet, so, set that // up: - device_itf.start_rx(.ep0, 0); + device_itf.ep_listen(.ep0, 0); if (self.driver_last) |drv| self.driver_class_control(device_itf, drv, .Ack, &self.setup_packet); } } else if (comptime handler.driver.len != 0) { const drv = &@field(self.driver_data.?, handler.driver); - @field(@FieldType(config0.Drivers, handler.driver), handler.function)(drv, ep.num, buffer); + @field(@FieldType(config0.Drivers, handler.driver), handler.function)(drv, ep.num); } } diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 7fd9362b8..d9e1bf736 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -122,6 +122,7 @@ pub fn CdcClassDriver(options: Options) type { tx: FIFO, last_len: u11, + rx_ready: bool, epin_buf: [options.max_packet_size]u8 = undefined, @@ -164,7 +165,7 @@ pub fn CdcClassDriver(options: Options) type { fn prep_out_transaction(self: *@This()) void { if (self.rx.get_writable_len() >= options.max_packet_size) { // Let endpoint know that we are ready for next packet - self.device.start_rx(self.ep_out, options.max_packet_size); + self.device.ep_listen(self.ep_out, options.max_packet_size); } } @@ -183,6 +184,7 @@ pub fn CdcClassDriver(options: Options) type { .rx = .empty, .tx = .empty, .last_len = 0, + .rx_ready = true, }; } @@ -204,14 +206,16 @@ pub fn CdcClassDriver(options: Options) type { return usb.nak; } - pub fn on_tx_ready(self: *@This(), ep_num: types.Endpoint.Num, data: []u8) void { + pub fn on_tx_ready(self: *@This(), ep_num: types.Endpoint.Num) void { if (ep_num != self.ep_out) return; - self.rx.write(data) catch {}; + var buf: [options.max_packet_size]u8 = undefined; + const len = self.device.ep_readv(ep_num, &.{&buf}); + self.rx.write(buf[0..len]) catch {}; self.prep_out_transaction(); } - pub fn on_rx(self: *@This(), ep_num: types.Endpoint.Num, _: []u8) void { + pub fn on_rx(self: *@This(), ep_num: types.Endpoint.Num) void { if (ep_num != self.ep_in) return; if (self.write_flush() == 0) { diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index c525c1b3c..e8896db6f 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -123,13 +123,12 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { return usb.nak; } - pub fn on_tx_ready(self: *@This(), ep: types.Endpoint.Num, data: []u8) void { + pub fn on_tx_ready(self: *@This(), ep: types.Endpoint.Num) void { _ = self; _ = ep; - _ = data; } - pub fn on_rx(self: *@This(), ep: types.Endpoint.Num, _: []u8) void { + pub fn on_rx(self: *@This(), ep: types.Endpoint.Num) void { _ = self; _ = ep; } diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index cbebe1bd2..f5d317f9f 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -87,7 +87,8 @@ pub fn Polled( return struct { const vtable: usb.DeviceInterface.VTable = .{ .start_tx = start_tx, - .start_rx = start_rx, + .ep_readv = ep_readv, + .ep_listen = ep_listen, .set_address = set_address, .endpoint_open = endpoint_open, }; @@ -156,18 +157,15 @@ pub fn Polled( if (ints.BUS_RESET != 0) { // Acknowledge by writing the write-one-to-clear status bit. peripherals.USB.SIE_STATUS.modify(.{ .BUS_RESET = 1 }); - peripherals.USB.ADDR_ENDP.modify(.{ .ADDRESS = 0 }); + set_address(&self.interface, 0); self.controller.on_bus_reset(); } } pub fn init() @This() { - if (chip == .RP2350) { - peripherals.USB.MAIN_CTRL.modify(.{ - .PHY_ISO = 0, - }); - } + if (chip == .RP2350) + peripherals.USB.MAIN_CTRL.write(.{ .PHY_ISO = 0 }); // Clear the control portion of DPRAM. This may not be necessary -- the // datasheet is ambiguous -- but the C examples do it, and so do we. @@ -308,7 +306,29 @@ pub fn Polled( bufctrl_ptr.write(bufctrl); } - fn start_rx( + fn ep_readv( + itf: *usb.DeviceInterface, + ep_num: usb.types.Endpoint.Num, + data: []const []u8, + ) usize { + const self: *@This() = @fieldParentPtr("interface", itf); + assert(data.len > 0); + + const bufctrl = &buffer_control[@intFromEnum(ep_num)].out.read(); + const ep = self.hardware_endpoint_get_by_address(.out(ep_num)); + var hw_buf: []align(1) u8 = ep.data_buffer[0..bufctrl.LENGTH_0]; + for (data) |dst| { + const len = @min(dst.len, hw_buf.len); + // make sure reads from device memory of size 1 + for (dst[0..len], hw_buf[0..len]) |*d, *s| + @atomicStore(u8, d, @atomicLoad(u8, s, .unordered), .unordered); + hw_buf = hw_buf[len..]; + if (hw_buf.len == 0) return hw_buf.ptr - ep.data_buffer.ptr; + } + unreachable; + } + + fn ep_listen( itf: *usb.DeviceInterface, ep_num: usb.types.Endpoint.Num, len: usize, @@ -367,11 +387,8 @@ pub fn Polled( }); } - fn set_address(itf: *usb.DeviceInterface, addr: u7) void { - const self: *@This() = @fieldParentPtr("interface", itf); - _ = self; - - peripherals.USB.ADDR_ENDP.modify(.{ .ADDRESS = addr }); + fn set_address(_: *usb.DeviceInterface, addr: u7) void { + peripherals.USB.ADDR_ENDP.write(.{ .ADDRESS = addr }); } fn hardware_endpoint_get_by_address(self: *@This(), ep: usb.types.Endpoint) *HardwareEndpointData { From 9504866c28eca36ed3657dfb14f3e07634cbae6c Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 10 Jan 2026 19:12:10 +0100 Subject: [PATCH 05/38] make CdcClassDriver rx interrupt-safe --- core/src/core/usb.zig | 8 ++-- core/src/core/usb/drivers/cdc.zig | 52 ++++++++++++++----------- port/raspberrypi/rp2xxx/src/hal/usb.zig | 7 ++-- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index b69b98d12..371ad54af 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -17,8 +17,8 @@ pub const nak: ?[]const u8 = null; pub const DeviceInterface = struct { pub const VTable = struct { start_tx: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, buffer: []const u8) void, - ep_readv: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, data: []const []u8) usize, - ep_listen: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, len: usize) void, + ep_readv: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, data: []const []u8) u11, + ep_listen: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, len: u11) void, endpoint_open: *const fn (self: *DeviceInterface, desc: *const descriptor.Endpoint) void, set_address: *const fn (self: *DeviceInterface, addr: u7) void, }; @@ -35,11 +35,11 @@ pub const DeviceInterface = struct { /// Called by drivers to retrieve a received packet. /// Must be called exactly once before each packet. /// Buffers in `data` must collectively be long enough to fit the whole packet. - pub fn ep_readv(self: *@This(), ep_num: types.Endpoint.Num, data: []const []u8) usize { + pub fn ep_readv(self: *@This(), ep_num: types.Endpoint.Num, data: []const []u8) u11 { return self.vtable.ep_readv(self, ep_num, data); } - pub fn ep_listen(self: *@This(), ep_num: types.Endpoint.Num, len: usize) void { + pub fn ep_listen(self: *@This(), ep_num: types.Endpoint.Num, len: u11) void { return self.vtable.ep_listen(self, ep_num, len); } diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index d9e1bf736..f6d2c8e19 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -115,14 +115,18 @@ pub fn CdcClassDriver(options: Options) type { device: *usb.DeviceInterface, ep_notif: types.Endpoint.Num, ep_in: types.Endpoint.Num, - ep_out: types.Endpoint.Num, line_coding: LineCoding align(4), - rx: FIFO, + /// OUT endpoint on which there is data ready to be read, + /// or .ep0 when no data is available. + ep_out: types.Endpoint.Num, + rx_data: [options.max_packet_size]u8, + rx_seek: u11, + rx_end: u11, + tx: FIFO, last_len: u11, - rx_ready: bool, epin_buf: [options.max_packet_size]u8 = undefined, @@ -131,9 +135,21 @@ pub fn CdcClassDriver(options: Options) type { } pub fn read(self: *@This(), dst: []u8) usize { - const read_count = self.rx.read(dst); - self.prep_out_transaction(); - return read_count; + const len = @min(dst.len, self.rx_end - self.rx_seek); + @memcpy(dst[0..len], self.rx_data[self.rx_seek..][0..len]); + self.rx_seek += len; + + if (self.rx_seek != self.rx_end) return len; + + // request more data + const ep_out = @atomicLoad(types.Endpoint.Num, &self.ep_out, .acquire); + if (ep_out != .ep0) { + self.rx_end = self.device.ep_readv(ep_out, &.{&self.rx_data}); + self.rx_seek = 0; + self.device.ep_listen(ep_out, options.max_packet_size); + } + + return len; } pub fn write(self: *@This(), data: []const u8) []const u8 { @@ -162,29 +178,26 @@ pub fn CdcClassDriver(options: Options) type { return len; } - fn prep_out_transaction(self: *@This()) void { - if (self.rx.get_writable_len() >= options.max_packet_size) { - // Let endpoint know that we are ready for next packet - self.device.ep_listen(self.ep_out, options.max_packet_size); - } - } - pub fn init(desc: *const Descriptor, device: *usb.DeviceInterface) @This() { + defer device.ep_listen(desc.ep_out.endpoint.num, options.max_packet_size); return .{ .device = device, .ep_notif = desc.ep_notifi.endpoint.num, .ep_in = desc.ep_in.endpoint.num, - .ep_out = desc.ep_out.endpoint.num, .line_coding = .{ .bit_rate = 115200, .stop_bits = 0, .parity = 0, .data_bits = 8, }, - .rx = .empty, + + .rx_data = undefined, + .rx_seek = 0, + .rx_end = 0, + .ep_out = .ep0, + .tx = .empty, .last_len = 0, - .rx_ready = true, }; } @@ -207,12 +220,7 @@ pub fn CdcClassDriver(options: Options) type { } pub fn on_tx_ready(self: *@This(), ep_num: types.Endpoint.Num) void { - if (ep_num != self.ep_out) return; - - var buf: [options.max_packet_size]u8 = undefined; - const len = self.device.ep_readv(ep_num, &.{&buf}); - self.rx.write(buf[0..len]) catch {}; - self.prep_out_transaction(); + @atomicStore(types.Endpoint.Num, &self.ep_out, ep_num, .release); } pub fn on_rx(self: *@This(), ep_num: types.Endpoint.Num) void { diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index f5d317f9f..66bc4f8d0 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -310,7 +310,7 @@ pub fn Polled( itf: *usb.DeviceInterface, ep_num: usb.types.Endpoint.Num, data: []const []u8, - ) usize { + ) u11 { const self: *@This() = @fieldParentPtr("interface", itf); assert(data.len > 0); @@ -323,7 +323,8 @@ pub fn Polled( for (dst[0..len], hw_buf[0..len]) |*d, *s| @atomicStore(u8, d, @atomicLoad(u8, s, .unordered), .unordered); hw_buf = hw_buf[len..]; - if (hw_buf.len == 0) return hw_buf.ptr - ep.data_buffer.ptr; + if (hw_buf.len == 0) + return @intCast(hw_buf.ptr - ep.data_buffer.ptr); } unreachable; } @@ -331,7 +332,7 @@ pub fn Polled( fn ep_listen( itf: *usb.DeviceInterface, ep_num: usb.types.Endpoint.Num, - len: usize, + len: u11, ) void { const self: *@This() = @fieldParentPtr("interface", itf); From 52ea727646337f489c5ce5da5c9d3a06945b6c6c Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 10 Jan 2026 19:35:46 +0100 Subject: [PATCH 06/38] add usb packet length type --- core/src/core/usb.zig | 12 ++++++------ core/src/core/usb/drivers/cdc.zig | 10 +++++----- core/src/core/usb/types.zig | 3 +++ port/raspberrypi/rp2xxx/src/hal/usb.zig | 4 ++-- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 371ad54af..b4df180f4 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -17,8 +17,8 @@ pub const nak: ?[]const u8 = null; pub const DeviceInterface = struct { pub const VTable = struct { start_tx: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, buffer: []const u8) void, - ep_readv: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, data: []const []u8) u11, - ep_listen: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, len: u11) void, + ep_readv: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, data: []const []u8) types.Len, + ep_listen: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, len: types.Len) void, endpoint_open: *const fn (self: *DeviceInterface, desc: *const descriptor.Endpoint) void, set_address: *const fn (self: *DeviceInterface, addr: u7) void, }; @@ -35,11 +35,11 @@ pub const DeviceInterface = struct { /// Called by drivers to retrieve a received packet. /// Must be called exactly once before each packet. /// Buffers in `data` must collectively be long enough to fit the whole packet. - pub fn ep_readv(self: *@This(), ep_num: types.Endpoint.Num, data: []const []u8) u11 { + pub fn ep_readv(self: *@This(), ep_num: types.Endpoint.Num, data: []const []u8) types.Len { return self.vtable.ep_readv(self, ep_num, data); } - pub fn ep_listen(self: *@This(), ep_num: types.Endpoint.Num, len: u11) void { + pub fn ep_listen(self: *@This(), ep_num: types.Endpoint.Num, len: types.Len) void { return self.vtable.ep_listen(self, ep_num, len); } @@ -175,8 +175,8 @@ pub fn DeviceController(config: Config) type { handler.* = .{ .driver = fld_drv.name, .function = switch (desc.endpoint.dir) { - .In => "on_rx", - .Out => "on_tx_ready", + .In => "on_tx_ready", + .Out => "on_rx", }, }; } diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index f6d2c8e19..6acfd9786 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -121,12 +121,12 @@ pub fn CdcClassDriver(options: Options) type { /// or .ep0 when no data is available. ep_out: types.Endpoint.Num, rx_data: [options.max_packet_size]u8, - rx_seek: u11, - rx_end: u11, + rx_seek: types.Len, + rx_end: types.Len, tx: FIFO, - last_len: u11, + last_len: types.Len, epin_buf: [options.max_packet_size]u8 = undefined, @@ -219,11 +219,11 @@ pub fn CdcClassDriver(options: Options) type { return usb.nak; } - pub fn on_tx_ready(self: *@This(), ep_num: types.Endpoint.Num) void { + pub fn on_rx(self: *@This(), ep_num: types.Endpoint.Num) void { @atomicStore(types.Endpoint.Num, &self.ep_out, ep_num, .release); } - pub fn on_rx(self: *@This(), ep_num: types.Endpoint.Num) void { + pub fn on_tx_ready(self: *@This(), ep_num: types.Endpoint.Num) void { if (ep_num != self.ep_in) return; if (self.write_flush() == 0) { diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index d0f010825..342e9c044 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -142,6 +142,9 @@ pub const SetupPacket = extern struct { length: u16, }; +/// Represents packet length. +pub const Len = u11; + /// u16 value, little endian regardless of native endianness. pub const U16Le = extern struct { value: [2]u8, diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 66bc4f8d0..063c9d8f2 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -310,7 +310,7 @@ pub fn Polled( itf: *usb.DeviceInterface, ep_num: usb.types.Endpoint.Num, data: []const []u8, - ) u11 { + ) usb.types.Len { const self: *@This() = @fieldParentPtr("interface", itf); assert(data.len > 0); @@ -332,7 +332,7 @@ pub fn Polled( fn ep_listen( itf: *usb.DeviceInterface, ep_num: usb.types.Endpoint.Num, - len: u11, + len: usb.types.Len, ) void { const self: *@This() = @fieldParentPtr("interface", itf); From b65598916456c620f44d5b36ba287d4361e89fc6 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 10 Jan 2026 20:34:28 +0100 Subject: [PATCH 07/38] make CdcClass driver tx interrupt-safe --- core/src/core/usb/drivers/cdc.zig | 90 ++++++++++----------- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 8 +- 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 6acfd9786..386ebb265 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -3,8 +3,6 @@ const usb = @import("../../usb.zig"); const descriptor = usb.descriptor; const types = usb.types; -const utilities = @import("../../../utilities.zig"); - pub const ManagementRequestType = enum(u8) { SetLineCoding = 0x20, GetLineCoding = 0x21, @@ -31,8 +29,6 @@ pub const Options = struct { }; pub fn CdcClassDriver(options: Options) type { - const FIFO = utilities.CircularBuffer(u8, options.max_packet_size); - return struct { pub const Descriptor = extern struct { itf_assoc: descriptor.InterfaceAssociation, @@ -114,7 +110,6 @@ pub fn CdcClassDriver(options: Options) type { device: *usb.DeviceInterface, ep_notif: types.Endpoint.Num, - ep_in: types.Endpoint.Num, line_coding: LineCoding align(4), /// OUT endpoint on which there is data ready to be read, @@ -124,14 +119,15 @@ pub fn CdcClassDriver(options: Options) type { rx_seek: types.Len, rx_end: types.Len, - tx: FIFO, - - last_len: types.Len, - - epin_buf: [options.max_packet_size]u8 = undefined, + /// IN endpoint where data can be sent, + /// or .ep0 when data is being sent. + ep_in: types.Endpoint.Num, + ep_in_original: types.Endpoint.Num, + tx_data: [options.max_packet_size]u8, + tx_end: types.Len, - pub fn available(self: *@This()) usize { - return self.rx.get_readable_len(); + pub fn available(self: *@This()) types.Len { + return self.rx_end - self.rx_seek; } pub fn read(self: *@This(), dst: []u8) usize { @@ -139,51 +135,54 @@ pub fn CdcClassDriver(options: Options) type { @memcpy(dst[0..len], self.rx_data[self.rx_seek..][0..len]); self.rx_seek += len; - if (self.rx_seek != self.rx_end) return len; + if (self.available() > 0) return len; // request more data - const ep_out = @atomicLoad(types.Endpoint.Num, &self.ep_out, .acquire); + const ep_out = @atomicLoad(types.Endpoint.Num, &self.ep_out, .seq_cst); if (ep_out != .ep0) { self.rx_end = self.device.ep_readv(ep_out, &.{&self.rx_data}); self.rx_seek = 0; + @atomicStore(types.Endpoint.Num, &self.ep_out, .ep0, .seq_cst); self.device.ep_listen(ep_out, options.max_packet_size); } return len; } - pub fn write(self: *@This(), data: []const u8) []const u8 { - const write_count = @min(self.tx.get_writable_len(), data.len); + pub fn write(self: *@This(), data: []const u8) usize { + const len = @min(self.tx_data.len - self.tx_end, data.len); - if (write_count > 0) { - self.tx.write_assume_capacity(data[0..write_count]); - } else { - return data[0..]; - } + if (len == 0) return 0; - if (self.tx.get_writable_len() == 0) { - _ = self.write_flush(); - } + @memcpy(self.tx_data[self.tx_end..][0..len], data[0..len]); + self.tx_end += @intCast(len); - return data[write_count..]; - } + if (self.tx_end == self.tx_data.len) + _ = self.flush(); - pub fn write_flush(self: *@This()) usize { - if (self.tx.get_readable_len() == 0) { - return 0; - } - const len = self.tx.read(&self.epin_buf); - self.device.start_tx(self.ep_in, self.epin_buf[0..len]); - self.last_len = @intCast(len); return len; } + pub fn flush(self: *@This()) bool { + if (self.tx_end == 0) + return true; + + const ep_in = @atomicLoad(types.Endpoint.Num, &self.ep_in, .seq_cst); + if (ep_in == .ep0) + return false; + + @atomicStore(types.Endpoint.Num, &self.ep_in, .ep0, .seq_cst); + + self.device.start_tx(ep_in, self.tx_data[0..self.tx_end]); + self.tx_end = 0; + return true; + } + pub fn init(desc: *const Descriptor, device: *usb.DeviceInterface) @This() { defer device.ep_listen(desc.ep_out.endpoint.num, options.max_packet_size); return .{ .device = device, .ep_notif = desc.ep_notifi.endpoint.num, - .ep_in = desc.ep_in.endpoint.num, .line_coding = .{ .bit_rate = 115200, .stop_bits = 0, @@ -191,13 +190,15 @@ pub fn CdcClassDriver(options: Options) type { .data_bits = 8, }, + .ep_out = .ep0, .rx_data = undefined, .rx_seek = 0, .rx_end = 0, - .ep_out = .ep0, - .tx = .empty, - .last_len = 0, + .ep_in = desc.ep_in.endpoint.num, + .ep_in_original = desc.ep_in.endpoint.num, + .tx_data = undefined, + .tx_end = 0, }; } @@ -220,20 +221,13 @@ pub fn CdcClassDriver(options: Options) type { } pub fn on_rx(self: *@This(), ep_num: types.Endpoint.Num) void { - @atomicStore(types.Endpoint.Num, &self.ep_out, ep_num, .release); + @atomicStore(types.Endpoint.Num, &self.ep_out, ep_num, .seq_cst); } pub fn on_tx_ready(self: *@This(), ep_num: types.Endpoint.Num) void { - if (ep_num != self.ep_in) return; - - if (self.write_flush() == 0) { - // If there is no data left, a empty packet should be sent if - // data len is multiple of EP Packet size and not zero - if (self.tx.get_readable_len() == 0 and self.last_len == options.max_packet_size) { - self.device.start_tx(self.ep_in, usb.ack); - self.last_len = 0; - } - } + if (ep_num != self.ep_in_original) return; + + @atomicStore(types.Endpoint.Num, &self.ep_in, ep_num, .seq_cst); } }; } diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 2d39844db..ec28753ae 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -88,7 +88,7 @@ pub fn main() !void { old = new; led.toggle(); i += 1; - std.log.info("cdc test: {}\r\n", .{i}); + std.log.info("cdc test: {}", .{i}); usb_cdc_write(&drivers.serial, "This is very very long text sent from RP Pico by USB CDC to your device: {}\r\n", .{i}); } @@ -111,12 +111,12 @@ pub fn usb_cdc_write(serial: *UsbSerial, comptime fmt: []const u8, args: anytype var write_buff = text; while (write_buff.len > 0) { - write_buff = serial.write(write_buff); + write_buff = write_buff[serial.write(write_buff)..]; usb_dev.poll(); } // Short messages are not sent right away; instead, they accumulate in a buffer, so we have to force a flush to send them - _ = serial.write_flush(); - usb_dev.poll(); + while (!serial.flush()) + usb_dev.poll(); } var usb_rx_buff: [1024]u8 = undefined; From 503c998b0ac7107f4c4d11754f6885f3ae6261c2 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 10 Jan 2026 21:59:36 +0100 Subject: [PATCH 08/38] allow partial usb tx and require calling ep_writev and ep_listen only once per packet --- core/src/core/usb.zig | 64 ++++++++++++------------- core/src/core/usb/drivers/cdc.zig | 3 +- port/raspberrypi/rp2xxx/src/hal/usb.zig | 53 +++++++------------- 3 files changed, 49 insertions(+), 71 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index b4df180f4..8e91fc904 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -16,20 +16,23 @@ pub const nak: ?[]const u8 = null; /// Any device implementation used with DeviceController must implement those functions pub const DeviceInterface = struct { pub const VTable = struct { - start_tx: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, buffer: []const u8) void, - ep_readv: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, data: []const []u8) types.Len, - ep_listen: *const fn (self: *DeviceInterface, ep_num: types.Endpoint.Num, len: types.Len) void, - endpoint_open: *const fn (self: *DeviceInterface, desc: *const descriptor.Endpoint) void, - set_address: *const fn (self: *DeviceInterface, addr: u7) void, + ep_writev: *const fn (*DeviceInterface, types.Endpoint.Num, []const []const u8) types.Len, + ep_readv: *const fn (*DeviceInterface, types.Endpoint.Num, []const []u8) types.Len, + ep_listen: *const fn (*DeviceInterface, types.Endpoint.Num, types.Len) void, + endpoint_open: *const fn (*DeviceInterface, *const descriptor.Endpoint) void, + set_address: *const fn (*DeviceInterface, u7) void, }; vtable: *const VTable, /// Called by drivers to send a packet. /// Submitting an empty slice signals an ACK. - /// If you intend to send ACK, please use the constant `usb.ack`. - pub fn start_tx(self: *@This(), ep_num: types.Endpoint.Num, buffer: []const u8) void { - return self.vtable.start_tx(self, ep_num, buffer); + pub fn ep_writev(self: *@This(), ep_num: types.Endpoint.Num, data: []const []const u8) types.Len { + return self.vtable.ep_writev(self, ep_num, data); + } + + pub fn ep_ack(self: *@This(), ep_num: types.Endpoint.Num) void { + assert(0 == self.ep_writev(ep_num, &.{ack})); } /// Called by drivers to retrieve a received packet. @@ -190,7 +193,7 @@ pub fn DeviceController(config: Config) type { /// 0 - no config set cfg_num: u16, /// Ep0 data waiting to be sent - tx_slice: []const u8, + tx_slice: ?[]const u8, /// Last setup packet request setup_packet: types.SetupPacket, /// Class driver associated with last setup request if any @@ -202,7 +205,7 @@ pub fn DeviceController(config: Config) type { pub const init: @This() = .{ .new_address = null, .cfg_num = 0, - .tx_slice = "", + .tx_slice = null, .setup_packet = undefined, .driver_last = null, .driver_data = null, @@ -267,30 +270,22 @@ pub fn DeviceController(config: Config) type { self.new_address = null; } - if (buffer.len > 0 and self.tx_slice.len > 0) { - self.tx_slice = self.tx_slice[buffer.len..]; - - const next_data_chunk = self.tx_slice[0..@min(64, self.tx_slice.len)]; - if (next_data_chunk.len > 0) { - device_itf.start_tx(.ep0, next_data_chunk); + if (self.tx_slice) |slice| { + if (slice.len > 0) { + const len = device_itf.ep_writev(.ep0, &.{slice}); + self.tx_slice = slice[len..]; } else { - device_itf.ep_listen(.ep0, 0); + // device_itf.ep_listen(.ep0, 0); + self.tx_slice = null; if (self.driver_last) |drv| self.driver_class_control(device_itf, drv, .Ack, &self.setup_packet); } - } else { - // Otherwise, we've just finished sending - // something to the host. We expect an ensuing - // status phase where the host sends us (via EP0 - // OUT) a zero-byte DATA packet, so, set that - // up: - device_itf.ep_listen(.ep0, 0); - - if (self.driver_last) |drv| - self.driver_class_control(device_itf, drv, .Ack, &self.setup_packet); } - } else if (comptime handler.driver.len != 0) { + } else if (comptime ep == types.Endpoint.out(.ep0)) { + log.info("ep0_out {}", .{buffer.len}); + } + if (comptime handler.driver.len != 0) { const drv = &@field(self.driver_data.?, handler.driver); @field(@FieldType(config0.Drivers, handler.driver), handler.function)(drv, ep.num); } @@ -308,9 +303,10 @@ pub fn DeviceController(config: Config) type { /// Command response utility function that can split long data in multiple packets fn send_cmd_response(self: *@This(), device_itf: *DeviceInterface, data: []const u8, expected_max_length: u16) void { - self.tx_slice = data[0..@min(data.len, expected_max_length)]; - const len = @min(config.device_descriptor.max_packet_size0, self.tx_slice.len); - device_itf.start_tx(.ep0, data[0..len]); + const limited = data[0..@min(data.len, expected_max_length)]; + const len = device_itf.ep_writev(.ep0, &.{limited}); + assert(len <= config.device_descriptor.max_packet_size0); + self.tx_slice = limited[len..]; } fn driver_class_control(self: *@This(), device_itf: *DeviceInterface, driver: DriverEnum, stage: types.ControlStage, setup: *const types.SetupPacket) void { @@ -333,7 +329,7 @@ pub fn DeviceController(config: Config) type { switch (std.meta.intToEnum(types.SetupRequest, setup.request) catch return) { .SetAddress => { self.new_address = @as(u8, @intCast(setup.value & 0xff)); - device_itf.start_tx(.ep0, ack); + device_itf.ep_ack(.ep0); if (config.debug) log.info(" SetAddress: {}", .{self.new_address.?}); }, .SetConfiguration => { @@ -348,7 +344,7 @@ pub fn DeviceController(config: Config) type { // TODO: call umount callback if any } } - device_itf.start_tx(.ep0, ack); + device_itf.ep_ack(.ep0); }, .GetDescriptor => { const descriptor_type = std.meta.intToEnum(descriptor.Type, setup.value >> 8) catch null; @@ -359,7 +355,7 @@ pub fn DeviceController(config: Config) type { .SetFeature => { if (std.meta.intToEnum(types.FeatureSelector, setup.value >> 8)) |feat| { switch (feat) { - .DeviceRemoteWakeup, .EndpointHalt => device_itf.start_tx(.ep0, ack), + .DeviceRemoteWakeup, .EndpointHalt => device_itf.ep_ack(.ep0), // TODO: https://github.com/ZigEmbeddedGroup/microzig/issues/453 .TestMode => {}, } diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 386ebb265..b1b20b8b4 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -1,5 +1,6 @@ const std = @import("std"); const usb = @import("../../usb.zig"); +const assert = std.debug.assert; const descriptor = usb.descriptor; const types = usb.types; @@ -173,7 +174,7 @@ pub fn CdcClassDriver(options: Options) type { @atomicStore(types.Endpoint.Num, &self.ep_in, .ep0, .seq_cst); - self.device.start_tx(ep_in, self.tx_data[0..self.tx_end]); + assert(self.tx_end == self.device.ep_writev(ep_in, &.{self.tx_data[0..self.tx_end]})); self.tx_end = 0; return true; } diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 063c9d8f2..2ef57bdc5 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -21,7 +21,6 @@ pub const Config = struct { }; const HardwareEndpointData = struct { - awaiting_rx: bool, data_buffer: []align(64) u8, }; @@ -86,7 +85,7 @@ pub fn Polled( return struct { const vtable: usb.DeviceInterface.VTable = .{ - .start_tx = start_tx, + .ep_writev = ep_writev, .ep_readv = ep_readv, .ep_listen = ep_listen, .set_address = set_address, @@ -147,9 +146,6 @@ pub fn Polled( const len = buffer_control[ep_num].get(ep.dir).read().LENGTH_0; self.controller.on_buffer(&self.interface, ep, ep_hard.data_buffer[0..len]); - - if (ep.dir == .Out) - ep_hard.awaiting_rx = false; } } @@ -248,6 +244,8 @@ pub fn Polled( // where the host will notice our presence. peripherals.USB.SIE_CTRL.modify(.{ .PULLUP_EN = 1 }); + self.interface.ep_listen(.ep0, 0); + return self; } @@ -256,38 +254,31 @@ pub fn Polled( /// The contents of `buffer` will be _copied_ into USB SRAM, so you can /// reuse `buffer` immediately after this returns. No need to wait for the /// packet to be sent. - fn start_tx( + fn ep_writev( itf: *usb.DeviceInterface, ep_num: usb.types.Endpoint.Num, - buffer: []const u8, - ) void { + data: []const []const u8, + ) usb.types.Len { const self: *@This() = @fieldParentPtr("interface", itf); - // It is technically possible to support longer buffers but this demo - // doesn't bother. - assert(buffer.len <= 64); - const bufctrl_ptr = &buffer_control[@intFromEnum(ep_num)].in; const ep = self.hardware_endpoint_get_by_address(.in(ep_num)); - // Wait for controller to give processor ownership of the buffer before writing it. - // This is technically not necessary, but the usb cdc driver is bugged. - while (bufctrl_ptr.read().AVAILABLE_0 == 1) {} - const len = buffer.len; + const len = @min(data[0].len, ep.data_buffer.len); switch (chip) { - .RP2040 => @memcpy(ep.data_buffer[0..len], buffer[0..len]), + .RP2040 => @memcpy(ep.data_buffer[0..len], data[0][0..len]), .RP2350 => { const dst: [*]align(4) u32 = @ptrCast(ep.data_buffer.ptr); - const src: [*]align(1) const u32 = @ptrCast(buffer.ptr); + const src: [*]align(1) const u32 = @ptrCast(data[0].ptr); for (0..len / 4) |i| dst[i] = src[i]; for (0..len % 4) |i| - ep.data_buffer[len - i - 1] = buffer[len - i - 1]; + ep.data_buffer[len - i - 1] = data[0][len - i - 1]; }, } var bufctrl = bufctrl_ptr.read(); - + assert(bufctrl.AVAILABLE_0 == 0); // Write the buffer information to the buffer control register bufctrl.PID_0 ^= 1; // flip DATA0/1 bufctrl.FULL_0 = 1; // We have put data in @@ -304,6 +295,8 @@ pub fn Polled( // Set available bit bufctrl.AVAILABLE_0 = 1; bufctrl_ptr.write(bufctrl); + + return @intCast(len); } fn ep_readv( @@ -330,27 +323,18 @@ pub fn Polled( } fn ep_listen( - itf: *usb.DeviceInterface, + _: *usb.DeviceInterface, ep_num: usb.types.Endpoint.Num, len: usb.types.Len, ) void { - const self: *@This() = @fieldParentPtr("interface", itf); - - // It is technically possible to support longer buffers but this demo doesn't bother. - assert(len <= 64); - const bufctrl_ptr = &buffer_control[@intFromEnum(ep_num)].out; - const ep = self.hardware_endpoint_get_by_address(.out(ep_num)); - // This function should only be called when the buffer is known to be available, - // but the current driver implementations do not conform to that. - if (ep.awaiting_rx) return; - - // Configure the OUT: var bufctrl = bufctrl_ptr.read(); + assert(bufctrl.AVAILABLE_0 == 0); + // Configure the OUT: bufctrl.PID_0 ^= 1; // Flip DATA0/1 bufctrl.FULL_0 = 0; // Buffer is NOT full, we want the computer to fill it - bufctrl.LENGTH_0 = @intCast(len); // Up tho this many bytes + bufctrl.LENGTH_0 = @intCast(@min(len, 64)); // Up tho this many bytes if (config.sync_noops != 0) { bufctrl_ptr.write(bufctrl); @@ -363,8 +347,6 @@ pub fn Polled( // Set available bit bufctrl.AVAILABLE_0 = 1; bufctrl_ptr.write(bufctrl); - - ep.awaiting_rx = true; } /// Returns a received USB setup packet @@ -405,7 +387,6 @@ pub fn Polled( const ep_hard = self.hardware_endpoint_get_by_address(ep); assert(desc.max_packet_size.into() <= 64); - ep_hard.awaiting_rx = false; buffer_control[@intFromEnum(ep.num)].get(ep.dir).modify(.{ .PID_0 = 1 }); From 93ce5961bf01503a490810ff3d7cadc64e8b4a59 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 10 Jan 2026 23:35:35 +0100 Subject: [PATCH 09/38] documentation and configurable handler names --- core/src/core/usb.zig | 27 ++++++++++------ core/src/core/usb/drivers/cdc.zig | 41 ++++++++++++++++--------- core/src/core/usb/drivers/hid.zig | 7 ++++- port/raspberrypi/rp2xxx/src/hal/usb.zig | 1 + 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 8e91fc904..1127450bb 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -26,22 +26,23 @@ pub const DeviceInterface = struct { vtable: *const VTable, /// Called by drivers to send a packet. - /// Submitting an empty slice signals an ACK. pub fn ep_writev(self: *@This(), ep_num: types.Endpoint.Num, data: []const []const u8) types.Len { return self.vtable.ep_writev(self, ep_num, data); } + /// Send ack on given IN endpoint. pub fn ep_ack(self: *@This(), ep_num: types.Endpoint.Num) void { assert(0 == self.ep_writev(ep_num, &.{ack})); } /// Called by drivers to retrieve a received packet. - /// Must be called exactly once before each packet. + /// Must be called exactly once for each packet. /// Buffers in `data` must collectively be long enough to fit the whole packet. pub fn ep_readv(self: *@This(), ep_num: types.Endpoint.Num, data: []const []u8) types.Len { return self.vtable.ep_readv(self, ep_num, data); } + /// Called by drivers to report readiness to receive up to `len` bytes. pub fn ep_listen(self: *@This(), ep_num: types.Endpoint.Num, len: types.Len) void { return self.vtable.ep_listen(self, ep_num, len); } @@ -162,7 +163,7 @@ pub fn DeviceController(config: Config) type { } }){}; }; - const handlers = blk: { + const endpoint_handlers = blk: { var ret: struct { In: [16]Handler, Out: [16]Handler } = .{ .In = @splat(.{ .driver = "", .function = "" }), .Out = @splat(.{ .driver = "", .function = "" }), @@ -173,14 +174,17 @@ pub fn DeviceController(config: Config) type { for (fields) |fld| { if (fld.type != descriptor.Endpoint) continue; const desc: descriptor.Endpoint = @field(cfg, fld.name); - const handler = &@field(ret, @tagName(desc.endpoint.dir))[@intFromEnum(desc.endpoint.num)]; - // assert(handler.driver.len == 0 and handler.function.len == 0); + const ep_num = @intFromEnum(desc.endpoint.num); + const handler = &@field(ret, @tagName(desc.endpoint.dir))[ep_num]; + const function = @field(fld_drv.type.handlers, fld.name); + if (handler.driver.len != 0 or handler.function.len != 0) + @compileError(std.fmt.comptimePrint( + "ep{} {t}: multiple handlers: {s}.{s} and {s}.{s}", + .{ ep_num, desc.endpoint.dir, handler.driver, handler.function, fld_drv.name, function }, + )); handler.* = .{ .driver = fld_drv.name, - .function = switch (desc.endpoint.dir) { - .In => "on_tx_ready", - .Out => "on_rx", - }, + .function = function, }; } } @@ -257,7 +261,7 @@ pub fn DeviceController(config: Config) type { if (config.debug) log.info("buff status", .{}); if (config.debug) log.info(" data: {any}", .{buffer}); - const handler = comptime @field(handlers, @tagName(ep.dir))[@intFromEnum(ep.num)]; + const handler = comptime @field(endpoint_handlers, @tagName(ep.dir))[@intFromEnum(ep.num)]; if (comptime ep == types.Endpoint.in(.ep0)) { if (config.debug) log.info(" EP0_IN_ADDR", .{}); @@ -275,6 +279,9 @@ pub fn DeviceController(config: Config) type { const len = device_itf.ep_writev(.ep0, &.{slice}); self.tx_slice = slice[len..]; } else { + // Otherwise, we've just finished sending tx_slice. + // We expect an ensuing status phase where the host + // sends us a zero-byte DATA packet via EP0 OUT. // device_itf.ep_listen(.ep0, 0); self.tx_slice = null; diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index b1b20b8b4..003e92a8c 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -3,6 +3,7 @@ const usb = @import("../../usb.zig"); const assert = std.debug.assert; const descriptor = usb.descriptor; const types = usb.types; +const EpNum = types.Endpoint.Num; pub const ManagementRequestType = enum(u8) { SetLineCoding = 0x20, @@ -109,21 +110,27 @@ pub fn CdcClassDriver(options: Options) type { } }; + pub const handlers = .{ + .ep_notifi = "on_notifi_ready", + .ep_out = "on_rx", + .ep_in = "on_tx_ready", + }; + device: *usb.DeviceInterface, - ep_notif: types.Endpoint.Num, + ep_notifi: EpNum, line_coding: LineCoding align(4), /// OUT endpoint on which there is data ready to be read, /// or .ep0 when no data is available. - ep_out: types.Endpoint.Num, + ep_out: EpNum, rx_data: [options.max_packet_size]u8, rx_seek: types.Len, rx_end: types.Len, /// IN endpoint where data can be sent, /// or .ep0 when data is being sent. - ep_in: types.Endpoint.Num, - ep_in_original: types.Endpoint.Num, + ep_in: EpNum, + ep_in_original: EpNum, tx_data: [options.max_packet_size]u8, tx_end: types.Len, @@ -139,11 +146,11 @@ pub fn CdcClassDriver(options: Options) type { if (self.available() > 0) return len; // request more data - const ep_out = @atomicLoad(types.Endpoint.Num, &self.ep_out, .seq_cst); + const ep_out = @atomicLoad(EpNum, &self.ep_out, .seq_cst); if (ep_out != .ep0) { self.rx_end = self.device.ep_readv(ep_out, &.{&self.rx_data}); self.rx_seek = 0; - @atomicStore(types.Endpoint.Num, &self.ep_out, .ep0, .seq_cst); + @atomicStore(EpNum, &self.ep_out, .ep0, .seq_cst); self.device.ep_listen(ep_out, options.max_packet_size); } @@ -164,15 +171,16 @@ pub fn CdcClassDriver(options: Options) type { return len; } + /// Returns true if flush operation succeded. pub fn flush(self: *@This()) bool { if (self.tx_end == 0) return true; - const ep_in = @atomicLoad(types.Endpoint.Num, &self.ep_in, .seq_cst); + const ep_in = @atomicLoad(EpNum, &self.ep_in, .seq_cst); if (ep_in == .ep0) return false; - @atomicStore(types.Endpoint.Num, &self.ep_in, .ep0, .seq_cst); + @atomicStore(EpNum, &self.ep_in, .ep0, .seq_cst); assert(self.tx_end == self.device.ep_writev(ep_in, &.{self.tx_data[0..self.tx_end]})); self.tx_end = 0; @@ -183,7 +191,7 @@ pub fn CdcClassDriver(options: Options) type { defer device.ep_listen(desc.ep_out.endpoint.num, options.max_packet_size); return .{ .device = device, - .ep_notif = desc.ep_notifi.endpoint.num, + .ep_notifi = desc.ep_notifi.endpoint.num, .line_coding = .{ .bit_rate = 115200, .stop_bits = 0, @@ -221,14 +229,19 @@ pub fn CdcClassDriver(options: Options) type { return usb.nak; } - pub fn on_rx(self: *@This(), ep_num: types.Endpoint.Num) void { - @atomicStore(types.Endpoint.Num, &self.ep_out, ep_num, .seq_cst); + pub fn on_rx(self: *@This(), ep_num: EpNum) void { + assert(EpNum.ep0 == @atomicLoad(EpNum, &self.ep_out, .seq_cst)); + @atomicStore(EpNum, &self.ep_out, ep_num, .seq_cst); } - pub fn on_tx_ready(self: *@This(), ep_num: types.Endpoint.Num) void { - if (ep_num != self.ep_in_original) return; + pub fn on_tx_ready(self: *@This(), ep_num: EpNum) void { + assert(EpNum.ep0 == @atomicLoad(EpNum, &self.ep_in, .seq_cst)); + @atomicStore(EpNum, &self.ep_in, ep_num, .seq_cst); + } - @atomicStore(types.Endpoint.Num, &self.ep_in, ep_num, .seq_cst); + pub fn on_notifi_ready(self: *@This(), ep_num: EpNum) void { + assert(EpNum.ep0 == @atomicLoad(EpNum, &self.ep_notifi, .seq_cst)); + @atomicStore(EpNum, &self.ep_notifi, ep_num, .seq_cst); } }; } diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index e8896db6f..8df44a778 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -35,7 +35,7 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { }, .hid = hid_descriptor, .ep_out = .{ - .endpoint = .in(@enumFromInt(first_endpoint_out)), + .endpoint = .out(@enumFromInt(first_endpoint_out)), .attributes = .{ .transfer_type = .Interrupt, .usage = .data }, .max_packet_size = .from(options.max_packet_size), .interval = options.endpoint_interval, @@ -57,6 +57,11 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { .report_length = .from(@sizeOf(@TypeOf(report_descriptor))), }; + pub const handlers = .{ + .ep_out = "on_rx", + .ep_in = "on_tx_ready", + }; + device: *usb.DeviceInterface, ep_in: types.Endpoint.Num, ep_out: types.Endpoint.Num, diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 2ef57bdc5..9cbfd181f 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -244,6 +244,7 @@ pub fn Polled( // where the host will notice our presence. peripherals.USB.SIE_CTRL.modify(.{ .PULLUP_EN = 1 }); + // Listen for ACKs self.interface.ep_listen(.ep0, 0); return self; From f21007636d0ca6b90c6e8f268d7ff94537959a5a Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 10 Jan 2026 23:54:51 +0100 Subject: [PATCH 10/38] first draft of an example driver that explains the comptime driver interface --- core/src/core/usb.zig | 1 + core/src/core/usb/drivers/example.zig | 96 +++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 core/src/core/usb/drivers/example.zig diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 1127450bb..75cfe31ea 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -5,6 +5,7 @@ const log = std.log.scoped(.usb); pub const descriptor = @import("usb/descriptor.zig"); pub const drivers = struct { pub const cdc = @import("usb/drivers/cdc.zig"); + pub const example = @import("usb/drivers/example.zig"); pub const hid = @import("usb/drivers/hid.zig"); }; pub const types = @import("usb/types.zig"); diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig new file mode 100644 index 000000000..b074a6b6d --- /dev/null +++ b/core/src/core/usb/drivers/example.zig @@ -0,0 +1,96 @@ +const std = @import("std"); +const usb = @import("../../usb.zig"); +const descriptor = usb.descriptor; + +pub const ExampleDriver = struct { + /// The descriptors need to have the same memory layout as the sent data. + pub const Descriptor = extern struct { + example_interface: descriptor.Interface, + ep_in1: descriptor.Endpoint, + ep_in2: descriptor.Endpoint, + ep_out: descriptor.Endpoint, + + /// This function is used during descriptor creation. If multiple instances + /// of a driver are used, a descriptor will be created for each. + pub fn create( + first_interface: u8, + first_string: u8, + first_endpoint_in: u4, + first_endpoint_out: u4, + ) @This() { + return .{ + .example_interface = .{ + .interface_number = first_interface + 1, + .alternate_setting = 0, + .num_endpoints = 2, + .interface_class = 10, + .interface_subclass = 0, + .interface_protocol = 0, + .interface_s = first_string, + }, + .ep_in1 = .{ + .endpoint = .in(@enumFromInt(first_endpoint_in)), + .attributes = .{ .transfer_type = .Interrupt, .usage = .data }, + .max_packet_size = .from(64), + .interval = 16, + }, + .ep_in2 = .{ + .endpoint = .in(@enumFromInt(first_endpoint_in + 1)), + .attributes = .{ .transfer_type = .Bulk, .usage = .data }, + .max_packet_size = .from(64), + .interval = 0, + }, + .ep_out = .{ + .endpoint = .out(@enumFromInt(first_endpoint_out)), + .attributes = .{ .transfer_type = .Bulk, .usage = .data }, + .max_packet_size = .from(64), + .interval = 0, + }, + }; + } + }; + + /// This is a mapping from endpoint descriptor field names + /// to handler function names. + pub const handlers = .{ + .ep_in1 = "handler1", + .ep_in2 = "handler2", + .ep_out = "handler3", + }; + + /// This function is called when the host chooses a configuration + /// that contains this driver. + pub fn init(desc: *const Descriptor, device: *usb.DeviceInterface) @This() { + defer device.ep_listen(desc.ep_out.endpoint.num, 64); + return .{}; + } + + /// Used for configuration through endpoint 0. + /// Data returned by this function is sent on endpoint 0. + pub fn class_control(self: *@This(), stage: usb.types.ControlStage, setup: *const usb.types.SetupPacket) ?[]const u8 { + _ = self; + _ = setup; + if (stage == .Setup) + return usb.ack + else + return usb.nak; + } + + /// Each endpoint (as defined in the descriptor) has its own handler. + /// Endpoint number is passed as an argument so that it does not need + /// to be stored in the driver. + pub fn handler1(self: *@This(), ep_num: usb.types.Endpoint.Num) void { + _ = self; + _ = ep_num; + } + + pub fn handler2(self: *@This(), ep_num: usb.types.Endpoint.Num) void { + _ = self; + _ = ep_num; + } + + pub fn handler3(self: *@This(), ep_num: usb.types.Endpoint.Num) void { + _ = self; + _ = ep_num; + } +}; From 942aa42d0eb7e8a6e9013cb5b3474a69b371383c Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sun, 11 Jan 2026 00:26:52 +0100 Subject: [PATCH 11/38] more error checking around interfaces and string descriptors --- core/src/core/usb.zig | 49 +++++++++++++++------------ core/src/core/usb/drivers/example.zig | 6 ++-- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 75cfe31ea..7d759a929 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -139,6 +139,12 @@ pub fn DeviceController(config: Config) type { } } + if (num_strings != config.string_descriptors.len) + @compileError(std.fmt.comptimePrint( + "expected {} string descriptros, got {}", + .{ num_strings, config.string_descriptors.len }, + )); + const desc_cfg: descriptor.Configuration = .{ .total_length = .from(size), .num_interfaces = num_interfaces, @@ -164,14 +170,28 @@ pub fn DeviceController(config: Config) type { } }){}; }; - const endpoint_handlers = blk: { - var ret: struct { In: [16]Handler, Out: [16]Handler } = .{ + const handlers = blk: { + var ret: struct { In: [16]Handler, Out: [16]Handler, itf: []const DriverEnum } = .{ .In = @splat(.{ .driver = "", .function = "" }), .Out = @splat(.{ .driver = "", .function = "" }), + .itf = &.{}, }; + var itf_handlers = ret.itf; for (driver_fields) |fld_drv| { const cfg = @field(config_descriptor, fld_drv.name); const fields = @typeInfo(@TypeOf(cfg)).@"struct".fields; + + const itf0 = @field(cfg, fields[0].name); + const itf_start, const itf_count = if (fields[0].type == descriptor.InterfaceAssociation) + .{ itf0.first_interface, itf0.interface_count } + else + .{ itf0.interface_number, 1 }; + + if (itf_start != itf_handlers.len) + @compileError("interface numbering mismatch"); + + itf_handlers = itf_handlers ++ &[1]DriverEnum{@field(DriverEnum, fld_drv.name)} ** itf_count; + for (fields) |fld| { if (fld.type != descriptor.Endpoint) continue; const desc: descriptor.Endpoint = @field(cfg, fld.name); @@ -189,6 +209,7 @@ pub fn DeviceController(config: Config) type { }; } } + ret.itf = itf_handlers; break :blk ret; }; @@ -232,24 +253,10 @@ pub fn DeviceController(config: Config) type { switch (setup.request_type.recipient) { .Device => try self.process_setup_request(device_itf, setup), .Interface => switch (@as(u8, @truncate(setup.index))) { - inline else => |itf_num| inline for (driver_fields) |fld_drv| { - const cfg = @field(config_descriptor, fld_drv.name); - comptime var fields = @typeInfo(@TypeOf(cfg)).@"struct".fields; - - const itf_count = if (fields[0].type != descriptor.InterfaceAssociation) - 1 - else blk: { - defer fields = fields[1..]; - break :blk @field(cfg, fields[0].name).interface_count; - }; - - const itf_start = @field(cfg, fields[0].name).interface_number; - - if (comptime itf_num >= itf_start and itf_num < itf_start + itf_count) { - const drv = @field(DriverEnum, fld_drv.name); - self.driver_last = drv; - self.driver_class_control(device_itf, drv, .Setup, setup); - } + inline else => |itf_num| if (itf_num < handlers.itf.len) { + const drv = handlers.itf[itf_num]; + self.driver_last = drv; + self.driver_class_control(device_itf, drv, .Setup, setup); }, }, .Endpoint => {}, @@ -262,7 +269,7 @@ pub fn DeviceController(config: Config) type { if (config.debug) log.info("buff status", .{}); if (config.debug) log.info(" data: {any}", .{buffer}); - const handler = comptime @field(endpoint_handlers, @tagName(ep.dir))[@intFromEnum(ep.num)]; + const handler = comptime @field(handlers, @tagName(ep.dir))[@intFromEnum(ep.num)]; if (comptime ep == types.Endpoint.in(.ep0)) { if (config.debug) log.info(" EP0_IN_ADDR", .{}); diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index b074a6b6d..722c2d889 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -20,10 +20,10 @@ pub const ExampleDriver = struct { ) @This() { return .{ .example_interface = .{ - .interface_number = first_interface + 1, + .interface_number = first_interface, .alternate_setting = 0, - .num_endpoints = 2, - .interface_class = 10, + .num_endpoints = 3, + .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_s = first_string, From 340d9c3ab9b712d49956027e70e3d04afd60715b Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sun, 11 Jan 2026 00:38:35 +0100 Subject: [PATCH 12/38] endpoint_open -> ep_open for consistency --- core/src/core/usb.zig | 8 ++++---- port/raspberrypi/rp2xxx/src/hal/usb.zig | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 7d759a929..00a4a39ea 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -20,7 +20,7 @@ pub const DeviceInterface = struct { ep_writev: *const fn (*DeviceInterface, types.Endpoint.Num, []const []const u8) types.Len, ep_readv: *const fn (*DeviceInterface, types.Endpoint.Num, []const []u8) types.Len, ep_listen: *const fn (*DeviceInterface, types.Endpoint.Num, types.Len) void, - endpoint_open: *const fn (*DeviceInterface, *const descriptor.Endpoint) void, + ep_open: *const fn (*DeviceInterface, *const descriptor.Endpoint) void, set_address: *const fn (*DeviceInterface, u7) void, }; @@ -52,8 +52,8 @@ pub const DeviceInterface = struct { /// direction is IN this may call the controller's `on_buffer` function, /// so driver initialization must be done before this function is called /// on IN endpoint descriptors. - pub fn endpoint_open(self: *@This(), desc: *const descriptor.Endpoint) void { - return self.vtable.endpoint_open(self, desc); + pub fn ep_open(self: *@This(), desc: *const descriptor.Endpoint) void { + return self.vtable.ep_open(self, desc); } /// Immediately sets the device address. @@ -434,7 +434,7 @@ pub fn DeviceController(config: Config) type { inline for (@typeInfo(@TypeOf(cfg)).@"struct".fields) |fld| { if (comptime fld.type == descriptor.Endpoint) - device_itf.endpoint_open(&@field(cfg, fld.name)); + device_itf.ep_open(&@field(cfg, fld.name)); } } } diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 9cbfd181f..145ae1b01 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -88,8 +88,8 @@ pub fn Polled( .ep_writev = ep_writev, .ep_readv = ep_readv, .ep_listen = ep_listen, + .ep_open = ep_open, .set_address = set_address, - .endpoint_open = endpoint_open, }; endpoints: [config.max_endpoints_count][2]HardwareEndpointData, @@ -227,13 +227,13 @@ pub fn Polled( }; @memset(std.mem.asBytes(&self.endpoints), 0); - endpoint_open(&self.interface, &.{ + ep_open(&self.interface, &.{ .endpoint = .in(.ep0), .max_packet_size = .from(64), .attributes = .{ .transfer_type = .Control, .usage = .data }, .interval = 0, }); - endpoint_open(&self.interface, &.{ + ep_open(&self.interface, &.{ .endpoint = .out(.ep0), .max_packet_size = .from(64), .attributes = .{ .transfer_type = .Control, .usage = .data }, @@ -379,7 +379,7 @@ pub fn Polled( return &self.endpoints[@intFromEnum(ep.num)][@intFromEnum(ep.dir)]; } - fn endpoint_open(itf: *usb.DeviceInterface, desc: *const usb.descriptor.Endpoint) void { + fn ep_open(itf: *usb.DeviceInterface, desc: *const usb.descriptor.Endpoint) void { const self: *@This() = @fieldParentPtr("interface", itf); assert(@intFromEnum(desc.endpoint.num) <= config.max_endpoints_count); From 9eabcabd34c1b30611a46de235a27684994bc78c Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sun, 11 Jan 2026 00:46:43 +0100 Subject: [PATCH 13/38] style fixes --- core/src/core/usb.zig | 11 +++++----- core/src/core/usb/drivers/cdc.zig | 35 +++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 00a4a39ea..442868e63 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -44,6 +44,7 @@ pub const DeviceInterface = struct { } /// Called by drivers to report readiness to receive up to `len` bytes. + /// After being called it may not be called again until a packet is received. pub fn ep_listen(self: *@This(), ep_num: types.Endpoint.Num, len: types.Len) void { return self.vtable.ep_listen(self, ep_num, len); } @@ -78,11 +79,6 @@ pub const Config = struct { configurations: []const Configuration, }; -const Handler = struct { - driver: []const u8, - function: []const u8, -}; - /// USB device controller /// /// This code handles usb enumeration and configuration and routes packets to drivers. @@ -171,6 +167,11 @@ pub fn DeviceController(config: Config) type { }; const handlers = blk: { + const Handler = struct { + driver: []const u8, + function: []const u8, + }; + var ret: struct { In: [16]Handler, Out: [16]Handler, itf: []const DriverEnum } = .{ .In = @splat(.{ .driver = "", .function = "" }), .Out = @splat(.{ .driver = "", .function = "" }), diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 003e92a8c..a5999478a 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -3,7 +3,6 @@ const usb = @import("../../usb.zig"); const assert = std.debug.assert; const descriptor = usb.descriptor; const types = usb.types; -const EpNum = types.Endpoint.Num; pub const ManagementRequestType = enum(u8) { SetLineCoding = 0x20, @@ -117,20 +116,20 @@ pub fn CdcClassDriver(options: Options) type { }; device: *usb.DeviceInterface, - ep_notifi: EpNum, + ep_notifi: types.Endpoint.Num, line_coding: LineCoding align(4), /// OUT endpoint on which there is data ready to be read, /// or .ep0 when no data is available. - ep_out: EpNum, + ep_out: types.Endpoint.Num, rx_data: [options.max_packet_size]u8, rx_seek: types.Len, rx_end: types.Len, /// IN endpoint where data can be sent, /// or .ep0 when data is being sent. - ep_in: EpNum, - ep_in_original: EpNum, + ep_in: types.Endpoint.Num, + ep_in_original: types.Endpoint.Num, tx_data: [options.max_packet_size]u8, tx_end: types.Len, @@ -146,11 +145,11 @@ pub fn CdcClassDriver(options: Options) type { if (self.available() > 0) return len; // request more data - const ep_out = @atomicLoad(EpNum, &self.ep_out, .seq_cst); + const ep_out = @atomicLoad(types.Endpoint.Num, &self.ep_out, .seq_cst); if (ep_out != .ep0) { self.rx_end = self.device.ep_readv(ep_out, &.{&self.rx_data}); self.rx_seek = 0; - @atomicStore(EpNum, &self.ep_out, .ep0, .seq_cst); + @atomicStore(types.Endpoint.Num, &self.ep_out, .ep0, .seq_cst); self.device.ep_listen(ep_out, options.max_packet_size); } @@ -176,11 +175,11 @@ pub fn CdcClassDriver(options: Options) type { if (self.tx_end == 0) return true; - const ep_in = @atomicLoad(EpNum, &self.ep_in, .seq_cst); + const ep_in = @atomicLoad(types.Endpoint.Num, &self.ep_in, .seq_cst); if (ep_in == .ep0) return false; - @atomicStore(EpNum, &self.ep_in, .ep0, .seq_cst); + @atomicStore(types.Endpoint.Num, &self.ep_in, .ep0, .seq_cst); assert(self.tx_end == self.device.ep_writev(ep_in, &.{self.tx_data[0..self.tx_end]})); self.tx_end = 0; @@ -229,19 +228,19 @@ pub fn CdcClassDriver(options: Options) type { return usb.nak; } - pub fn on_rx(self: *@This(), ep_num: EpNum) void { - assert(EpNum.ep0 == @atomicLoad(EpNum, &self.ep_out, .seq_cst)); - @atomicStore(EpNum, &self.ep_out, ep_num, .seq_cst); + pub fn on_rx(self: *@This(), ep_num: types.Endpoint.Num) void { + assert(.ep0 == @atomicLoad(types.Endpoint.Num, &self.ep_out, .seq_cst)); + @atomicStore(types.Endpoint.Num, &self.ep_out, ep_num, .seq_cst); } - pub fn on_tx_ready(self: *@This(), ep_num: EpNum) void { - assert(EpNum.ep0 == @atomicLoad(EpNum, &self.ep_in, .seq_cst)); - @atomicStore(EpNum, &self.ep_in, ep_num, .seq_cst); + pub fn on_tx_ready(self: *@This(), ep_num: types.Endpoint.Num) void { + assert(.ep0 == @atomicLoad(types.Endpoint.Num, &self.ep_in, .seq_cst)); + @atomicStore(types.Endpoint.Num, &self.ep_in, ep_num, .seq_cst); } - pub fn on_notifi_ready(self: *@This(), ep_num: EpNum) void { - assert(EpNum.ep0 == @atomicLoad(EpNum, &self.ep_notifi, .seq_cst)); - @atomicStore(EpNum, &self.ep_notifi, ep_num, .seq_cst); + pub fn on_notifi_ready(self: *@This(), ep_num: types.Endpoint.Num) void { + assert(.ep0 == @atomicLoad(types.Endpoint.Num, &self.ep_notifi, .seq_cst)); + @atomicStore(types.Endpoint.Num, &self.ep_notifi, ep_num, .seq_cst); } }; } From 870f0d60e6a8e5fa626fbe8d53ec28b93bdaa881 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sun, 11 Jan 2026 01:08:14 +0100 Subject: [PATCH 14/38] appease linter bot --- core/src/core/usb.zig | 11 +++++------ core/src/core/usb/descriptor.zig | 16 ++++++++-------- core/src/core/usb/descriptor/cdc.zig | 2 +- core/src/core/usb/descriptor/hid.zig | 4 ++-- core/src/core/usb/drivers/cdc.zig | 4 +--- core/src/core/usb/drivers/hid.zig | 14 +++++++------- core/src/core/usb/types.zig | 2 +- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 8 ++++---- 8 files changed, 29 insertions(+), 32 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 442868e63..5f9db5d4e 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -351,14 +351,13 @@ pub fn DeviceController(config: Config) type { .SetConfiguration => { if (config.debug) log.info(" SetConfiguration", .{}); if (self.cfg_num != setup.value) { + // if (self.cfg_num > 0) + // deinitialize drivers + self.cfg_num = setup.value; - if (self.cfg_num > 0) { + if (self.cfg_num > 0) try self.process_set_config(device_itf, self.cfg_num - 1); - // TODO: call mount callback if any - } else { - // TODO: call umount callback if any - } } device_itf.ep_ack(.ep0); }, @@ -427,7 +426,7 @@ pub fn DeviceController(config: Config) type { } fn process_set_config(self: *@This(), device_itf: *DeviceInterface, _: u16) !void { - // TODO: we support just one config for now so ignore config index + // We support just one config for now so ignore config index self.driver_data = @as(config0.Drivers, undefined); inline for (driver_fields) |fld_drv| { const cfg = @field(config_descriptor, fld_drv.name); diff --git a/core/src/core/usb/descriptor.zig b/core/src/core/usb/descriptor.zig index 63777e064..2e22d83e2 100644 --- a/core/src/core/usb/descriptor.zig +++ b/core/src/core/usb/descriptor.zig @@ -57,7 +57,7 @@ pub const Device = extern struct { /// Type of this descriptor, must be `DeviceQualifier`. descriptor_type: Type = .DeviceQualifier, /// Specification version as Binary Coded Decimal - bcd_usb: types.U16Le, + bcd_usb: types.U16_Le, /// Class, subclass and protocol of device. device_triple: DeviceTriple, /// Maximum unit of data this device can move. @@ -77,17 +77,17 @@ pub const Device = extern struct { /// Type of this descriptor, must be `Device`. descriptor_type: Type = .Device, /// Specification version as Binary Coded Decimal - bcd_usb: types.U16Le, + bcd_usb: types.U16_Le, /// Class, subclass and protocol of device. device_triple: DeviceTriple, /// Maximum length of data this device can move. max_packet_size0: u8, /// ID of product vendor. - vendor: types.U16Le, + vendor: types.U16_Le, /// ID of product. - product: types.U16Le, + product: types.U16_Le, /// Device version number as Binary Coded Decimal. - bcd_device: types.U16Le, + bcd_device: types.U16_Le, /// Index of manufacturer name in string descriptor table. manufacturer_s: u8, /// Index of product name in string descriptor table. @@ -144,7 +144,7 @@ pub const Configuration = extern struct { /// Total length of all descriptors in this configuration, concatenated. /// This will include this descriptor, plus at least one interface /// descriptor, plus each interface descriptor's endpoint descriptors. - total_length: types.U16Le, + total_length: types.U16_Le, /// Number of interface descriptors in this configuration. num_interfaces: u8, /// Number to use when requesting this configuration via a @@ -171,7 +171,7 @@ pub const String = struct { const ret: *const extern struct { length: u8 = @sizeOf(@This()), descriptor_type: Type = .String, - lang: types.U16Le, + lang: types.U16_Le, } = comptime &.{ .lang = .from(@intFromEnum(lang)) }; return .{ .data = @ptrCast(ret) }; } @@ -221,7 +221,7 @@ pub const Endpoint = extern struct { /// control the transfer type using the values from `TransferType`. attributes: Attributes, /// Maximum packet size this endpoint can accept/produce. - max_packet_size: types.U16Le, + max_packet_size: types.U16_Le, /// Interval for polling interrupt/isochronous endpoints (which we don't /// currently support) in milliseconds. interval: u8, diff --git a/core/src/core/usb/descriptor/cdc.zig b/core/src/core/usb/descriptor/cdc.zig index 0d9f8e5fc..bf6478f59 100644 --- a/core/src/core/usb/descriptor/cdc.zig +++ b/core/src/core/usb/descriptor/cdc.zig @@ -22,7 +22,7 @@ pub const Header = extern struct { descriptor_subtype: SubType = .Header, // USB Class Definitions for Communication Devices Specification release // number in binary-coded decimal. Typically 0x01_10. - bcd_cdc: types.U16Le, + bcd_cdc: types.U16_Le, }; pub const CallManagement = extern struct { diff --git a/core/src/core/usb/descriptor/hid.zig b/core/src/core/usb/descriptor/hid.zig index c3ce76afb..a77224545 100644 --- a/core/src/core/usb/descriptor/hid.zig +++ b/core/src/core/usb/descriptor/hid.zig @@ -70,7 +70,7 @@ pub const Hid = extern struct { /// Type of this descriptor descriptor_type: descriptor.Type = .CsDevice, /// Numeric expression identifying the HID Class Specification release - bcd_hid: types.U16Le, + bcd_hid: types.U16_Le, /// Numeric expression identifying country code of the localized hardware country_code: CountryCode, /// Numeric expression specifying the number of class descriptors @@ -78,7 +78,7 @@ pub const Hid = extern struct { /// Type of HID class report report_type: Type = .Report, /// The total size of the Report descriptor - report_length: types.U16Le, + report_length: types.U16_Le, }; // +++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index a5999478a..79d5ba32c 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -129,7 +129,6 @@ pub fn CdcClassDriver(options: Options) type { /// IN endpoint where data can be sent, /// or .ep0 when data is being sent. ep_in: types.Endpoint.Num, - ep_in_original: types.Endpoint.Num, tx_data: [options.max_packet_size]u8, tx_end: types.Len, @@ -204,7 +203,6 @@ pub fn CdcClassDriver(options: Options) type { .rx_end = 0, .ep_in = desc.ep_in.endpoint.num, - .ep_in_original = desc.ep_in.endpoint.num, .tx_data = undefined, .tx_end = 0, }; @@ -213,7 +211,7 @@ pub fn CdcClassDriver(options: Options) type { pub fn class_control(self: *@This(), stage: types.ControlStage, setup: *const types.SetupPacket) ?[]const u8 { if (std.meta.intToEnum(ManagementRequestType, setup.request)) |request| { if (stage == .Setup) switch (request) { - .SetLineCoding => return usb.ack, // HACK, we should handle data phase somehow to read sent line_coding + .SetLineCoding => return usb.ack, // we should handle data phase somehow to read sent line_coding .GetLineCoding => return std.mem.asBytes(&self.line_coding), .SetControlLineState => { // const DTR_BIT = 1; diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index 8df44a778..f4a055f10 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -90,18 +90,19 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { const hid_request_type = std.meta.intToEnum(descriptor.hid.RequestType, setup.request) catch return usb.nak; switch (hid_request_type) { .SetIdle => { - // TODO: The host is attempting to limit bandwidth by requesting that + // TODO: https://github.com/ZigEmbeddedGroup/microzig/issues/454 + // The host is attempting to limit bandwidth by requesting that // the device only return report data when its values actually change, // or when the specified duration elapses. In practice, the device can // still send reports as often as it wants, but for completeness this // should be implemented eventually. // - // https://github.com/ZigEmbeddedGroup/microzig/issues/454 return usb.ack; }, .SetProtocol => { - // TODO: The device should switch the format of its reports from the - // boot keyboard/mouse protocol to the format described in its report descriptor, + // TODO: https://github.com/ZigEmbeddedGroup/microzig/issues/454 + // The device should switch the format of its reports from the boot + // keyboard/mouse protocol to the format described in its report descriptor, // or vice versa. // // For now, this request is ACKed without doing anything; in practice, @@ -109,15 +110,14 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { // Unless the report format matches the boot protocol exactly (see ReportDescriptorKeyboard), // our device might not work in a limited BIOS environment. // - // https://github.com/ZigEmbeddedGroup/microzig/issues/454 return usb.ack; }, .SetReport => { - // TODO: This request sends a feature or output report to the device, + // TODO: https://github.com/ZigEmbeddedGroup/microzig/issues/454 + // This request sends a feature or output report to the device, // e.g. turning on the caps lock LED. This must be handled in an // application-specific way, so notify the application code of the event. // - // https://github.com/ZigEmbeddedGroup/microzig/issues/454 return usb.ack; }, else => {}, diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index 342e9c044..9a87c98ae 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -146,7 +146,7 @@ pub const SetupPacket = extern struct { pub const Len = u11; /// u16 value, little endian regardless of native endianness. -pub const U16Le = extern struct { +pub const U16_Le = extern struct { value: [2]u8, pub fn from(val: u16) @This() { diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index ec28753ae..e1063c41b 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -11,7 +11,7 @@ const led = gpio.num(25); const uart = rp2xxx.uart.instance.num(0); const uart_tx_pin = gpio.num(0); -const UsbSerial = usb.drivers.cdc.CdcClassDriver(.{ .max_packet_size = 64 }); +const USB_Serial = usb.drivers.cdc.CdcClassDriver(.{ .max_packet_size = 64 }); var usb_dev: rp2xxx.usb.Polled( usb.Config{ @@ -43,7 +43,7 @@ var usb_dev: rp2xxx.usb.Polled( .configuration_s = 0, .attributes = .{ .self_powered = true }, .max_current_ma = 100, - .Drivers = struct { serial: UsbSerial }, + .Drivers = struct { serial: USB_Serial }, }}, }, .{}, @@ -106,7 +106,7 @@ var usb_tx_buff: [1024]u8 = undefined; // Transfer data to host // NOTE: After each USB chunk transfer, we have to call the USB task so that bus TX events can be handled -pub fn usb_cdc_write(serial: *UsbSerial, comptime fmt: []const u8, args: anytype) void { +pub fn usb_cdc_write(serial: *USB_Serial, comptime fmt: []const u8, args: anytype) void { const text = std.fmt.bufPrint(&usb_tx_buff, fmt, args) catch &.{}; var write_buff = text; @@ -124,7 +124,7 @@ var usb_rx_buff: [1024]u8 = undefined; // Receive data from host // NOTE: Read code was not tested extensively. In case of issues, try to call USB task before every read operation pub fn usb_cdc_read( - serial: *UsbSerial, + serial: *USB_Serial, ) []const u8 { var total_read: usize = 0; var read_buff: []u8 = usb_rx_buff[0..]; From 2a79c7888ebd81c80545437648d8819db8d0fa83 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sun, 11 Jan 2026 17:08:34 +0100 Subject: [PATCH 15/38] add u32 little endian wrapper --- core/src/core/usb/types.zig | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index 9a87c98ae..63c9e423d 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -147,15 +147,26 @@ pub const Len = u11; /// u16 value, little endian regardless of native endianness. pub const U16_Le = extern struct { - value: [2]u8, + value_le: u16 align(1), pub fn from(val: u16) @This() { - var self: @This() = undefined; - std.mem.writeInt(u16, &self.value, val, .little); - return self; + return .{ .value_le = std.mem.nativeToLittle(u16, val) }; } pub fn into(self: @This()) u16 { - return std.mem.readInt(u16, &self.value, .little); + return std.mem.littleToNative(u16, self.value_le); + } +}; + +/// u32 value, little endian regardless of native endianness. +pub const U32_Le = extern struct { + value_le: u32 align(1), + + pub fn from(val: u32) @This() { + return .{ .value_le = std.mem.nativeToLittle(u32, val) }; + } + + pub fn into(self: @This()) u32 { + return std.mem.littleToNative(u32, self.value_le); } }; From f553d34238a8f2fe942dd7530a26147ed722a4cd Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sun, 11 Jan 2026 17:22:46 +0100 Subject: [PATCH 16/38] respect endianness in setup packet and shortcuts for bulk and interrupt endpoints --- core/src/core/usb.zig | 27 ++++++++++++++------------- core/src/core/usb/descriptor.zig | 3 +++ core/src/core/usb/drivers/cdc.zig | 4 ++-- core/src/core/usb/drivers/example.zig | 6 +++--- core/src/core/usb/drivers/hid.zig | 6 +++--- core/src/core/usb/types.zig | 6 +++--- 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 5f9db5d4e..d43dea4d4 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -253,7 +253,7 @@ pub fn DeviceController(config: Config) type { switch (setup.request_type.recipient) { .Device => try self.process_setup_request(device_itf, setup), - .Interface => switch (@as(u8, @truncate(setup.index))) { + .Interface => switch (@as(u8, @truncate(setup.index.into()))) { inline else => |itf_num| if (itf_num < handlers.itf.len) { const drv = handlers.itf[itf_num]; self.driver_last = drv; @@ -330,7 +330,7 @@ pub fn DeviceController(config: Config) type { inline else => |d| { const drv = &@field(self.driver_data.?, @tagName(d)); if (drv.class_control(stage, setup)) |response| - self.send_cmd_response(device_itf, response, setup.length); + self.send_cmd_response(device_itf, response, setup.length.into()); }, }; } @@ -344,31 +344,32 @@ pub fn DeviceController(config: Config) type { .Standard => { switch (std.meta.intToEnum(types.SetupRequest, setup.request) catch return) { .SetAddress => { - self.new_address = @as(u8, @intCast(setup.value & 0xff)); + self.new_address = @truncate(setup.value.into()); device_itf.ep_ack(.ep0); if (config.debug) log.info(" SetAddress: {}", .{self.new_address.?}); }, .SetConfiguration => { if (config.debug) log.info(" SetConfiguration", .{}); - if (self.cfg_num != setup.value) { + const new_cfg = setup.value.into(); + if (self.cfg_num != new_cfg) { // if (self.cfg_num > 0) // deinitialize drivers - self.cfg_num = setup.value; + self.cfg_num = new_cfg; - if (self.cfg_num > 0) + if (new_cfg > 0) try self.process_set_config(device_itf, self.cfg_num - 1); } device_itf.ep_ack(.ep0); }, .GetDescriptor => { - const descriptor_type = std.meta.intToEnum(descriptor.Type, setup.value >> 8) catch null; + const descriptor_type = std.meta.intToEnum(descriptor.Type, setup.value.into() >> 8) catch null; if (descriptor_type) |dt| { try self.process_get_descriptor(device_itf, setup, dt); } }, .SetFeature => { - if (std.meta.intToEnum(types.FeatureSelector, setup.value >> 8)) |feat| { + if (std.meta.intToEnum(types.FeatureSelector, setup.value.into() >> 8)) |feat| { switch (feat) { .DeviceRemoteWakeup, .EndpointHalt => device_itf.ep_ack(.ep0), // TODO: https://github.com/ZigEmbeddedGroup/microzig/issues/453 @@ -387,25 +388,25 @@ pub fn DeviceController(config: Config) type { .Device => { if (config.debug) log.info(" Device", .{}); - self.send_cmd_response(device_itf, @ptrCast(&config.device_descriptor), setup.length); + self.send_cmd_response(device_itf, @ptrCast(&config.device_descriptor), setup.length.into()); }, .Configuration => { if (config.debug) log.info(" Config", .{}); - self.send_cmd_response(device_itf, @ptrCast(&config_descriptor), setup.length); + self.send_cmd_response(device_itf, @ptrCast(&config_descriptor), setup.length.into()); }, .String => { if (config.debug) log.info(" String", .{}); // String descriptor index is in bottom 8 bits of // `value`. - const i: usize = @intCast(setup.value & 0xff); + const i: u8 = @truncate(setup.value.into()); if (i >= config.string_descriptors.len) log.warn("host requested invalid string descriptor {}", .{i}) else self.send_cmd_response( device_itf, config.string_descriptors[i].data, - setup.length, + setup.length.into(), ); }, .Interface => { @@ -419,7 +420,7 @@ pub fn DeviceController(config: Config) type { // We will just copy parts of the DeviceDescriptor because // the DeviceQualifierDescriptor can be seen as a subset. const qualifier = comptime &config.device_descriptor.qualifier(); - self.send_cmd_response(device_itf, @ptrCast(qualifier), setup.length); + self.send_cmd_response(device_itf, @ptrCast(qualifier), setup.length.into()); }, else => {}, } diff --git a/core/src/core/usb/descriptor.zig b/core/src/core/usb/descriptor.zig index 2e22d83e2..c565602cb 100644 --- a/core/src/core/usb/descriptor.zig +++ b/core/src/core/usb/descriptor.zig @@ -204,6 +204,9 @@ pub const Endpoint = extern struct { synchronisation: Synchronisation = .none, usage: Usage, reserved: u2 = 0, + + pub const bulk: @This() = .{ .transfer_type = .Bulk, .usage = .data }; + pub const interrupt: @This() = .{ .transfer_type = .Interrupt, .usage = .data }; }; comptime { diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 79d5ba32c..e87f3c1c5 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -95,13 +95,13 @@ pub fn CdcClassDriver(options: Options) type { }, .ep_out = .{ .endpoint = .out(@enumFromInt(first_endpoint_out)), - .attributes = .{ .transfer_type = .Bulk, .usage = .data }, + .attributes = .bulk, .max_packet_size = .from(options.max_packet_size), .interval = 0, }, .ep_in = .{ .endpoint = .in(@enumFromInt(first_endpoint_in + 1)), - .attributes = .{ .transfer_type = .Bulk, .usage = .data }, + .attributes = .bulk, .max_packet_size = .from(options.max_packet_size), .interval = 0, }, diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index 722c2d889..a00fc7dcb 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -30,19 +30,19 @@ pub const ExampleDriver = struct { }, .ep_in1 = .{ .endpoint = .in(@enumFromInt(first_endpoint_in)), - .attributes = .{ .transfer_type = .Interrupt, .usage = .data }, + .attributes = .interrupt, .max_packet_size = .from(64), .interval = 16, }, .ep_in2 = .{ .endpoint = .in(@enumFromInt(first_endpoint_in + 1)), - .attributes = .{ .transfer_type = .Bulk, .usage = .data }, + .attributes = .bulk, .max_packet_size = .from(64), .interval = 0, }, .ep_out = .{ .endpoint = .out(@enumFromInt(first_endpoint_out)), - .attributes = .{ .transfer_type = .Bulk, .usage = .data }, + .attributes = .bulk, .max_packet_size = .from(64), .interval = 0, }, diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index f4a055f10..93e061842 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -36,13 +36,13 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { .hid = hid_descriptor, .ep_out = .{ .endpoint = .out(@enumFromInt(first_endpoint_out)), - .attributes = .{ .transfer_type = .Interrupt, .usage = .data }, + .attributes = .interrupt, .max_packet_size = .from(options.max_packet_size), .interval = options.endpoint_interval, }, .ep_in = .{ .endpoint = .in(@enumFromInt(first_endpoint_in)), - .attributes = .{ .transfer_type = .Interrupt, .usage = .data }, + .attributes = .interrupt, .max_packet_size = .from(options.max_packet_size), .interval = options.endpoint_interval, }, @@ -78,7 +78,7 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { _ = self; if (stage == .Setup) switch (setup.request_type.type) { .Standard => { - const hid_desc_type = std.meta.intToEnum(descriptor.hid.Hid.Type, (setup.value >> 8) & 0xff) catch return usb.nak; + const hid_desc_type = std.meta.intToEnum(descriptor.hid.Hid.Type, setup.value.into() >> 8) catch return usb.nak; const request_code = std.meta.intToEnum(types.SetupRequest, setup.request) catch return usb.nak; if (request_code == .GetDescriptor and hid_desc_type == .Hid) diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index 63c9e423d..54db44e0e 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -133,13 +133,13 @@ pub const SetupPacket = extern struct { /// conflict. request: u8, /// A simple argument of up to 16 bits, specific to the request. - value: u16, + value: U16_Le, /// Not used in the requests we support. - index: u16, + index: U16_Le, /// If data will be transferred after this request (in the direction given /// by `request_type`), this gives the number of bytes (OUT) or maximum /// number of bytes (IN). - length: u16, + length: U16_Le, }; /// Represents packet length. From 2ed6b4dbf4a52ef3320e7edc0932801b9b8384b3 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Tue, 13 Jan 2026 15:30:41 +0100 Subject: [PATCH 17/38] cleanum usb examples --- core/src/core/usb/drivers/cdc.zig | 2 +- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 21 +++++++++------------ examples/raspberrypi/rp2xxx/src/usb_hid.zig | 6 ++---- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index e87f3c1c5..4a9f22a03 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -80,7 +80,7 @@ pub fn CdcClassDriver(options: Options) type { }, .ep_notifi = .{ .endpoint = .in(@enumFromInt(first_endpoint_in)), - .attributes = .{ .transfer_type = .Interrupt, .usage = .data }, + .attributes = .interrupt, .max_packet_size = .from(endpoint_notifi_size), .interval = 16, }, diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index e1063c41b..0cf159620 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -41,8 +41,8 @@ var usb_dev: rp2xxx.usb.Polled( .configurations = &.{.{ .num = 1, .configuration_s = 0, - .attributes = .{ .self_powered = true }, - .max_current_ma = 100, + .attributes = .{ .self_powered = false }, + .max_current_ma = 50, .Drivers = struct { serial: USB_Serial }, }}, }, @@ -107,11 +107,10 @@ var usb_tx_buff: [1024]u8 = undefined; // Transfer data to host // NOTE: After each USB chunk transfer, we have to call the USB task so that bus TX events can be handled pub fn usb_cdc_write(serial: *USB_Serial, comptime fmt: []const u8, args: anytype) void { - const text = std.fmt.bufPrint(&usb_tx_buff, fmt, args) catch &.{}; + var tx = std.fmt.bufPrint(&usb_tx_buff, fmt, args) catch &.{}; - var write_buff = text; - while (write_buff.len > 0) { - write_buff = write_buff[serial.write(write_buff)..]; + while (tx.len > 0) { + tx = tx[serial.write(tx)..]; usb_dev.poll(); } // Short messages are not sent right away; instead, they accumulate in a buffer, so we have to force a flush to send them @@ -126,15 +125,13 @@ var usb_rx_buff: [1024]u8 = undefined; pub fn usb_cdc_read( serial: *USB_Serial, ) []const u8 { - var total_read: usize = 0; - var read_buff: []u8 = usb_rx_buff[0..]; + var rx_len: usize = 0; while (true) { - const len = serial.read(read_buff); - read_buff = read_buff[len..]; - total_read += len; + const len = serial.read(usb_rx_buff[rx_len..]); + rx_len += len; if (len == 0) break; } - return usb_rx_buff[0..total_read]; + return usb_rx_buff[0..rx_len]; } diff --git a/examples/raspberrypi/rp2xxx/src/usb_hid.zig b/examples/raspberrypi/rp2xxx/src/usb_hid.zig index 2e66911fa..f3f7b888f 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -16,8 +16,6 @@ const HidDriver = usb.drivers.hid.HidClassDriver( usb.descriptor.hid.ReportDescriptorKeyboard, ); -const usb_config_descriptor = microzig.core.usb.descriptor.Configuration.create(); - var usb_dev: rp2xxx.usb.Polled( usb.Config{ .device_descriptor = .{ @@ -46,8 +44,8 @@ var usb_dev: rp2xxx.usb.Polled( .configurations = &.{.{ .num = 1, .configuration_s = 0, - .attributes = .{ .self_powered = true }, - .max_current_ma = 100, + .attributes = .{ .self_powered = false }, + .max_current_ma = 50, .Drivers = struct { hid: HidDriver }, }}, }, From cd4e348342816629f8619d611ab6896b776c25c1 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Tue, 13 Jan 2026 15:44:24 +0100 Subject: [PATCH 18/38] change the example usb driver to a simple echo --- core/src/core/usb.zig | 2 +- core/src/core/usb/drivers/example.zig | 85 +++++++++++++++------------ 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index d43dea4d4..2e9aad3c6 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -5,7 +5,7 @@ const log = std.log.scoped(.usb); pub const descriptor = @import("usb/descriptor.zig"); pub const drivers = struct { pub const cdc = @import("usb/drivers/cdc.zig"); - pub const example = @import("usb/drivers/example.zig"); + pub const echo = @import("usb/drivers/example.zig"); pub const hid = @import("usb/drivers/hid.zig"); }; pub const types = @import("usb/types.zig"); diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index a00fc7dcb..c5cab6b15 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -1,14 +1,14 @@ const std = @import("std"); const usb = @import("../../usb.zig"); -const descriptor = usb.descriptor; +const log = std.log.scoped(.usb_echo); -pub const ExampleDriver = struct { +/// This is an example driver that sends any data received on ep2 to ep1. +pub const EchoExampleDriver = struct { /// The descriptors need to have the same memory layout as the sent data. pub const Descriptor = extern struct { - example_interface: descriptor.Interface, - ep_in1: descriptor.Endpoint, - ep_in2: descriptor.Endpoint, - ep_out: descriptor.Endpoint, + example_interface: usb.descriptor.Interface, + ep_in: usb.descriptor.Endpoint, + ep_out: usb.descriptor.Endpoint, /// This function is used during descriptor creation. If multiple instances /// of a driver are used, a descriptor will be created for each. @@ -22,26 +22,20 @@ pub const ExampleDriver = struct { .example_interface = .{ .interface_number = first_interface, .alternate_setting = 0, - .num_endpoints = 3, - .interface_class = 0, - .interface_subclass = 0, - .interface_protocol = 0, + .num_endpoints = 2, + .interface_class = 0xFF, + .interface_subclass = 0xFF, + .interface_protocol = 0xFF, .interface_s = first_string, }, - .ep_in1 = .{ + .ep_in = .{ .endpoint = .in(@enumFromInt(first_endpoint_in)), - .attributes = .interrupt, - .max_packet_size = .from(64), - .interval = 16, - }, - .ep_in2 = .{ - .endpoint = .in(@enumFromInt(first_endpoint_in + 1)), .attributes = .bulk, .max_packet_size = .from(64), .interval = 0, }, .ep_out = .{ - .endpoint = .out(@enumFromInt(first_endpoint_out)), + .endpoint = .out(@enumFromInt(first_endpoint_out + 1)), .attributes = .bulk, .max_packet_size = .from(64), .interval = 0, @@ -50,19 +44,27 @@ pub const ExampleDriver = struct { } }; - /// This is a mapping from endpoint descriptor field names - /// to handler function names. + /// This is a mapping from endpoint descriptor field names to handler + /// function names. Counterintuitively, usb devices send data on 'in' + /// endpoints and receive on 'out' endpoints. pub const handlers = .{ - .ep_in1 = "handler1", - .ep_in2 = "handler2", - .ep_out = "handler3", + .ep_in = "on_tx_ready", + .ep_out = "on_rx", }; + device: *usb.DeviceInterface, + ep_tx: usb.types.Endpoint.Num, + /// This function is called when the host chooses a configuration /// that contains this driver. pub fn init(desc: *const Descriptor, device: *usb.DeviceInterface) @This() { - defer device.ep_listen(desc.ep_out.endpoint.num, 64); - return .{}; + const self: @This() = .{ + .device = device, + .ep_tx = desc.ep_in.endpoint.num, + }; + // Listen for first packet + device.ep_listen(desc.ep_out.endpoint.num, 64); + return self; } /// Used for configuration through endpoint 0. @@ -79,18 +81,29 @@ pub const ExampleDriver = struct { /// Each endpoint (as defined in the descriptor) has its own handler. /// Endpoint number is passed as an argument so that it does not need /// to be stored in the driver. - pub fn handler1(self: *@This(), ep_num: usb.types.Endpoint.Num) void { - _ = self; - _ = ep_num; + pub fn on_tx_ready(self: *@This(), ep_tx: usb.types.Endpoint.Num) void { + log.info("tx ready", .{}); + // Mark transmission as available + @atomicStore(usb.types.Endpoint.Num, &self.ep_tx, ep_tx, .seq_cst); } - pub fn handler2(self: *@This(), ep_num: usb.types.Endpoint.Num) void { - _ = self; - _ = ep_num; - } - - pub fn handler3(self: *@This(), ep_num: usb.types.Endpoint.Num) void { - _ = self; - _ = ep_num; + pub fn on_rx(self: *@This(), ep_rx: usb.types.Endpoint.Num) void { + var buf: [64]u8 = undefined; + // Read incoming packet into a local buffer + const len_rx = self.device.ep_readv(ep_rx, &.{&buf}); + log.info("Received: {s}", .{buf[0..len_rx]}); + // Check if we can transmit + const ep_tx = @atomicLoad(usb.types.Endpoint.Num, &self.ep_tx, .seq_cst); + if (ep_tx != .ep0) { + // Mark transmission as not available + @atomicStore(usb.types.Endpoint.Num, &self.ep_tx, .ep0, .seq_cst); + // Send received packet + log.info("Sending {} bytes", .{len_rx}); + const len_tx = self.device.ep_writev(ep_tx, &.{buf[0..len_rx]}); + if (len_tx != len_rx) + log.err("Only sent {} bytes", .{len_tx}); + } + // Listen for next packet + self.device.ep_listen(ep_rx, 64); } }; From 0959d374c706fdb23ef1e7d1b9ba459610671fe2 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Tue, 13 Jan 2026 18:10:31 +0100 Subject: [PATCH 19/38] assign endpoints and interfaces (more) automatically --- core/src/core/usb.zig | 70 ++++++++++++++++++--------- core/src/core/usb/drivers/cdc.zig | 27 +++++------ core/src/core/usb/drivers/example.zig | 22 ++++----- core/src/core/usb/drivers/hid.zig | 10 ++-- 4 files changed, 75 insertions(+), 54 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 2e9aad3c6..a4cba0842 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -13,6 +13,34 @@ pub const types = @import("usb/types.zig"); pub const ack: []const u8 = ""; pub const nak: ?[]const u8 = null; +pub const DescriptorAllocator = struct { + next_ep_num: [2]u8, + next_itf_num: u8, + unique_endpoints: bool, + + pub fn init(unique_endpoints: bool) @This() { + return .{ + .next_ep_num = @splat(1), + .next_itf_num = 0, + .unique_endpoints = unique_endpoints, + }; + } + + pub fn next_ep(self: *@This(), dir: types.Dir) types.Endpoint { + const idx: u1 = @intFromEnum(dir); + const ret = self.next_ep_num[idx]; + self.next_ep_num[idx] += 1; + if (self.unique_endpoints) + self.next_ep_num[idx ^ 1] += 1; + return .{ .dir = dir, .num = @enumFromInt(ret) }; + } + + pub fn next_itf(self: *@This()) u8 { + defer self.next_itf_num += 1; + return self.next_itf_num; + } +}; + /// USB Device interface /// Any device implementation used with DeviceController must implement those functions pub const DeviceInterface = struct { @@ -77,6 +105,10 @@ pub const Config = struct { debug: bool = false, /// Currently only a single configuration is supported. configurations: []const Configuration, + /// Only use either IN or OUT on each endpoint. Useful for debugging. + /// Realistically, it should only be turned off if you are exhausting + /// the 15 endpoint limit. + unique_endpoints: bool = true, }; /// USB device controller @@ -90,39 +122,25 @@ pub fn DeviceController(config: Config) type { const driver_fields = @typeInfo(config0.Drivers).@"struct".fields; const DriverEnum = std.meta.FieldEnum(config0.Drivers); const config_descriptor = blk: { - var num_interfaces = 0; - var num_strings = 4; - var num_ep_in = 1; - var num_ep_out = 1; + var alloc: DescriptorAllocator = .init(config.unique_endpoints); + var next_string = 4; var size = @sizeOf(descriptor.Configuration); var fields: [driver_fields.len + 1]std.builtin.Type.StructField = undefined; for (driver_fields, 1..) |drv, i| { - const payload = drv.type.Descriptor.create(num_interfaces, num_strings, num_ep_in, num_ep_out); + const payload = drv.type.Descriptor.create(&alloc, next_string); const Payload = @TypeOf(payload); size += @sizeOf(Payload); - fields[i] = .{ - .name = drv.name, - .type = Payload, - .default_value_ptr = &payload, - .is_comptime = false, - .alignment = 1, - }; - for (@typeInfo(Payload).@"struct".fields) |fld| { const desc = @field(payload, fld.name); switch (fld.type) { descriptor.Interface => { - num_interfaces += 1; if (desc.interface_s != 0) - num_strings += 1; - }, - descriptor.Endpoint => switch (desc.endpoint.dir) { - .In => num_ep_in += 1, - .Out => num_ep_out += 1, + next_string += 1; }, + descriptor.Endpoint, descriptor.InterfaceAssociation, descriptor.cdc.Header, descriptor.cdc.CallManagement, @@ -133,17 +151,25 @@ pub fn DeviceController(config: Config) type { else => @compileLog(fld), } } + + fields[i] = .{ + .name = drv.name, + .type = Payload, + .default_value_ptr = &payload, + .is_comptime = false, + .alignment = 1, + }; } - if (num_strings != config.string_descriptors.len) + if (next_string != config.string_descriptors.len) @compileError(std.fmt.comptimePrint( "expected {} string descriptros, got {}", - .{ num_strings, config.string_descriptors.len }, + .{ next_string, config.string_descriptors.len }, )); const desc_cfg: descriptor.Configuration = .{ .total_length = .from(size), - .num_interfaces = num_interfaces, + .num_interfaces = alloc.next_itf_num, .configuration_value = config0.num, .configuration_s = config0.configuration_s, .attributes = config0.attributes, diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 4a9f22a03..1bebb2f87 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -44,15 +44,14 @@ pub fn CdcClassDriver(options: Options) type { ep_in: descriptor.Endpoint, pub fn create( - first_interface: u8, + alloc: *usb.DescriptorAllocator, first_string: u8, - first_endpoint_in: u4, - first_endpoint_out: u4, ) @This() { - const endpoint_notifi_size = 8; + const itf_notifi = alloc.next_itf(); + const itf_data = alloc.next_itf(); return .{ .itf_assoc = .{ - .first_interface = first_interface, + .first_interface = itf_notifi, .interface_count = 2, .function_class = 2, .function_subclass = 2, @@ -60,7 +59,7 @@ pub fn CdcClassDriver(options: Options) type { .function = 0, }, .itf_notifi = .{ - .interface_number = first_interface, + .interface_number = itf_notifi, .alternate_setting = 0, .num_endpoints = 1, .interface_class = 2, @@ -71,21 +70,21 @@ pub fn CdcClassDriver(options: Options) type { .cdc_header = .{ .bcd_cdc = .from(0x0120) }, .cdc_call_mgmt = .{ .capabilities = 0, - .data_interface = first_interface + 1, + .data_interface = itf_data, }, .cdc_acm = .{ .capabilities = 6 }, .cdc_union = .{ - .master_interface = first_interface, - .slave_interface_0 = first_interface + 1, + .master_interface = itf_notifi, + .slave_interface_0 = itf_data, }, .ep_notifi = .{ - .endpoint = .in(@enumFromInt(first_endpoint_in)), + .endpoint = alloc.next_ep(.In), .attributes = .interrupt, - .max_packet_size = .from(endpoint_notifi_size), + .max_packet_size = .from(8), .interval = 16, }, .itf_data = .{ - .interface_number = first_interface + 1, + .interface_number = itf_data, .alternate_setting = 0, .num_endpoints = 2, .interface_class = 10, @@ -94,13 +93,13 @@ pub fn CdcClassDriver(options: Options) type { .interface_s = 0, }, .ep_out = .{ - .endpoint = .out(@enumFromInt(first_endpoint_out)), + .endpoint = alloc.next_ep(.Out), .attributes = .bulk, .max_packet_size = .from(options.max_packet_size), .interval = 0, }, .ep_in = .{ - .endpoint = .in(@enumFromInt(first_endpoint_in + 1)), + .endpoint = alloc.next_ep(.In), .attributes = .bulk, .max_packet_size = .from(options.max_packet_size), .interval = 0, diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index c5cab6b15..b065eecc8 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -12,15 +12,15 @@ pub const EchoExampleDriver = struct { /// This function is used during descriptor creation. If multiple instances /// of a driver are used, a descriptor will be created for each. + /// Endpoint numbers are allocated automatically, this function should + /// use placeholder .ep0 values on all endpoints. pub fn create( - first_interface: u8, + alloc: *usb.DescriptorAllocator, first_string: u8, - first_endpoint_in: u4, - first_endpoint_out: u4, ) @This() { return .{ .example_interface = .{ - .interface_number = first_interface, + .interface_number = alloc.next_itf(), .alternate_setting = 0, .num_endpoints = 2, .interface_class = 0xFF, @@ -28,14 +28,14 @@ pub const EchoExampleDriver = struct { .interface_protocol = 0xFF, .interface_s = first_string, }, - .ep_in = .{ - .endpoint = .in(@enumFromInt(first_endpoint_in)), + .ep_out = .{ + .endpoint = alloc.next_ep(.Out), .attributes = .bulk, .max_packet_size = .from(64), .interval = 0, }, - .ep_out = .{ - .endpoint = .out(@enumFromInt(first_endpoint_out + 1)), + .ep_in = .{ + .endpoint = alloc.next_ep(.In), .attributes = .bulk, .max_packet_size = .from(64), .interval = 0, @@ -58,13 +58,11 @@ pub const EchoExampleDriver = struct { /// This function is called when the host chooses a configuration /// that contains this driver. pub fn init(desc: *const Descriptor, device: *usb.DeviceInterface) @This() { - const self: @This() = .{ + defer device.ep_listen(desc.ep_out.endpoint.num, 64); + return .{ .device = device, .ep_tx = desc.ep_in.endpoint.num, }; - // Listen for first packet - device.ep_listen(desc.ep_out.endpoint.num, 64); - return self; } /// Used for configuration through endpoint 0. diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index 93e061842..3921462d4 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -18,14 +18,12 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { ep_in: descriptor.Endpoint, pub fn create( - first_interface: u8, + alloc: *usb.DescriptorAllocator, first_string: u8, - first_endpoint_in: u4, - first_endpoint_out: u4, ) @This() { return .{ .interface = .{ - .interface_number = first_interface, + .interface_number = alloc.next_itf(), .alternate_setting = 0, .num_endpoints = 2, .interface_class = 3, @@ -35,13 +33,13 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { }, .hid = hid_descriptor, .ep_out = .{ - .endpoint = .out(@enumFromInt(first_endpoint_out)), + .endpoint = alloc.next_ep(.Out), .attributes = .interrupt, .max_packet_size = .from(options.max_packet_size), .interval = options.endpoint_interval, }, .ep_in = .{ - .endpoint = .in(@enumFromInt(first_endpoint_in)), + .endpoint = alloc.next_ep(.In), .attributes = .interrupt, .max_packet_size = .from(options.max_packet_size), .interval = options.endpoint_interval, From 36f02ea822c413288928a6aed4049806c813b71b Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Tue, 13 Jan 2026 19:39:18 +0100 Subject: [PATCH 20/38] better handling of device class, subclass and protocol --- core/src/core/usb/descriptor.zig | 28 +-- core/src/core/usb/drivers/cdc.zig | 8 +- core/src/core/usb/drivers/example.zig | 4 +- core/src/core/usb/drivers/hid.zig | 8 +- core/src/core/usb/types.zig | 224 +++++++++++++++++--- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 6 +- examples/raspberrypi/rp2xxx/src/usb_hid.zig | 6 +- 7 files changed, 210 insertions(+), 74 deletions(-) diff --git a/core/src/core/usb/descriptor.zig b/core/src/core/usb/descriptor.zig index c565602cb..f7371ba99 100644 --- a/core/src/core/usb/descriptor.zig +++ b/core/src/core/usb/descriptor.zig @@ -29,22 +29,6 @@ pub const Type = enum(u8) { /// Describes a device. This is the most broad description in USB and is /// typically the first thing the host asks for. pub const Device = extern struct { - /// Class, subclass and protocol of device. - pub const DeviceTriple = extern struct { - /// Class of device, giving a broad functional area. - class: types.ClassCode, - /// Subclass of device, refining the class. - subclass: u8, - /// Protocol within the subclass. - protocol: u8, - - pub const unspecified: @This() = .{ - .class = .Unspecified, - .subclass = 0, - .protocol = 0, - }; - }; - /// USB Device Qualifier Descriptor /// This descriptor is a subset of the DeviceDescriptor pub const Qualifier = extern struct { @@ -59,7 +43,7 @@ pub const Device = extern struct { /// Specification version as Binary Coded Decimal bcd_usb: types.U16_Le, /// Class, subclass and protocol of device. - device_triple: DeviceTriple, + device_triple: types.ClassSubclassProtocol, /// Maximum unit of data this device can move. max_packet_size0: u8, /// Number of configurations supported by this device. @@ -79,7 +63,7 @@ pub const Device = extern struct { /// Specification version as Binary Coded Decimal bcd_usb: types.U16_Le, /// Class, subclass and protocol of device. - device_triple: DeviceTriple, + device_triple: types.ClassSubclassProtocol, /// Maximum length of data this device can move. max_packet_size0: u8, /// ID of product vendor. @@ -248,12 +232,8 @@ pub const Interface = extern struct { alternate_setting: u8, /// Number of endpoint descriptors in this interface. num_endpoints: u8, - /// Interface class code, distinguishing the type of interface. - interface_class: u8, - /// Interface subclass code, refining the class of interface. - interface_subclass: u8, - /// Protocol within the interface class/subclass. - interface_protocol: u8, + /// Interface class, subclass and protocol. + interface_triple: types.ClassSubclassProtocol, /// Index of interface name within string descriptor table. interface_s: u8, }; diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 1bebb2f87..d8e3dddca 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -62,9 +62,7 @@ pub fn CdcClassDriver(options: Options) type { .interface_number = itf_notifi, .alternate_setting = 0, .num_endpoints = 1, - .interface_class = 2, - .interface_subclass = 2, - .interface_protocol = 0, + .interface_triple = .from(.Cdc, .Abstract, .NoneRequired), .interface_s = first_string, }, .cdc_header = .{ .bcd_cdc = .from(0x0120) }, @@ -87,9 +85,7 @@ pub fn CdcClassDriver(options: Options) type { .interface_number = itf_data, .alternate_setting = 0, .num_endpoints = 2, - .interface_class = 10, - .interface_subclass = 0, - .interface_protocol = 0, + .interface_triple = .from(.CdcData, .Unused, .NoneRequired), .interface_s = 0, }, .ep_out = .{ diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index b065eecc8..0b067f5f1 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -23,9 +23,7 @@ pub const EchoExampleDriver = struct { .interface_number = alloc.next_itf(), .alternate_setting = 0, .num_endpoints = 2, - .interface_class = 0xFF, - .interface_subclass = 0xFF, - .interface_protocol = 0xFF, + .interface_triple = .vendor_specific, .interface_s = first_string, }, .ep_out = .{ diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index 3921462d4..5f9f8dc2b 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -26,9 +26,11 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { .interface_number = alloc.next_itf(), .alternate_setting = 0, .num_endpoints = 2, - .interface_class = 3, - .interface_subclass = @intFromBool(options.boot_protocol), - .interface_protocol = @intFromBool(options.boot_protocol), + .interface_triple = .from( + .Hid, + if (options.boot_protocol) .Boot else .Unspecified, + if (options.boot_protocol) .Boot else .None, + ), .interface_s = first_string, }, .hid = hid_descriptor, diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index 54db44e0e..c5e018f07 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -1,34 +1,202 @@ const std = @import("std"); const assert = std.debug.assert; -/// Class of device, giving a broad functional area. -pub const ClassCode = enum(u8) { - Unspecified = 0x00, - Audio = 0x01, - Cdc = 0x02, - Hid = 0x03, - Physical = 0x05, - Image = 0x06, - Printer = 0x07, - MassStorage = 0x08, - Hub = 0x09, - CdcData = 0x0A, - SmartCard = 0x0B, - ContentSecurity = 0x0D, - Video = 0x0E, - PersonalHealthcare = 0x0F, - AudioVideoDevice = 0x10, - BillboardDevice = 0x11, - USBTypeCBridge = 0x12, - USBBulkDisplayProtocol = 0x13, - MCTPoverUSBProtocolEndpoint = 0x14, - I3C = 0x3C, - DiagnosticDevice = 0xDC, - WirelessController = 0xE0, - Miscellaneous = 0xEF, - ApplicationSpecific = 0xFE, - VendorSpecific = 0xFF, - _, +pub const ClassSubclassProtocol = extern struct { + /// Class of device, giving a broad functional area. + pub const ClassCode = enum(u8) { + Unspecified = 0x00, + Audio = 0x01, + Cdc = 0x02, + Hid = 0x03, + Physical = 0x05, + Image = 0x06, + Printer = 0x07, + MassStorage = 0x08, + Hub = 0x09, + CdcData = 0x0A, + SmartCard = 0x0B, + ContentSecurity = 0x0D, + Video = 0x0E, + PersonalHealthcare = 0x0F, + AudioVideoDevice = 0x10, + BillboardDevice = 0x11, + USBTypeCBridge = 0x12, + USBBulkDisplayProtocol = 0x13, + MCTPoverUSBProtocolEndpoint = 0x14, + I3C = 0x3C, + DiagnosticDevice = 0xDC, + WirelessController = 0xE0, + Miscellaneous = 0xEF, + ApplicationSpecific = 0xFE, + VendorSpecific = 0xFF, + _, + + pub fn Subclass(self: @This()) type { + const name = "Subclass" ++ @tagName(self); + return if (@hasDecl(ClassSubclassProtocol, name)) + @field(ClassSubclassProtocol, name) + else + ClassSubclassProtocol.SubclassDefault; + } + + pub fn Protocol(self: @This()) type { + const name = "Protocol" ++ @tagName(self); + return if (@hasDecl(ClassSubclassProtocol, name)) + @field(ClassSubclassProtocol, name) + else + ClassSubclassProtocol.ProtocolDefault; + } + }; + + pub const SubclassDefault = enum(u8) { + Unspecified = 0x00, + VendorSpecific = 0xFF, + _, + }; + + pub const ProtocolDefault = enum(u8) { + NoneRequired = 0x00, + VendorSpecific = 0xFF, + _, + }; + + pub const SubclassCdc = enum(u8) { + Unspecified = 0x00, + VendorSpecific = 0xFF, + /// Direct Line Control Model + DirectLine = 0x01, + /// Abstract Control Model + Abstract = 0x02, + /// Telephone Control Model + Telephone = 0x03, + /// Multi-Channel Control Model + MultChannel = 0x04, + /// CAPI Control Model + CAPI = 0x05, + /// Ethernet Networking Control Model + Ethernet = 0x06, + /// ATM Networking Control Model + ATM_Networking = 0x07, + /// Wireless Handset Control Model + WirelessHeadest = 0x08, + /// Device Management + DeviceManagement = 0x09, + /// Mobile Direct Line Model + MobileDirect = 0x0A, + /// OBEX + OBEX = 0x0B, + /// Ethernet Emulation Model + EthernetEmulation = 0x0C, + /// Network Control Model + Network = 0x0D, + _, + }; + + pub const ProtocolCdc = enum(u8) { + /// USB specification No class specific protocol required + NoneRequired = 0x00, + /// ITU-T V.250 AT Commands: V.250 etc + AT_ITU_T_V_250 = 0x01, + /// PCCA-101 AT Commands defined by PCCA-101 + AT_PCCA_101 = 0x02, + /// PCCA-101 AT Commands defined by PCCA-101 & Annex O + AT_PCCA_101_O = 0x03, + /// GSM 7.07 AT Commands defined by GSM 07.07 + AT_GSM_7_07 = 0x04, + /// 3GPP 27.07 AT Commands defined by 3GPP 27.007 + AT_3GPP_27_07 = 0x05, + /// C-S0017-0 AT Commands defined by TIA for CDMA + AT_C_S0017_0 = 0x06, + /// USB EEM Ethernet Emulation module + USB_EEM = 0x07, + /// External Protocol: Commands defined by Command Set Functional Descriptor + External = 0xFE, + /// USB Specification Vendor-specific + VendorSpecific = 0xFF, + _, + }; + + pub const SubclassCdcData = enum(u8) { + Unused = 0, + VendorSpecific = 0xFF, + _, + }; + + pub const ProtocolCdcData = enum(u8) { + NoneRequired = 0, + VendorSpecific = 0xFF, + /// Network Transfer Block + NetworkTransferBlock = 0x01, + /// Physical interface protocol for ISDN BRI + ISDN_BRI = 0x30, + /// HDLC + HDLC = 0x31, + /// Transparent + Transparent = 0x32, + /// Management protocol for Q.921 data link protocol + Management_Q_921 = 0x50, + /// Data link protocol for Q.931 + DataLink_Q_931 = 0x51, + /// TEI-multiplexor for Q.921 data link protocol + TEI_Multiplexor_Q_921 = 0x52, + /// Data compression procedures + DataCompressionProcedures = 0x90, + /// Euro-ISDN protocol control + Euro_ISDN = 0x91, + /// V.24 rate adaptation to ISDN + RateAdaptation_V_24 = 0x92, + /// CAPI Commands + CAPI = 0x93, + /// Host based driver. Note: This protocol code should only be used + /// in messages between host and device to identify the host driver + /// portion of a protocol stack. + HostBasedDriver = 0xFD, + /// CDC Specification The protocol(s) are described using a Protocol + /// Unit Functional Descriptors on Communications Class Interface + SpecifiedIn_PUF_Descriptor = 0xFE, + _, + }; + + pub const SubclassHid = enum(u8) { + Unspecified = 0x00, + VendorSpecific = 0xFF, + /// + Boot = 0x01, + _, + }; + + pub const ProtocolHid = enum(u8) { + NoneRequired = 0x00, + VendorSpecific = 0xFF, + /// + Boot = 0x01, + _, + }; + + /// Class code, distinguishing the type of interface. + class: ClassCode, + /// Interface subclass code, refining the class of interface. + subclass: u8, + /// Protocol within the interface class/subclass. + protocol: u8, + + pub const unspecified: @This() = + .from(.Unspecified, .Unspecified, .NoneRequired); + + pub const vendor_specific: @This() = + .from(.VendorSpecific, .VendorSpecific, .VendorSpecific); + + pub fn from( + comptime class: ClassCode, + subclass: class.Subclass(), + protocol: class.Protocol(), + ) @This() { + return .{ + .class = class, + .subclass = @intFromEnum(subclass), + .protocol = @intFromEnum(protocol), + }; + } }; /// Types of transfer that can be indicated by the `attributes` field on `EndpointDescriptor`. diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 0cf159620..6905367fe 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -17,11 +17,7 @@ var usb_dev: rp2xxx.usb.Polled( usb.Config{ .device_descriptor = .{ .bcd_usb = .from(0x0200), - .device_triple = .{ - .class = .Miscellaneous, - .subclass = 2, - .protocol = 1, - }, + .device_triple = .from(.Miscellaneous, @enumFromInt(2), @enumFromInt(1)), .max_packet_size0 = 64, .vendor = .from(0x2E8A), .product = .from(0x000A), diff --git a/examples/raspberrypi/rp2xxx/src/usb_hid.zig b/examples/raspberrypi/rp2xxx/src/usb_hid.zig index f3f7b888f..cded00432 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -20,11 +20,7 @@ var usb_dev: rp2xxx.usb.Polled( usb.Config{ .device_descriptor = .{ .bcd_usb = .from(0x0200), - .device_triple = .{ - .class = .Unspecified, - .subclass = 0, - .protocol = 0, - }, + .device_triple = .unspecified, .max_packet_size0 = 64, .vendor = .from(0x2E8A), .product = .from(0x000A), From b7bed055f72ace83f62e3f58e8935e628bb1c187 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Tue, 13 Jan 2026 20:22:19 +0100 Subject: [PATCH 21/38] separate usb device and controller --- core/src/core/usb.zig | 11 ++++ examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 69 ++++++++++----------- examples/raspberrypi/rp2xxx/src/usb_hid.zig | 65 ++++++++++--------- port/raspberrypi/rp2xxx/src/hal/usb.zig | 17 +++-- 4 files changed, 84 insertions(+), 78 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index a4cba0842..a496daa64 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -111,6 +111,17 @@ pub const Config = struct { unique_endpoints: bool = true, }; +pub fn validate_controller(T: type) void { + comptime { + const info = @typeInfo(T); + if (info != .pointer or info.pointer.is_const or info.pointer.size != .one) + @compileError("Expected a mutable pointer to the usb controller, got: " ++ @typeName(T)); + const Controller = info.pointer.child; + _ = Controller; + // More checks + } +} + /// USB device controller /// /// This code handles usb enumeration and configuration and routes packets to drivers. diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 6905367fe..211d4728a 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -13,37 +13,36 @@ const uart_tx_pin = gpio.num(0); const USB_Serial = usb.drivers.cdc.CdcClassDriver(.{ .max_packet_size = 64 }); -var usb_dev: rp2xxx.usb.Polled( - usb.Config{ - .device_descriptor = .{ - .bcd_usb = .from(0x0200), - .device_triple = .from(.Miscellaneous, @enumFromInt(2), @enumFromInt(1)), - .max_packet_size0 = 64, - .vendor = .from(0x2E8A), - .product = .from(0x000A), - .bcd_device = .from(0x0100), - .manufacturer_s = 1, - .product_s = 2, - .serial_s = 3, - .num_configurations = 1, - }, - .string_descriptors = &.{ - .from_lang(.English), - .from_str("Raspberry Pi"), - .from_str("Pico Test Device"), - .from_str("someserial"), - .from_str("Board CDC"), - }, - .configurations = &.{.{ - .num = 1, - .configuration_s = 0, - .attributes = .{ .self_powered = false }, - .max_current_ma = 50, - .Drivers = struct { serial: USB_Serial }, - }}, +var usb_device: rp2xxx.usb.Polled(.{}) = undefined; + +var usb_controller: usb.DeviceController(.{ + .device_descriptor = .{ + .bcd_usb = .from(0x0200), + .device_triple = .from(.Miscellaneous, @enumFromInt(2), @enumFromInt(1)), + .max_packet_size0 = 64, + .vendor = .from(0x2E8A), + .product = .from(0x000A), + .bcd_device = .from(0x0100), + .manufacturer_s = 1, + .product_s = 2, + .serial_s = 3, + .num_configurations = 1, }, - .{}, -) = undefined; + .string_descriptors = &.{ + .from_lang(.English), + .from_str("Raspberry Pi"), + .from_str("Pico Test Device"), + .from_str("someserial"), + .from_str("Board CDC"), + }, + .configurations = &.{.{ + .num = 1, + .configuration_s = 0, + .attributes = .{ .self_powered = false }, + .max_current_ma = 50, + .Drivers = struct { serial: USB_Serial }, + }}, +}) = .init; pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { std.log.err("panic: {s}", .{message}); @@ -68,7 +67,7 @@ pub fn main() !void { led.put(1); // Then initialize the USB device using the configuration defined above - usb_dev = .init(); + usb_device = .init(); var old: u64 = time.get_time_since_boot().to_us(); var new: u64 = 0; @@ -76,9 +75,9 @@ pub fn main() !void { var i: u32 = 0; while (true) { // You can now poll for USB events - usb_dev.poll(); + usb_device.poll(&usb_controller); - if (usb_dev.controller.drivers()) |drivers| { + if (usb_controller.drivers()) |drivers| { new = time.get_time_since_boot().to_us(); if (new - old > 500000) { old = new; @@ -107,11 +106,11 @@ pub fn usb_cdc_write(serial: *USB_Serial, comptime fmt: []const u8, args: anytyp while (tx.len > 0) { tx = tx[serial.write(tx)..]; - usb_dev.poll(); + usb_device.poll(&usb_controller); } // Short messages are not sent right away; instead, they accumulate in a buffer, so we have to force a flush to send them while (!serial.flush()) - usb_dev.poll(); + usb_device.poll(&usb_controller); } var usb_rx_buff: [1024]u8 = undefined; diff --git a/examples/raspberrypi/rp2xxx/src/usb_hid.zig b/examples/raspberrypi/rp2xxx/src/usb_hid.zig index cded00432..8a9b4ba43 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -16,37 +16,36 @@ const HidDriver = usb.drivers.hid.HidClassDriver( usb.descriptor.hid.ReportDescriptorKeyboard, ); -var usb_dev: rp2xxx.usb.Polled( - usb.Config{ - .device_descriptor = .{ - .bcd_usb = .from(0x0200), - .device_triple = .unspecified, - .max_packet_size0 = 64, - .vendor = .from(0x2E8A), - .product = .from(0x000A), - .bcd_device = .from(0x0100), - .manufacturer_s = 1, - .product_s = 2, - .serial_s = 3, - .num_configurations = 1, - }, - .string_descriptors = &.{ - .from_lang(.English), - .from_str("Raspberry Pi"), - .from_str("Pico Test Device"), - .from_str("someserial"), - .from_str("Boot Keyboard"), - }, - .configurations = &.{.{ - .num = 1, - .configuration_s = 0, - .attributes = .{ .self_powered = false }, - .max_current_ma = 50, - .Drivers = struct { hid: HidDriver }, - }}, +var usb_device: rp2xxx.usb.Polled(.{}) = undefined; + +var usb_controller: usb.DeviceController(.{ + .device_descriptor = .{ + .bcd_usb = .from(0x0200), + .device_triple = .unspecified, + .max_packet_size0 = 64, + .vendor = .from(0x2E8A), + .product = .from(0x000A), + .bcd_device = .from(0x0100), + .manufacturer_s = 1, + .product_s = 2, + .serial_s = 3, + .num_configurations = 1, + }, + .string_descriptors = &.{ + .from_lang(.English), + .from_str("Raspberry Pi"), + .from_str("Pico Test Device"), + .from_str("someserial"), + .from_str("Boot Keyboard"), }, - .{}, -) = undefined; + .configurations = &.{.{ + .num = 1, + .configuration_s = 0, + .attributes = .{ .self_powered = false }, + .max_current_ma = 50, + .Drivers = struct { hid: HidDriver }, + }}, +}) = .init; pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { std.log.err("panic: {s}", .{message}); @@ -71,15 +70,15 @@ pub fn main() !void { led.put(1); // Then initialize the USB device using the configuration defined above - usb_dev = .init(); + usb_device = .init(); var old: u64 = time.get_time_since_boot().to_us(); var new: u64 = 0; while (true) { // You can now poll for USB events - usb_dev.poll(); + usb_device.poll(&usb_controller); - if (usb_dev.controller.drivers()) |drivers| { + if (usb_controller.drivers()) |drivers| { _ = drivers; // TODO new = time.get_time_since_boot().to_us(); diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 145ae1b01..1e668c3a7 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -74,10 +74,7 @@ const endpoint_control: *volatile [15]PerEndpoint(EndpointControlMimo) = @ptrCas /// A set of functions required by the abstract USB impl to /// create a concrete one. -pub fn Polled( - controller_config: usb.Config, - config: Config, -) type { +pub fn Polled(config: Config) type { comptime { if (config.max_endpoints_count > RP2XXX_MAX_ENDPOINTS_COUNT) @compileError("RP2XXX USB endpoints number can't be grater than RP2XXX_MAX_ENDPOINTS_COUNT"); @@ -94,10 +91,11 @@ pub fn Polled( endpoints: [config.max_endpoints_count][2]HardwareEndpointData, data_buffer: []align(64) u8, - controller: usb.DeviceController(controller_config), interface: usb.DeviceInterface, - pub fn poll(self: *@This()) void { + pub fn poll(self: *@This(), controller: anytype) void { + comptime usb.validate_controller(@TypeOf(controller)); + // Check which interrupt flags are set. const ints = peripherals.USB.INTS.read(); @@ -109,7 +107,7 @@ pub fn Polled( buffer_control[0].in.modify(.{ .PID_0 = 0 }); const setup = get_setup_packet(); - self.controller.on_setup_req(&self.interface, &setup); + controller.on_setup_req(&self.interface, &setup); } var buff_status: u32 = 0; @@ -145,7 +143,7 @@ pub fn Polled( // than the buffer size. const len = buffer_control[ep_num].get(ep.dir).read().LENGTH_0; - self.controller.on_buffer(&self.interface, ep, ep_hard.data_buffer[0..len]); + controller.on_buffer(&self.interface, ep, ep_hard.data_buffer[0..len]); } } @@ -155,7 +153,7 @@ pub fn Polled( peripherals.USB.SIE_STATUS.modify(.{ .BUS_RESET = 1 }); set_address(&self.interface, 0); - self.controller.on_bus_reset(); + controller.on_bus_reset(); } } @@ -223,7 +221,6 @@ pub fn Polled( .endpoints = undefined, .data_buffer = rp2xxx_buffers.data_buffer, .interface = .{ .vtable = &vtable }, - .controller = .init, }; @memset(std.mem.asBytes(&self.endpoints), 0); From 93cbc874e4d3009e02406183d072bea2dd58abf7 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Tue, 13 Jan 2026 20:51:12 +0100 Subject: [PATCH 22/38] more convenient descriptor creation --- core/src/core/usb.zig | 7 ++++- core/src/core/usb/descriptor.zig | 30 ++++++++++++++++++--- core/src/core/usb/drivers/cdc.zig | 22 +++------------ core/src/core/usb/drivers/example.zig | 15 +++-------- core/src/core/usb/drivers/hid.zig | 18 +++---------- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 1 + examples/raspberrypi/rp2xxx/src/usb_hid.zig | 3 ++- port/raspberrypi/rp2xxx/src/hal/usb.zig | 24 +++++++---------- 8 files changed, 56 insertions(+), 64 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index a496daa64..db9692a7f 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -109,6 +109,8 @@ pub const Config = struct { /// Realistically, it should only be turned off if you are exhausting /// the 15 endpoint limit. unique_endpoints: bool = true, + /// Device specific, either 8, 16, 32 or 64. + max_supported_packet_size: types.Len, }; pub fn validate_controller(T: type) void { @@ -133,6 +135,9 @@ pub fn DeviceController(config: Config) type { const driver_fields = @typeInfo(config0.Drivers).@"struct".fields; const DriverEnum = std.meta.FieldEnum(config0.Drivers); const config_descriptor = blk: { + const max_psize = config.max_supported_packet_size; + assert(max_psize == 8 or max_psize == 16 or max_psize == 32 or max_psize == 64); + var alloc: DescriptorAllocator = .init(config.unique_endpoints); var next_string = 4; @@ -140,7 +145,7 @@ pub fn DeviceController(config: Config) type { var fields: [driver_fields.len + 1]std.builtin.Type.StructField = undefined; for (driver_fields, 1..) |drv, i| { - const payload = drv.type.Descriptor.create(&alloc, next_string); + const payload = drv.type.Descriptor.create(&alloc, next_string, max_psize); const Payload = @TypeOf(payload); size += @sizeOf(Payload); diff --git a/core/src/core/usb/descriptor.zig b/core/src/core/usb/descriptor.zig index f7371ba99..b46be3d9c 100644 --- a/core/src/core/usb/descriptor.zig +++ b/core/src/core/usb/descriptor.zig @@ -188,9 +188,6 @@ pub const Endpoint = extern struct { synchronisation: Synchronisation = .none, usage: Usage, reserved: u2 = 0, - - pub const bulk: @This() = .{ .transfer_type = .Bulk, .usage = .data }; - pub const interrupt: @This() = .{ .transfer_type = .Interrupt, .usage = .data }; }; comptime { @@ -212,6 +209,33 @@ pub const Endpoint = extern struct { /// Interval for polling interrupt/isochronous endpoints (which we don't /// currently support) in milliseconds. interval: u8, + + pub fn control(ep: types.Endpoint, max_packet_size: types.Len) @This() { + return .{ + .endpoint = ep, + .attributes = .{ .transfer_type = .Control, .usage = .data }, + .max_packet_size = .from(max_packet_size), + .interval = 0, // Unused for bulk endpoints + }; + } + + pub fn bulk(ep: types.Endpoint, max_packet_size: types.Len) @This() { + return .{ + .endpoint = ep, + .attributes = .{ .transfer_type = .Bulk, .usage = .data }, + .max_packet_size = .from(max_packet_size), + .interval = 0, // Unused for bulk endpoints + }; + } + + pub fn interrupt(ep: types.Endpoint, max_packet_size: types.Len, poll_interval: u8) @This() { + return .{ + .endpoint = ep, + .attributes = .{ .transfer_type = .Interrupt, .usage = .data }, + .max_packet_size = .from(max_packet_size), + .interval = poll_interval, + }; + } }; /// Description of an interface within a configuration. diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index d8e3dddca..0a1878986 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -46,6 +46,7 @@ pub fn CdcClassDriver(options: Options) type { pub fn create( alloc: *usb.DescriptorAllocator, first_string: u8, + max_supported_packet_size: usb.types.Len, ) @This() { const itf_notifi = alloc.next_itf(); const itf_data = alloc.next_itf(); @@ -75,12 +76,7 @@ pub fn CdcClassDriver(options: Options) type { .master_interface = itf_notifi, .slave_interface_0 = itf_data, }, - .ep_notifi = .{ - .endpoint = alloc.next_ep(.In), - .attributes = .interrupt, - .max_packet_size = .from(8), - .interval = 16, - }, + .ep_notifi = .interrupt(alloc.next_ep(.In), 8, 16), .itf_data = .{ .interface_number = itf_data, .alternate_setting = 0, @@ -88,18 +84,8 @@ pub fn CdcClassDriver(options: Options) type { .interface_triple = .from(.CdcData, .Unused, .NoneRequired), .interface_s = 0, }, - .ep_out = .{ - .endpoint = alloc.next_ep(.Out), - .attributes = .bulk, - .max_packet_size = .from(options.max_packet_size), - .interval = 0, - }, - .ep_in = .{ - .endpoint = alloc.next_ep(.In), - .attributes = .bulk, - .max_packet_size = .from(options.max_packet_size), - .interval = 0, - }, + .ep_out = .bulk(alloc.next_ep(.Out), max_supported_packet_size), + .ep_in = .bulk(alloc.next_ep(.In), max_supported_packet_size), }; } }; diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index 0b067f5f1..88eb4b2ff 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -17,6 +17,7 @@ pub const EchoExampleDriver = struct { pub fn create( alloc: *usb.DescriptorAllocator, first_string: u8, + max_supported_packet_size: usb.types.Len, ) @This() { return .{ .example_interface = .{ @@ -26,18 +27,8 @@ pub const EchoExampleDriver = struct { .interface_triple = .vendor_specific, .interface_s = first_string, }, - .ep_out = .{ - .endpoint = alloc.next_ep(.Out), - .attributes = .bulk, - .max_packet_size = .from(64), - .interval = 0, - }, - .ep_in = .{ - .endpoint = alloc.next_ep(.In), - .attributes = .bulk, - .max_packet_size = .from(64), - .interval = 0, - }, + .ep_out = .bulk(alloc.next_ep(.Out), max_supported_packet_size), + .ep_in = .bulk(alloc.next_ep(.In), max_supported_packet_size), }; } }; diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index 5f9f8dc2b..a9ba15780 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -4,9 +4,8 @@ const descriptor = usb.descriptor; const types = usb.types; pub const Options = struct { - max_packet_size: u16, boot_protocol: bool, - endpoint_interval: u8, + poll_interval: u8, }; pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { @@ -20,6 +19,7 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { pub fn create( alloc: *usb.DescriptorAllocator, first_string: u8, + max_supported_packet_size: usb.types.Len, ) @This() { return .{ .interface = .{ @@ -34,18 +34,8 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { .interface_s = first_string, }, .hid = hid_descriptor, - .ep_out = .{ - .endpoint = alloc.next_ep(.Out), - .attributes = .interrupt, - .max_packet_size = .from(options.max_packet_size), - .interval = options.endpoint_interval, - }, - .ep_in = .{ - .endpoint = alloc.next_ep(.In), - .attributes = .interrupt, - .max_packet_size = .from(options.max_packet_size), - .interval = options.endpoint_interval, - }, + .ep_out = .interrupt(alloc.next_ep(.Out), max_supported_packet_size, options.poll_interval), + .ep_in = .interrupt(alloc.next_ep(.In), max_supported_packet_size, options.poll_interval), }; } }; diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 211d4728a..c127c44c7 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -42,6 +42,7 @@ var usb_controller: usb.DeviceController(.{ .max_current_ma = 50, .Drivers = struct { serial: USB_Serial }, }}, + .max_supported_packet_size = @TypeOf(usb_device).max_supported_packet_size, }) = .init; pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { diff --git a/examples/raspberrypi/rp2xxx/src/usb_hid.zig b/examples/raspberrypi/rp2xxx/src/usb_hid.zig index 8a9b4ba43..29d127e91 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -12,7 +12,7 @@ const uart = rp2xxx.uart.instance.num(0); const uart_tx_pin = gpio.num(0); const HidDriver = usb.drivers.hid.HidClassDriver( - .{ .max_packet_size = 64, .boot_protocol = true, .endpoint_interval = 0 }, + .{ .boot_protocol = true, .poll_interval = 0 }, usb.descriptor.hid.ReportDescriptorKeyboard, ); @@ -45,6 +45,7 @@ var usb_controller: usb.DeviceController(.{ .max_current_ma = 50, .Drivers = struct { hid: HidDriver }, }}, + .max_supported_packet_size = @TypeOf(usb_device).max_supported_packet_size, }) = .init; pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 1e668c3a7..e2bbabafa 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -81,6 +81,8 @@ pub fn Polled(config: Config) type { } return struct { + pub const max_supported_packet_size = 64; + const vtable: usb.DeviceInterface.VTable = .{ .ep_writev = ep_writev, .ep_readv = ep_readv, @@ -224,18 +226,8 @@ pub fn Polled(config: Config) type { }; @memset(std.mem.asBytes(&self.endpoints), 0); - ep_open(&self.interface, &.{ - .endpoint = .in(.ep0), - .max_packet_size = .from(64), - .attributes = .{ .transfer_type = .Control, .usage = .data }, - .interval = 0, - }); - ep_open(&self.interface, &.{ - .endpoint = .out(.ep0), - .max_packet_size = .from(64), - .attributes = .{ .transfer_type = .Control, .usage = .data }, - .interval = 0, - }); + ep_open(&self.interface, &.control(.in(.ep0), max_supported_packet_size)); + ep_open(&self.interface, &.control(.out(.ep0), max_supported_packet_size)); // Present full-speed device by enabling pullup on DP. This is the point // where the host will notice our presence. @@ -321,18 +313,20 @@ pub fn Polled(config: Config) type { } fn ep_listen( - _: *usb.DeviceInterface, + itf: *usb.DeviceInterface, ep_num: usb.types.Endpoint.Num, len: usb.types.Len, ) void { + const self: *@This() = @fieldParentPtr("interface", itf); const bufctrl_ptr = &buffer_control[@intFromEnum(ep_num)].out; var bufctrl = bufctrl_ptr.read(); + const ep = self.hardware_endpoint_get_by_address(.out(ep_num)); assert(bufctrl.AVAILABLE_0 == 0); // Configure the OUT: bufctrl.PID_0 ^= 1; // Flip DATA0/1 bufctrl.FULL_0 = 0; // Buffer is NOT full, we want the computer to fill it - bufctrl.LENGTH_0 = @intCast(@min(len, 64)); // Up tho this many bytes + bufctrl.LENGTH_0 = @intCast(@min(len, ep.data_buffer.len)); // Up tho this many bytes if (config.sync_noops != 0) { bufctrl_ptr.write(bufctrl); @@ -384,7 +378,7 @@ pub fn Polled(config: Config) type { const ep = desc.endpoint; const ep_hard = self.hardware_endpoint_get_by_address(ep); - assert(desc.max_packet_size.into() <= 64); + assert(desc.max_packet_size.into() <= max_supported_packet_size); buffer_control[@intFromEnum(ep.num)].get(ep.dir).modify(.{ .PID_0 = 1 }); From 55921cc00c41b2e4c0574be19abde28859a9ca45 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Tue, 13 Jan 2026 21:32:17 +0100 Subject: [PATCH 23/38] handle bus resets correctly --- core/src/core/usb.zig | 59 ++++++++++++++++--------- core/src/core/usb/drivers/cdc.zig | 6 +-- core/src/core/usb/drivers/example.zig | 11 +++-- core/src/core/usb/drivers/hid.zig | 4 +- port/raspberrypi/rp2xxx/src/hal/usb.zig | 7 ++- 5 files changed, 55 insertions(+), 32 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index db9692a7f..b7a2a5e93 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -294,7 +294,7 @@ pub fn DeviceController(config: Config) type { self.driver_last = null; switch (setup.request_type.recipient) { - .Device => try self.process_setup_request(device_itf, setup), + .Device => self.process_setup_request(device_itf, setup), .Interface => switch (@as(u8, @truncate(setup.index.into()))) { inline else => |itf_num| if (itf_num < handlers.itf.len) { const drv = handlers.itf[itf_num]; @@ -350,9 +350,11 @@ pub fn DeviceController(config: Config) type { } /// Called by the device implementation on bus reset. - pub fn on_bus_reset(self: *@This()) void { + pub fn on_bus_reset(self: *@This(), device_itf: *DeviceInterface) void { if (config.debug) log.info("bus reset", .{}); + self.process_set_config(device_itf, 0); + // Reset our state. self.* = .init; } @@ -377,7 +379,7 @@ pub fn DeviceController(config: Config) type { }; } - fn process_setup_request(self: *@This(), device_itf: *DeviceInterface, setup: *const types.SetupPacket) !void { + fn process_setup_request(self: *@This(), device_itf: *DeviceInterface, setup: *const types.SetupPacket) void { switch (setup.request_type.type) { .Class => { //const itfIndex = setup.index & 0x00ff; @@ -386,28 +388,19 @@ pub fn DeviceController(config: Config) type { .Standard => { switch (std.meta.intToEnum(types.SetupRequest, setup.request) catch return) { .SetAddress => { + if (config.debug) log.info(" SetAddress: {}", .{self.new_address.?}); self.new_address = @truncate(setup.value.into()); device_itf.ep_ack(.ep0); - if (config.debug) log.info(" SetAddress: {}", .{self.new_address.?}); }, .SetConfiguration => { if (config.debug) log.info(" SetConfiguration", .{}); - const new_cfg = setup.value.into(); - if (self.cfg_num != new_cfg) { - // if (self.cfg_num > 0) - // deinitialize drivers - - self.cfg_num = new_cfg; - - if (new_cfg > 0) - try self.process_set_config(device_itf, self.cfg_num - 1); - } + self.process_set_config(device_itf, setup.value.into()); device_itf.ep_ack(.ep0); }, .GetDescriptor => { const descriptor_type = std.meta.intToEnum(descriptor.Type, setup.value.into() >> 8) catch null; if (descriptor_type) |dt| { - try self.process_get_descriptor(device_itf, setup, dt); + self.process_get_descriptor(device_itf, setup, dt); } }, .SetFeature => { @@ -425,7 +418,7 @@ pub fn DeviceController(config: Config) type { } } - fn process_get_descriptor(self: *@This(), device_itf: *DeviceInterface, setup: *const types.SetupPacket, descriptor_type: descriptor.Type) !void { + fn process_get_descriptor(self: *@This(), device_itf: *DeviceInterface, setup: *const types.SetupPacket, descriptor_type: descriptor.Type) void { switch (descriptor_type) { .Device => { if (config.debug) log.info(" Device", .{}); @@ -468,16 +461,40 @@ pub fn DeviceController(config: Config) type { } } - fn process_set_config(self: *@This(), device_itf: *DeviceInterface, _: u16) !void { + fn process_set_config(self: *@This(), device_itf: *DeviceInterface, cfg_num: u16) void { + if (cfg_num == self.cfg_num) return; + + if (self.driver_data) |data_old| { + _ = data_old; + // deinitialize drivers + } + + self.cfg_num = cfg_num; + if (cfg_num == 0) { + self.driver_data = null; + return; + } + // We support just one config for now so ignore config index self.driver_data = @as(config0.Drivers, undefined); inline for (driver_fields) |fld_drv| { const cfg = @field(config_descriptor, fld_drv.name); - @field(self.driver_data.?, fld_drv.name) = .init(&cfg, device_itf); + const desc_fields = @typeInfo(@TypeOf(cfg)).@"struct".fields; + + // Open OUT endpoint first so that the driver can call ep_listen in init + inline for (desc_fields) |fld| { + const desc = &@field(cfg, fld.name); + if (comptime fld.type == descriptor.Endpoint and desc.endpoint.dir == .Out) + device_itf.ep_open(desc); + } + + @field(self.driver_data.?, fld_drv.name).init(&cfg, device_itf); - inline for (@typeInfo(@TypeOf(cfg)).@"struct".fields) |fld| { - if (comptime fld.type == descriptor.Endpoint) - device_itf.ep_open(&@field(cfg, fld.name)); + // Open IN endpoint last so that callbacks can happen + inline for (desc_fields) |fld| { + const desc = &@field(cfg, fld.name); + if (comptime fld.type == descriptor.Endpoint and desc.endpoint.dir == .In) + device_itf.ep_open(desc); } } } diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 0a1878986..918a12fff 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -166,9 +166,8 @@ pub fn CdcClassDriver(options: Options) type { return true; } - pub fn init(desc: *const Descriptor, device: *usb.DeviceInterface) @This() { - defer device.ep_listen(desc.ep_out.endpoint.num, options.max_packet_size); - return .{ + pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { + self.* = .{ .device = device, .ep_notifi = desc.ep_notifi.endpoint.num, .line_coding = .{ @@ -187,6 +186,7 @@ pub fn CdcClassDriver(options: Options) type { .tx_data = undefined, .tx_end = 0, }; + device.ep_listen(desc.ep_out.endpoint.num, options.max_packet_size); } pub fn class_control(self: *@This(), stage: types.ControlStage, setup: *const types.SetupPacket) ?[]const u8 { diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index 88eb4b2ff..fc92190bc 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -45,13 +45,16 @@ pub const EchoExampleDriver = struct { ep_tx: usb.types.Endpoint.Num, /// This function is called when the host chooses a configuration - /// that contains this driver. - pub fn init(desc: *const Descriptor, device: *usb.DeviceInterface) @This() { - defer device.ep_listen(desc.ep_out.endpoint.num, 64); - return .{ + /// that contains this driver. `self` points to undefined memory. + pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { + self.* = .{ .device = device, .ep_tx = desc.ep_in.endpoint.num, }; + device.ep_listen( + desc.ep_out.endpoint.num, + @intCast(desc.ep_out.max_packet_size.into()), + ); } /// Used for configuration through endpoint 0. diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index a9ba15780..b092060b1 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -56,8 +56,8 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { ep_in: types.Endpoint.Num, ep_out: types.Endpoint.Num, - pub fn init(desc: *const Descriptor, device: *usb.DeviceInterface) @This() { - return .{ + pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { + self.* = .{ .device = device, .ep_in = desc.ep_in.endpoint.num, .ep_out = desc.ep_out.endpoint.num, diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index e2bbabafa..4b37b8017 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -151,11 +151,14 @@ pub fn Polled(config: Config) type { // Has the host signaled a bus reset? if (ints.BUS_RESET != 0) { + // Abort all endpoints + peripherals.USB.EP_ABORT.raw = 0xFFFFFFFF; // Acknowledge by writing the write-one-to-clear status bit. peripherals.USB.SIE_STATUS.modify(.{ .BUS_RESET = 1 }); set_address(&self.interface, 0); - - controller.on_bus_reset(); + controller.on_bus_reset(&self.interface); + while (peripherals.USB.EP_ABORT_DONE.raw != 0xFFFFFFFF) {} + peripherals.USB.EP_ABORT.raw = 0; } } From bbe968ff7a646c78749273ab834f489b968195c0 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Tue, 13 Jan 2026 22:00:13 +0100 Subject: [PATCH 24/38] more examples cleanup --- core/src/core/usb/drivers/example.zig | 2 +- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 34 ++++++++++--------- examples/raspberrypi/rp2xxx/src/usb_hid.zig | 36 +++++++++++---------- port/raspberrypi/rp2xxx/src/hal/usb.zig | 1 + 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index fc92190bc..b40f8051f 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -7,8 +7,8 @@ pub const EchoExampleDriver = struct { /// The descriptors need to have the same memory layout as the sent data. pub const Descriptor = extern struct { example_interface: usb.descriptor.Interface, - ep_in: usb.descriptor.Endpoint, ep_out: usb.descriptor.Endpoint, + ep_in: usb.descriptor.Endpoint, /// This function is used during descriptor creation. If multiple instances /// of a driver are used, a descriptor will be created for each. diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index c127c44c7..03257af82 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -4,22 +4,17 @@ const microzig = @import("microzig"); const rp2xxx = microzig.hal; const time = rp2xxx.time; const gpio = rp2xxx.gpio; - const usb = microzig.core.usb; +const USB_Device = rp2xxx.usb.Polled(.{}); +const USB_Serial = usb.drivers.cdc.CdcClassDriver(.{ .max_packet_size = USB_Device.max_supported_packet_size }); -const led = gpio.num(25); -const uart = rp2xxx.uart.instance.num(0); -const uart_tx_pin = gpio.num(0); - -const USB_Serial = usb.drivers.cdc.CdcClassDriver(.{ .max_packet_size = 64 }); - -var usb_device: rp2xxx.usb.Polled(.{}) = undefined; +var usb_device: USB_Device = undefined; var usb_controller: usb.DeviceController(.{ .device_descriptor = .{ - .bcd_usb = .from(0x0200), + .bcd_usb = USB_Device.max_supported_bcd_usb, .device_triple = .from(.Miscellaneous, @enumFromInt(2), @enumFromInt(1)), - .max_packet_size0 = 64, + .max_packet_size0 = USB_Device.max_supported_packet_size, .vendor = .from(0x2E8A), .product = .from(0x000A), .bcd_device = .from(0x0100), @@ -42,7 +37,7 @@ var usb_controller: usb.DeviceController(.{ .max_current_ma = 50, .Drivers = struct { serial: USB_Serial }, }}, - .max_supported_packet_size = @TypeOf(usb_device).max_supported_packet_size, + .max_supported_packet_size = USB_Device.max_supported_packet_size, }) = .init; pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { @@ -56,16 +51,23 @@ pub const microzig_options = microzig.Options{ .logFn = rp2xxx.uart.log, }; +const pin_config: rp2xxx.pins.GlobalConfiguration = .{ + .GPIO0 = .{ .function = .UART0_TX }, + .GPIO25 = .{ .name = "led", .direction = .out }, +}; + +const pins = pin_config.pins(); + pub fn main() !void { - uart_tx_pin.set_function(.uart); + pin_config.apply(); + + const uart = rp2xxx.uart.instance.num(0); uart.apply(.{ .clock_config = rp2xxx.clock_config, }); rp2xxx.uart.init_logger(uart); - led.set_function(.sio); - led.set_direction(.out); - led.put(1); + pins.led.put(1); // Then initialize the USB device using the configuration defined above usb_device = .init(); @@ -82,7 +84,7 @@ pub fn main() !void { new = time.get_time_since_boot().to_us(); if (new - old > 500000) { old = new; - led.toggle(); + pins.led.toggle(); i += 1; std.log.info("cdc test: {}", .{i}); diff --git a/examples/raspberrypi/rp2xxx/src/usb_hid.zig b/examples/raspberrypi/rp2xxx/src/usb_hid.zig index 29d127e91..13dc99021 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -4,25 +4,20 @@ const microzig = @import("microzig"); const rp2xxx = microzig.hal; const time = rp2xxx.time; const gpio = rp2xxx.gpio; - const usb = microzig.core.usb; - -const led = gpio.num(25); -const uart = rp2xxx.uart.instance.num(0); -const uart_tx_pin = gpio.num(0); - -const HidDriver = usb.drivers.hid.HidClassDriver( +const USB_Device = rp2xxx.usb.Polled(.{}); +const HID_Driver = usb.drivers.hid.HidClassDriver( .{ .boot_protocol = true, .poll_interval = 0 }, usb.descriptor.hid.ReportDescriptorKeyboard, ); -var usb_device: rp2xxx.usb.Polled(.{}) = undefined; +var usb_device: USB_Device = undefined; var usb_controller: usb.DeviceController(.{ .device_descriptor = .{ - .bcd_usb = .from(0x0200), + .bcd_usb = USB_Device.max_supported_bcd_usb, .device_triple = .unspecified, - .max_packet_size0 = 64, + .max_packet_size0 = USB_Device.max_supported_packet_size, .vendor = .from(0x2E8A), .product = .from(0x000A), .bcd_device = .from(0x0100), @@ -43,9 +38,9 @@ var usb_controller: usb.DeviceController(.{ .configuration_s = 0, .attributes = .{ .self_powered = false }, .max_current_ma = 50, - .Drivers = struct { hid: HidDriver }, + .Drivers = struct { hid: HID_Driver }, }}, - .max_supported_packet_size = @TypeOf(usb_device).max_supported_packet_size, + .max_supported_packet_size = USB_Device.max_supported_packet_size, }) = .init; pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { @@ -59,16 +54,23 @@ pub const microzig_options = microzig.Options{ .logFn = rp2xxx.uart.log, }; +const pin_config: rp2xxx.pins.GlobalConfiguration = .{ + .GPIO0 = .{ .function = .UART0_TX }, + .GPIO25 = .{ .name = "led", .direction = .out }, +}; + +const pins = pin_config.pins(); + pub fn main() !void { - uart_tx_pin.set_function(.uart); + pin_config.apply(); + + const uart = rp2xxx.uart.instance.num(0); uart.apply(.{ .clock_config = rp2xxx.clock_config, }); rp2xxx.uart.init_logger(uart); - led.set_function(.sio); - led.set_direction(.out); - led.put(1); + pins.led.put(1); // Then initialize the USB device using the configuration defined above usb_device = .init(); @@ -85,7 +87,7 @@ pub fn main() !void { new = time.get_time_since_boot().to_us(); if (new - old > 500000) { old = new; - led.toggle(); + pins.led.toggle(); } } } diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 4b37b8017..86f37712f 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -82,6 +82,7 @@ pub fn Polled(config: Config) type { return struct { pub const max_supported_packet_size = 64; + pub const max_supported_bcd_usb: usb.types.U16_Le = .from(0x0210); const vtable: usb.DeviceInterface.VTable = .{ .ep_writev = ep_writev, From baee9804c8acd791d9cc319aed80f3ede679ca7f Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 17 Jan 2026 13:19:17 +0100 Subject: [PATCH 25/38] add BOS descriptor --- core/src/core/usb/descriptor.zig | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/src/core/usb/descriptor.zig b/core/src/core/usb/descriptor.zig index b46be3d9c..8baada416 100644 --- a/core/src/core/usb/descriptor.zig +++ b/core/src/core/usb/descriptor.zig @@ -17,7 +17,8 @@ pub const Type = enum(u8) { Interface = 0x04, Endpoint = 0x05, DeviceQualifier = 0x06, - InterfaceAssociation = 0x0b, + InterfaceAssociation = 0x0B, + BOS = 0x0F, CsDevice = 0x21, CsConfig = 0x22, CsString = 0x23, @@ -287,3 +288,20 @@ pub const InterfaceAssociation = extern struct { // Index of the string descriptor describing the associated interfaces. function: u8, }; + +pub const BOS = struct { + pub const Object = union(enum) {}; + + data: []const u8, + + pub fn from(objects: []const Object) @This() { + const data: []const u8 = ""; + const header: []const u8 = @ptrCast(&extern struct { + length: u8 = @sizeOf(@This()), + descriptor_type: Type = .BOS, + total_length: types.U16_Le = .from(@sizeOf(@This()) + data.len), + num_descriptors: u8 = @intCast(objects.len), + }{}); + return .{ .data = header ++ data }; + } +}; From ae7d20316bce87840a7b794fff0a25b6539ac876 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 17 Jan 2026 13:42:14 +0100 Subject: [PATCH 26/38] simplify setup packet handling --- core/src/core/usb.zig | 212 +++++++++++--------------- core/src/core/usb/drivers/cdc.zig | 33 ++-- core/src/core/usb/drivers/example.zig | 9 +- core/src/core/usb/drivers/hid.zig | 6 +- core/src/core/usb/types.zig | 10 +- 5 files changed, 110 insertions(+), 160 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index b7a2a5e93..6cc234a29 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -102,7 +102,6 @@ pub const Config = struct { device_descriptor: descriptor.Device, string_descriptors: []const descriptor.String, - debug: bool = false, /// Currently only a single configuration is supported. configurations: []const Configuration, /// Only use either IN or OUT on each endpoint. Useful for debugging. @@ -256,27 +255,22 @@ pub fn DeviceController(config: Config) type { break :blk ret; }; - /// When the host sets the device address, the acknowledgement - /// step must use the _old_ address. - new_address: ?u8, + /// If not zero, change the device address at the next opportunity. + /// Necessary because when the host sets the device address, + /// the acknowledgement step must use the _old_ address. + new_address: u8, /// 0 - no config set cfg_num: u16, /// Ep0 data waiting to be sent tx_slice: ?[]const u8, - /// Last setup packet request - setup_packet: types.SetupPacket, - /// Class driver associated with last setup request if any - driver_last: ?DriverEnum, /// Driver state driver_data: ?config0.Drivers, /// Initial values pub const init: @This() = .{ - .new_address = null, + .new_address = 0, .cfg_num = 0, .tx_slice = null, - .setup_packet = undefined, - .driver_last = null, .driver_data = null, }; @@ -288,41 +282,37 @@ pub fn DeviceController(config: Config) type { /// Called by the device implementation when a setup request has been received. pub fn on_setup_req(self: *@This(), device_itf: *DeviceInterface, setup: *const types.SetupPacket) void { - if (config.debug) log.info("setup req", .{}); - - self.setup_packet = setup.*; - self.driver_last = null; - - switch (setup.request_type.recipient) { - .Device => self.process_setup_request(device_itf, setup), - .Interface => switch (@as(u8, @truncate(setup.index.into()))) { - inline else => |itf_num| if (itf_num < handlers.itf.len) { - const drv = handlers.itf[itf_num]; - self.driver_last = drv; - self.driver_class_control(device_itf, drv, .Setup, setup); - }, - }, - .Endpoint => {}, - else => {}, + log.debug("on_setup_req", .{}); + + const ret = switch (setup.request_type.recipient) { + .Device => self.process_device_setup(device_itf, setup), + .Interface => self.process_interface_setup(setup), + else => nak, + }; + if (ret) |data| { + if (data.len == 0) + device_itf.ep_ack(.ep0) + else { + const limited = data[0..@min(data.len, setup.length.into())]; + const len = device_itf.ep_writev(.ep0, &.{limited}); + assert(len <= config.device_descriptor.max_packet_size0); + self.tx_slice = limited[len..]; + } } } /// Called by the device implementation when a packet has been sent or received. - pub fn on_buffer(self: *@This(), device_itf: *DeviceInterface, comptime ep: types.Endpoint, buffer: []u8) void { - if (config.debug) log.info("buff status", .{}); - if (config.debug) log.info(" data: {any}", .{buffer}); + pub fn on_buffer(self: *@This(), device_itf: *DeviceInterface, comptime ep: types.Endpoint, _: []u8) void { + log.debug("on_buffer {t} {t}", .{ ep.num, ep.dir }); const handler = comptime @field(handlers, @tagName(ep.dir))[@intFromEnum(ep.num)]; if (comptime ep == types.Endpoint.in(.ep0)) { - if (config.debug) log.info(" EP0_IN_ADDR", .{}); - // We use this opportunity to finish the delayed // SetAddress request, if there is one: - if (self.new_address) |addr| { - // Change our address: - device_itf.set_address(@intCast(addr)); - self.new_address = null; + if (self.new_address != 0) { + device_itf.set_address(@intCast(self.new_address)); + self.new_address = 0; } if (self.tx_slice) |slice| { @@ -336,13 +326,14 @@ pub fn DeviceController(config: Config) type { // device_itf.ep_listen(.ep0, 0); self.tx_slice = null; - if (self.driver_last) |drv| - self.driver_class_control(device_itf, drv, .Ack, &self.setup_packet); + // None of the drivers so far are using the ACK phase + // if (self.driver_last) |drv| + // self.driver_class_control(device_itf, drv, .Ack, &self.setup_packet); } } - } else if (comptime ep == types.Endpoint.out(.ep0)) { - log.info("ep0_out {}", .{buffer.len}); - } + } else if (comptime ep == types.Endpoint.out(.ep0)) + log.warn("Unhandled packet on ep0 Out", .{}); + if (comptime handler.driver.len != 0) { const drv = &@field(self.driver_data.?, handler.driver); @field(@FieldType(config0.Drivers, handler.driver), handler.function)(drv, ep.num); @@ -351,7 +342,7 @@ pub fn DeviceController(config: Config) type { /// Called by the device implementation on bus reset. pub fn on_bus_reset(self: *@This(), device_itf: *DeviceInterface) void { - if (config.debug) log.info("bus reset", .{}); + log.debug("on_bus_reset", .{}); self.process_set_config(device_itf, 0); @@ -361,104 +352,71 @@ pub fn DeviceController(config: Config) type { // Utility functions - /// Command response utility function that can split long data in multiple packets - fn send_cmd_response(self: *@This(), device_itf: *DeviceInterface, data: []const u8, expected_max_length: u16) void { - const limited = data[0..@min(data.len, expected_max_length)]; - const len = device_itf.ep_writev(.ep0, &.{limited}); - assert(len <= config.device_descriptor.max_packet_size0); - self.tx_slice = limited[len..]; - } - - fn driver_class_control(self: *@This(), device_itf: *DeviceInterface, driver: DriverEnum, stage: types.ControlStage, setup: *const types.SetupPacket) void { - return switch (driver) { - inline else => |d| { - const drv = &@field(self.driver_data.?, @tagName(d)); - if (drv.class_control(stage, setup)) |response| - self.send_cmd_response(device_itf, response, setup.length.into()); - }, - }; - } - - fn process_setup_request(self: *@This(), device_itf: *DeviceInterface, setup: *const types.SetupPacket) void { + fn process_device_setup(self: *@This(), device_itf: *DeviceInterface, setup: *const types.SetupPacket) ?[]const u8 { switch (setup.request_type.type) { - .Class => { - //const itfIndex = setup.index & 0x00ff; - log.info("Device.Class", .{}); - }, .Standard => { - switch (std.meta.intToEnum(types.SetupRequest, setup.request) catch return) { - .SetAddress => { - if (config.debug) log.info(" SetAddress: {}", .{self.new_address.?}); - self.new_address = @truncate(setup.value.into()); - device_itf.ep_ack(.ep0); - }, - .SetConfiguration => { - if (config.debug) log.info(" SetConfiguration", .{}); - self.process_set_config(device_itf, setup.value.into()); - device_itf.ep_ack(.ep0); - }, - .GetDescriptor => { - const descriptor_type = std.meta.intToEnum(descriptor.Type, setup.value.into() >> 8) catch null; - if (descriptor_type) |dt| { - self.process_get_descriptor(device_itf, setup, dt); + const request: types.SetupRequest = @enumFromInt(setup.request); + log.debug("Device setup: {t}", .{request}); + switch (request) { + .SetAddress => self.new_address = @truncate(setup.value.into()), + .SetConfiguration => self.process_set_config(device_itf, setup.value.into()), + .GetDescriptor => return get_descriptor(setup.value.into()), + .SetFeature => { + const feature: types.FeatureSelector = @enumFromInt(setup.value.into() >> 8); + switch (feature) { + .DeviceRemoteWakeup, .EndpointHalt => {}, + // TODO: https://github.com/ZigEmbeddedGroup/microzig/issues/453 + .TestMode => return nak, + else => return nak, } }, - .SetFeature => { - if (std.meta.intToEnum(types.FeatureSelector, setup.value.into() >> 8)) |feat| { - switch (feat) { - .DeviceRemoteWakeup, .EndpointHalt => device_itf.ep_ack(.ep0), - // TODO: https://github.com/ZigEmbeddedGroup/microzig/issues/453 - .TestMode => {}, - } - } else |_| {} + _ => { + log.warn("Unsupported standard request: {}", .{setup.request}); + return nak; }, } + return ack; + }, + else => |t| { + log.warn("Unhandled device setup request: {t}", .{t}); + return nak; }, - else => {}, } } - fn process_get_descriptor(self: *@This(), device_itf: *DeviceInterface, setup: *const types.SetupPacket, descriptor_type: descriptor.Type) void { - switch (descriptor_type) { - .Device => { - if (config.debug) log.info(" Device", .{}); - - self.send_cmd_response(device_itf, @ptrCast(&config.device_descriptor), setup.length.into()); + fn process_interface_setup(self: *@This(), setup: *const types.SetupPacket) ?[]const u8 { + const itf_num: u8 = @truncate(setup.index.into()); + switch (itf_num) { + inline else => |itf| if (comptime itf < handlers.itf.len) { + const drv = handlers.itf[itf]; + return @field(self.driver_data.?, @tagName(drv)).interface_setup(setup); + } else { + log.warn("Interface index ({}) out of range ({})", .{ itf_num, handlers.itf.len }); + return nak; }, - .Configuration => { - if (config.debug) log.info(" Config", .{}); + } + } - self.send_cmd_response(device_itf, @ptrCast(&config_descriptor), setup.length.into()); - }, - .String => { - if (config.debug) log.info(" String", .{}); - // String descriptor index is in bottom 8 bits of - // `value`. - const i: u8 = @truncate(setup.value.into()); - if (i >= config.string_descriptors.len) - log.warn("host requested invalid string descriptor {}", .{i}) - else - self.send_cmd_response( - device_itf, - config.string_descriptors[i].data, - setup.length.into(), - ); - }, - .Interface => { - if (config.debug) log.info(" Interface", .{}); - }, - .Endpoint => { - if (config.debug) log.info(" Endpoint", .{}); + fn get_descriptor(value: u16) ?[]const u8 { + const asBytes = std.mem.asBytes; + const desc_type: descriptor.Type = @enumFromInt(value >> 8); + const desc_idx: u8 = @truncate(value); + log.debug("Request for {t} descriptor {}", .{ desc_type, desc_idx }); + return switch (desc_type) { + .Device => asBytes(&config.device_descriptor), + .DeviceQualifier => asBytes(comptime &config.device_descriptor.qualifier()), + .Configuration => asBytes(&config_descriptor), + .String => if (desc_idx < config.string_descriptors.len) + config.string_descriptors[desc_idx].data + else { + log.warn( + "Descriptor index ({}) out of range ({})", + .{ desc_idx, config.string_descriptors.len }, + ); + return nak; }, - .DeviceQualifier => { - if (config.debug) log.info(" DeviceQualifier", .{}); - // We will just copy parts of the DeviceDescriptor because - // the DeviceQualifierDescriptor can be seen as a subset. - const qualifier = comptime &config.device_descriptor.qualifier(); - self.send_cmd_response(device_itf, @ptrCast(qualifier), setup.length.into()); - }, - else => {}, - } + else => nak, + }; } fn process_set_config(self: *@This(), device_itf: *DeviceInterface, cfg_num: u16) void { diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 918a12fff..3ff9446e4 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -9,6 +9,7 @@ pub const ManagementRequestType = enum(u8) { GetLineCoding = 0x21, SetControlLineState = 0x22, SendBreak = 0x23, + _, }; pub const LineCoding = extern struct { @@ -189,22 +190,22 @@ pub fn CdcClassDriver(options: Options) type { device.ep_listen(desc.ep_out.endpoint.num, options.max_packet_size); } - pub fn class_control(self: *@This(), stage: types.ControlStage, setup: *const types.SetupPacket) ?[]const u8 { - if (std.meta.intToEnum(ManagementRequestType, setup.request)) |request| { - if (stage == .Setup) switch (request) { - .SetLineCoding => return usb.ack, // we should handle data phase somehow to read sent line_coding - .GetLineCoding => return std.mem.asBytes(&self.line_coding), - .SetControlLineState => { - // const DTR_BIT = 1; - // self.is_ready = (setup.value & DTR_BIT) != 0; - // self.line_state = @intCast(setup.value & 0xFF); - return usb.ack; - }, - .SendBreak => return usb.ack, - }; - } else |_| {} - - return usb.nak; + pub fn interface_setup(self: *@This(), setup: *const types.SetupPacket) ?[]const u8 { + const mgmt_request: ManagementRequestType = @enumFromInt(setup.request); + std.log.debug("cdc setup: {t}", .{mgmt_request}); + + return switch (mgmt_request) { + .SetLineCoding => usb.ack, // we should handle data phase somehow to read sent line_coding + .GetLineCoding => std.mem.asBytes(&self.line_coding), + .SetControlLineState => blk: { + // const DTR_BIT = 1; + // self.is_ready = (setup.value & DTR_BIT) != 0; + // self.line_state = @intCast(setup.value & 0xFF); + break :blk usb.ack; + }, + .SendBreak => usb.ack, + else => usb.nak, + }; } pub fn on_rx(self: *@This(), ep_num: types.Endpoint.Num) void { diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index b40f8051f..156f3ba41 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -57,15 +57,12 @@ pub const EchoExampleDriver = struct { ); } - /// Used for configuration through endpoint 0. + /// Used for interface configuration through endpoint 0. /// Data returned by this function is sent on endpoint 0. - pub fn class_control(self: *@This(), stage: usb.types.ControlStage, setup: *const usb.types.SetupPacket) ?[]const u8 { + pub fn interface_setup(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { _ = self; _ = setup; - if (stage == .Setup) - return usb.ack - else - return usb.nak; + return usb.ack; } /// Each endpoint (as defined in the descriptor) has its own handler. diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index b092060b1..669cde2d1 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -64,9 +64,9 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { }; } - pub fn class_control(self: *@This(), stage: types.ControlStage, setup: *const types.SetupPacket) ?[]const u8 { + pub fn interface_setup(self: *@This(), setup: *const types.SetupPacket) ?[]const u8 { _ = self; - if (stage == .Setup) switch (setup.request_type.type) { + switch (setup.request_type.type) { .Standard => { const hid_desc_type = std.meta.intToEnum(descriptor.hid.Hid.Type, setup.value.into() >> 8) catch return usb.nak; const request_code = std.meta.intToEnum(types.SetupRequest, setup.request) catch return usb.nak; @@ -114,7 +114,7 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { } }, else => {}, - }; + } return usb.nak; } diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index c5e018f07..8cfa83390 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -207,19 +207,13 @@ pub const TransferType = enum(u2) { Interrupt = 3, }; -pub const ControlStage = enum { - Idle, - Setup, - Data, - Ack, -}; - /// The types of USB SETUP requests that we understand. pub const SetupRequest = enum(u8) { SetFeature = 0x03, SetAddress = 0x05, GetDescriptor = 0x06, SetConfiguration = 0x09, + _, }; pub const FeatureSelector = enum(u8) { @@ -227,6 +221,7 @@ pub const FeatureSelector = enum(u8) { DeviceRemoteWakeup = 0x01, TestMode = 0x02, // The remaining features only apply to OTG devices. + _, }; /// USB deals in two different transfer directions, called OUT (host-to-device) @@ -302,7 +297,6 @@ pub const SetupPacket = extern struct { request: u8, /// A simple argument of up to 16 bits, specific to the request. value: U16_Le, - /// Not used in the requests we support. index: U16_Le, /// If data will be transferred after this request (in the direction given /// by `request_type`), this gives the number of bytes (OUT) or maximum From a6e8c89deae736e65de3331d13f482e4705f22dd Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 17 Jan 2026 14:08:43 +0100 Subject: [PATCH 27/38] add usb device logging --- core/src/core/usb.zig | 2 +- port/raspberrypi/rp2xxx/src/hal/usb.zig | 89 +++++++++++++++---------- 2 files changed, 55 insertions(+), 36 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 6cc234a29..cd6f56c02 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -302,7 +302,7 @@ pub fn DeviceController(config: Config) type { } /// Called by the device implementation when a packet has been sent or received. - pub fn on_buffer(self: *@This(), device_itf: *DeviceInterface, comptime ep: types.Endpoint, _: []u8) void { + pub fn on_buffer(self: *@This(), device_itf: *DeviceInterface, comptime ep: types.Endpoint) void { log.debug("on_buffer {t} {t}", .{ ep.num, ep.dir }); const handler = comptime @field(handlers, @tagName(ep.dir))[@intFromEnum(ep.num)]; diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 86f37712f..9a75f2984 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -5,6 +5,7 @@ const std = @import("std"); const assert = std.debug.assert; +const log = std.log.scoped(.usb_dev); const microzig = @import("microzig"); const peripherals = microzig.chip.peripherals; @@ -18,6 +19,7 @@ pub const Config = struct { max_endpoints_count: comptime_int = RP2XXX_MAX_ENDPOINTS_COUNT, max_interfaces_count: comptime_int = 16, sync_noops: comptime_int = 3, + log_level: std.log.Level = .warn, }; const HardwareEndpointData = struct { @@ -82,7 +84,7 @@ pub fn Polled(config: Config) type { return struct { pub const max_supported_packet_size = 64; - pub const max_supported_bcd_usb: usb.types.U16_Le = .from(0x0210); + pub const max_supported_bcd_usb: usb.types.U16_Le = .from(0x0110); const vtable: usb.DeviceInterface.VTable = .{ .ep_writev = ep_writev, @@ -110,48 +112,47 @@ pub fn Polled(config: Config) type { buffer_control[0].in.modify(.{ .PID_0 = 0 }); const setup = get_setup_packet(); + if (config.log_level == .debug) + log.debug("setup {any}", .{setup}); controller.on_setup_req(&self.interface, &setup); } - var buff_status: u32 = 0; // Events on one or more buffers? (In practice, always one.) if (ints.BUFF_STATUS != 0) { - const bufbits_init = peripherals.USB.BUFF_STATUS.raw; - buff_status |= bufbits_init; - peripherals.USB.BUFF_STATUS.write_raw(bufbits_init); - } - - inline for (0..2 * config.max_endpoints_count) |shift| { - if (buff_status & (@as(u32, 1) << shift) != 0) { - // Here we exploit knowledge of the ordering of buffer control - // registers in the peripheral. Each endpoint has a pair of - // registers, IN being first - const ep_num = shift / 2; - const ep: usb.types.Endpoint = comptime .{ - .num = @enumFromInt(ep_num), - .dir = if (shift & 1 == 0) .In else .Out, - }; - - const ep_hard = self.hardware_endpoint_get_by_address(ep); - - // We should only get here if we've been notified that - // the buffer is ours again. This is indicated by the hw - // _clearing_ the AVAILABLE bit. - // - // It seems the hardware sets the AVAILABLE bit _after_ setting BUFF_STATUS - // So we wait for it just to be sure. - while (buffer_control[ep_num].get(ep.dir).read().AVAILABLE_0 != 0) {} - - // Get the actual length of the data, which may be less - // than the buffer size. - const len = buffer_control[ep_num].get(ep.dir).read().LENGTH_0; - - controller.on_buffer(&self.interface, ep, ep_hard.data_buffer[0..len]); + const buff_status = peripherals.USB.BUFF_STATUS.raw; + + inline for (0..2 * config.max_endpoints_count) |shift| { + if (buff_status & (@as(u32, 1) << shift) != 0) { + // Here we exploit knowledge of the ordering of buffer control + // registers in the peripheral. Each endpoint has a pair of + // registers, IN being first + const ep_num = shift / 2; + const ep: usb.types.Endpoint = comptime .{ + .num = @enumFromInt(ep_num), + .dir = if (shift % 2 == 0) .In else .Out, + }; + + // We should only get here if we've been notified that + // the buffer is ours again. This is indicated by the hw + // _clearing_ the AVAILABLE bit. + // + // It seems the hardware sets the AVAILABLE bit _after_ setting BUFF_STATUS + // So we wait for it just to be sure. + while (buffer_control[ep_num].get(ep.dir).read().AVAILABLE_0 != 0) {} + + if (config.log_level == .debug) + log.debug("buffer ep{} {t}", .{ ep_num, ep.dir }); + controller.on_buffer(&self.interface, ep); + } } + peripherals.USB.BUFF_STATUS.raw = buff_status; } // Has the host signaled a bus reset? if (ints.BUS_RESET != 0) { + if (config.log_level == .debug or config.log_level == .info) + log.debug("bus reset", .{}); + // Abort all endpoints peripherals.USB.EP_ABORT.raw = 0xFFFFFFFF; // Acknowledge by writing the write-one-to-clear status bit. @@ -253,6 +254,9 @@ pub fn Polled(config: Config) type { ep_num: usb.types.Endpoint.Num, data: []const []const u8, ) usb.types.Len { + if (config.log_level == .debug) + log.debug("writev {t} {}: {any}", .{ ep_num, data[0].len, data[0] }); + const self: *@This() = @fieldParentPtr("interface", itf); const bufctrl_ptr = &buffer_control[@intFromEnum(ep_num)].in; @@ -298,6 +302,9 @@ pub fn Polled(config: Config) type { ep_num: usb.types.Endpoint.Num, data: []const []u8, ) usb.types.Len { + if (config.log_level == .debug) + log.debug("readv {t} {}: {any}", .{ ep_num, data[0].len, data[0] }); + const self: *@This() = @fieldParentPtr("interface", itf); assert(data.len > 0); @@ -321,6 +328,9 @@ pub fn Polled(config: Config) type { ep_num: usb.types.Endpoint.Num, len: usb.types.Len, ) void { + if (config.log_level == .debug) + log.debug("listen {t} {}", .{ ep_num, len }); + const self: *@This() = @fieldParentPtr("interface", itf); const bufctrl_ptr = &buffer_control[@intFromEnum(ep_num)].out; @@ -367,6 +377,9 @@ pub fn Polled(config: Config) type { } fn set_address(_: *usb.DeviceInterface, addr: u7) void { + if (config.log_level == .debug) + log.debug("set addr {}", .{addr}); + peripherals.USB.ADDR_ENDP.write(.{ .ADDRESS = addr }); } @@ -375,11 +388,17 @@ pub fn Polled(config: Config) type { } fn ep_open(itf: *usb.DeviceInterface, desc: *const usb.descriptor.Endpoint) void { + const ep = desc.endpoint; + const attr = desc.attributes; + if (config.log_level == .debug) log.debug( + "ep open {t} {t} {{ type: {t}, sync: {t}, usage: {t}, size: {} }}", + .{ ep.num, ep.dir, attr.transfer_type, attr.synchronisation, attr.usage, desc.max_packet_size.into() }, + ); + const self: *@This() = @fieldParentPtr("interface", itf); assert(@intFromEnum(desc.endpoint.num) <= config.max_endpoints_count); - const ep = desc.endpoint; const ep_hard = self.hardware_endpoint_get_by_address(ep); assert(desc.max_packet_size.into() <= max_supported_packet_size); @@ -394,7 +413,7 @@ pub fn Polled(config: Config) type { endpoint_control[@intFromEnum(ep.num) - 1].get(ep.dir).write(.{ .ENABLE = 1, .INTERRUPT_PER_BUFF = 1, - .ENDPOINT_TYPE = @enumFromInt(@intFromEnum(desc.attributes.transfer_type)), + .ENDPOINT_TYPE = @enumFromInt(@intFromEnum(attr.transfer_type)), .BUFFER_ADDRESS = rp2xxx_buffers.data_offset(ep_hard.data_buffer), }); } From 66f01165b391585f9b77f8d75fa551f2590e1123 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 17 Jan 2026 14:26:06 +0100 Subject: [PATCH 28/38] better use scoped logging --- core/src/core/usb.zig | 2 +- core/src/core/usb/drivers/cdc.zig | 67 ++++++++++----------- core/src/core/usb/drivers/hid.zig | 30 ++++----- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 5 ++ examples/raspberrypi/rp2xxx/src/usb_hid.zig | 5 ++ port/raspberrypi/rp2xxx/src/hal/usb.zig | 25 +++----- 6 files changed, 68 insertions(+), 66 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index cd6f56c02..5b13350f5 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -1,6 +1,6 @@ const std = @import("std"); const assert = std.debug.assert; -const log = std.log.scoped(.usb); +const log = std.log.scoped(.usb_ctrl); pub const descriptor = @import("usb/descriptor.zig"); pub const drivers = struct { diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 3ff9446e4..fcad72f28 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -1,8 +1,7 @@ const std = @import("std"); const usb = @import("../../usb.zig"); const assert = std.debug.assert; -const descriptor = usb.descriptor; -const types = usb.types; +const log = std.log.scoped(.usb_cdc); pub const ManagementRequestType = enum(u8) { SetLineCoding = 0x20, @@ -33,16 +32,16 @@ pub const Options = struct { pub fn CdcClassDriver(options: Options) type { return struct { pub const Descriptor = extern struct { - itf_assoc: descriptor.InterfaceAssociation, - itf_notifi: descriptor.Interface, - cdc_header: descriptor.cdc.Header, - cdc_call_mgmt: descriptor.cdc.CallManagement, - cdc_acm: descriptor.cdc.AbstractControlModel, - cdc_union: descriptor.cdc.Union, - ep_notifi: descriptor.Endpoint, - itf_data: descriptor.Interface, - ep_out: descriptor.Endpoint, - ep_in: descriptor.Endpoint, + itf_assoc: usb.descriptor.InterfaceAssociation, + itf_notifi: usb.descriptor.Interface, + cdc_header: usb.descriptor.cdc.Header, + cdc_call_mgmt: usb.descriptor.cdc.CallManagement, + cdc_acm: usb.descriptor.cdc.AbstractControlModel, + cdc_union: usb.descriptor.cdc.Union, + ep_notifi: usb.descriptor.Endpoint, + itf_data: usb.descriptor.Interface, + ep_out: usb.descriptor.Endpoint, + ep_in: usb.descriptor.Endpoint, pub fn create( alloc: *usb.DescriptorAllocator, @@ -98,23 +97,23 @@ pub fn CdcClassDriver(options: Options) type { }; device: *usb.DeviceInterface, - ep_notifi: types.Endpoint.Num, + ep_notifi: usb.types.Endpoint.Num, line_coding: LineCoding align(4), /// OUT endpoint on which there is data ready to be read, /// or .ep0 when no data is available. - ep_out: types.Endpoint.Num, + ep_out: usb.types.Endpoint.Num, rx_data: [options.max_packet_size]u8, - rx_seek: types.Len, - rx_end: types.Len, + rx_seek: usb.types.Len, + rx_end: usb.types.Len, /// IN endpoint where data can be sent, /// or .ep0 when data is being sent. - ep_in: types.Endpoint.Num, + ep_in: usb.types.Endpoint.Num, tx_data: [options.max_packet_size]u8, - tx_end: types.Len, + tx_end: usb.types.Len, - pub fn available(self: *@This()) types.Len { + pub fn available(self: *@This()) usb.types.Len { return self.rx_end - self.rx_seek; } @@ -126,11 +125,11 @@ pub fn CdcClassDriver(options: Options) type { if (self.available() > 0) return len; // request more data - const ep_out = @atomicLoad(types.Endpoint.Num, &self.ep_out, .seq_cst); + const ep_out = @atomicLoad(usb.types.Endpoint.Num, &self.ep_out, .seq_cst); if (ep_out != .ep0) { self.rx_end = self.device.ep_readv(ep_out, &.{&self.rx_data}); self.rx_seek = 0; - @atomicStore(types.Endpoint.Num, &self.ep_out, .ep0, .seq_cst); + @atomicStore(usb.types.Endpoint.Num, &self.ep_out, .ep0, .seq_cst); self.device.ep_listen(ep_out, options.max_packet_size); } @@ -156,11 +155,11 @@ pub fn CdcClassDriver(options: Options) type { if (self.tx_end == 0) return true; - const ep_in = @atomicLoad(types.Endpoint.Num, &self.ep_in, .seq_cst); + const ep_in = @atomicLoad(usb.types.Endpoint.Num, &self.ep_in, .seq_cst); if (ep_in == .ep0) return false; - @atomicStore(types.Endpoint.Num, &self.ep_in, .ep0, .seq_cst); + @atomicStore(usb.types.Endpoint.Num, &self.ep_in, .ep0, .seq_cst); assert(self.tx_end == self.device.ep_writev(ep_in, &.{self.tx_data[0..self.tx_end]})); self.tx_end = 0; @@ -190,9 +189,9 @@ pub fn CdcClassDriver(options: Options) type { device.ep_listen(desc.ep_out.endpoint.num, options.max_packet_size); } - pub fn interface_setup(self: *@This(), setup: *const types.SetupPacket) ?[]const u8 { + pub fn interface_setup(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { const mgmt_request: ManagementRequestType = @enumFromInt(setup.request); - std.log.debug("cdc setup: {t}", .{mgmt_request}); + log.debug("cdc setup: {t}", .{mgmt_request}); return switch (mgmt_request) { .SetLineCoding => usb.ack, // we should handle data phase somehow to read sent line_coding @@ -208,19 +207,19 @@ pub fn CdcClassDriver(options: Options) type { }; } - pub fn on_rx(self: *@This(), ep_num: types.Endpoint.Num) void { - assert(.ep0 == @atomicLoad(types.Endpoint.Num, &self.ep_out, .seq_cst)); - @atomicStore(types.Endpoint.Num, &self.ep_out, ep_num, .seq_cst); + pub fn on_rx(self: *@This(), ep_num: usb.types.Endpoint.Num) void { + assert(.ep0 == @atomicLoad(usb.types.Endpoint.Num, &self.ep_out, .seq_cst)); + @atomicStore(usb.types.Endpoint.Num, &self.ep_out, ep_num, .seq_cst); } - pub fn on_tx_ready(self: *@This(), ep_num: types.Endpoint.Num) void { - assert(.ep0 == @atomicLoad(types.Endpoint.Num, &self.ep_in, .seq_cst)); - @atomicStore(types.Endpoint.Num, &self.ep_in, ep_num, .seq_cst); + pub fn on_tx_ready(self: *@This(), ep_num: usb.types.Endpoint.Num) void { + assert(.ep0 == @atomicLoad(usb.types.Endpoint.Num, &self.ep_in, .seq_cst)); + @atomicStore(usb.types.Endpoint.Num, &self.ep_in, ep_num, .seq_cst); } - pub fn on_notifi_ready(self: *@This(), ep_num: types.Endpoint.Num) void { - assert(.ep0 == @atomicLoad(types.Endpoint.Num, &self.ep_notifi, .seq_cst)); - @atomicStore(types.Endpoint.Num, &self.ep_notifi, ep_num, .seq_cst); + pub fn on_notifi_ready(self: *@This(), ep_num: usb.types.Endpoint.Num) void { + assert(.ep0 == @atomicLoad(usb.types.Endpoint.Num, &self.ep_notifi, .seq_cst)); + @atomicStore(usb.types.Endpoint.Num, &self.ep_notifi, ep_num, .seq_cst); } }; } diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index 669cde2d1..1052e11af 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -1,7 +1,7 @@ const std = @import("std"); const usb = @import("../../usb.zig"); -const descriptor = usb.descriptor; -const types = usb.types; +const assert = std.debug.assert; +const log = std.log.scoped(.usb_hid); pub const Options = struct { boot_protocol: bool, @@ -11,10 +11,10 @@ pub const Options = struct { pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { return struct { pub const Descriptor = extern struct { - interface: descriptor.Interface, - hid: descriptor.hid.Hid, - ep_out: descriptor.Endpoint, - ep_in: descriptor.Endpoint, + interface: usb.descriptor.Interface, + hid: usb.descriptor.hid.Hid, + ep_out: usb.descriptor.Endpoint, + ep_in: usb.descriptor.Endpoint, pub fn create( alloc: *usb.DescriptorAllocator, @@ -40,7 +40,7 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { } }; - const hid_descriptor: descriptor.hid.Hid = .{ + const hid_descriptor: usb.descriptor.hid.Hid = .{ .bcd_hid = .from(0x0111), .country_code = .NotSupported, .num_descriptors = 1, @@ -53,8 +53,8 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { }; device: *usb.DeviceInterface, - ep_in: types.Endpoint.Num, - ep_out: types.Endpoint.Num, + ep_in: usb.types.Endpoint.Num, + ep_out: usb.types.Endpoint.Num, pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { self.* = .{ @@ -64,12 +64,12 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { }; } - pub fn interface_setup(self: *@This(), setup: *const types.SetupPacket) ?[]const u8 { + pub fn interface_setup(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { _ = self; switch (setup.request_type.type) { .Standard => { - const hid_desc_type = std.meta.intToEnum(descriptor.hid.Hid.Type, setup.value.into() >> 8) catch return usb.nak; - const request_code = std.meta.intToEnum(types.SetupRequest, setup.request) catch return usb.nak; + const hid_desc_type = std.meta.intToEnum(usb.descriptor.hid.Hid.Type, setup.value.into() >> 8) catch return usb.nak; + const request_code = std.meta.intToEnum(usb.types.SetupRequest, setup.request) catch return usb.nak; if (request_code == .GetDescriptor and hid_desc_type == .Hid) return @as([]const u8, @ptrCast(&hid_descriptor)) @@ -77,7 +77,7 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { return @as([]const u8, @ptrCast(&report_descriptor)); }, .Class => { - const hid_request_type = std.meta.intToEnum(descriptor.hid.RequestType, setup.request) catch return usb.nak; + const hid_request_type = std.meta.intToEnum(usb.descriptor.hid.RequestType, setup.request) catch return usb.nak; switch (hid_request_type) { .SetIdle => { // TODO: https://github.com/ZigEmbeddedGroup/microzig/issues/454 @@ -118,12 +118,12 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { return usb.nak; } - pub fn on_tx_ready(self: *@This(), ep: types.Endpoint.Num) void { + pub fn on_tx_ready(self: *@This(), ep: usb.types.Endpoint.Num) void { _ = self; _ = ep; } - pub fn on_rx(self: *@This(), ep: types.Endpoint.Num) void { + pub fn on_rx(self: *@This(), ep: usb.types.Endpoint.Num) void { _ = self; _ = ep; } diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 03257af82..30949bc32 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -48,6 +48,11 @@ pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noretu pub const microzig_options = microzig.Options{ .log_level = .debug, + .log_scope_levels = &.{ + .{ .scope = .usb_dev, .level = .warn }, + .{ .scope = .usb_ctrl, .level = .warn }, + .{ .scope = .usb_cdc, .level = .warn }, + }, .logFn = rp2xxx.uart.log, }; diff --git a/examples/raspberrypi/rp2xxx/src/usb_hid.zig b/examples/raspberrypi/rp2xxx/src/usb_hid.zig index 13dc99021..2105a9b34 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -51,6 +51,11 @@ pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noretu pub const microzig_options = microzig.Options{ .log_level = .debug, + .log_scope_levels = &.{ + .{ .scope = .usb_dev, .level = .warn }, + .{ .scope = .usb_ctrl, .level = .warn }, + .{ .scope = .usb_hid, .level = .warn }, + }, .logFn = rp2xxx.uart.log, }; diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 9a75f2984..4033f7484 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -19,7 +19,6 @@ pub const Config = struct { max_endpoints_count: comptime_int = RP2XXX_MAX_ENDPOINTS_COUNT, max_interfaces_count: comptime_int = 16, sync_noops: comptime_int = 3, - log_level: std.log.Level = .warn, }; const HardwareEndpointData = struct { @@ -112,8 +111,8 @@ pub fn Polled(config: Config) type { buffer_control[0].in.modify(.{ .PID_0 = 0 }); const setup = get_setup_packet(); - if (config.log_level == .debug) - log.debug("setup {any}", .{setup}); + + log.debug("setup {any}", .{setup}); controller.on_setup_req(&self.interface, &setup); } @@ -140,8 +139,7 @@ pub fn Polled(config: Config) type { // So we wait for it just to be sure. while (buffer_control[ep_num].get(ep.dir).read().AVAILABLE_0 != 0) {} - if (config.log_level == .debug) - log.debug("buffer ep{} {t}", .{ ep_num, ep.dir }); + log.debug("buffer ep{} {t}", .{ ep_num, ep.dir }); controller.on_buffer(&self.interface, ep); } } @@ -150,8 +148,7 @@ pub fn Polled(config: Config) type { // Has the host signaled a bus reset? if (ints.BUS_RESET != 0) { - if (config.log_level == .debug or config.log_level == .info) - log.debug("bus reset", .{}); + log.info("bus reset", .{}); // Abort all endpoints peripherals.USB.EP_ABORT.raw = 0xFFFFFFFF; @@ -254,8 +251,7 @@ pub fn Polled(config: Config) type { ep_num: usb.types.Endpoint.Num, data: []const []const u8, ) usb.types.Len { - if (config.log_level == .debug) - log.debug("writev {t} {}: {any}", .{ ep_num, data[0].len, data[0] }); + log.debug("writev {t} {}: {any}", .{ ep_num, data[0].len, data[0] }); const self: *@This() = @fieldParentPtr("interface", itf); @@ -302,8 +298,7 @@ pub fn Polled(config: Config) type { ep_num: usb.types.Endpoint.Num, data: []const []u8, ) usb.types.Len { - if (config.log_level == .debug) - log.debug("readv {t} {}: {any}", .{ ep_num, data[0].len, data[0] }); + log.debug("readv {t} {}: {any}", .{ ep_num, data[0].len, data[0] }); const self: *@This() = @fieldParentPtr("interface", itf); assert(data.len > 0); @@ -328,8 +323,7 @@ pub fn Polled(config: Config) type { ep_num: usb.types.Endpoint.Num, len: usb.types.Len, ) void { - if (config.log_level == .debug) - log.debug("listen {t} {}", .{ ep_num, len }); + log.debug("listen {t} {}", .{ ep_num, len }); const self: *@This() = @fieldParentPtr("interface", itf); const bufctrl_ptr = &buffer_control[@intFromEnum(ep_num)].out; @@ -377,8 +371,7 @@ pub fn Polled(config: Config) type { } fn set_address(_: *usb.DeviceInterface, addr: u7) void { - if (config.log_level == .debug) - log.debug("set addr {}", .{addr}); + log.debug("set addr {}", .{addr}); peripherals.USB.ADDR_ENDP.write(.{ .ADDRESS = addr }); } @@ -390,7 +383,7 @@ pub fn Polled(config: Config) type { fn ep_open(itf: *usb.DeviceInterface, desc: *const usb.descriptor.Endpoint) void { const ep = desc.endpoint; const attr = desc.attributes; - if (config.log_level == .debug) log.debug( + log.debug( "ep open {t} {t} {{ type: {t}, sync: {t}, usage: {t}, size: {} }}", .{ ep.num, ep.dir, attr.transfer_type, attr.synchronisation, attr.usage, desc.max_packet_size.into() }, ); From f07fe5c941bbe2e87f9e94e98786e21293294611 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 17 Jan 2026 14:37:28 +0100 Subject: [PATCH 29/38] reorganize ClassSubclassProtocol --- core/src/core/usb/types.zig | 334 ++++++++++++++++++++++-------------- 1 file changed, 209 insertions(+), 125 deletions(-) diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index 8cfa83390..e592e0ed0 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -32,145 +32,229 @@ pub const ClassSubclassProtocol = extern struct { _, pub fn Subclass(self: @This()) type { - const name = "Subclass" ++ @tagName(self); - return if (@hasDecl(ClassSubclassProtocol, name)) - @field(ClassSubclassProtocol, name) - else - ClassSubclassProtocol.SubclassDefault; + return @field(ClassSubclassProtocol.Subclass, @tagName(self)); } pub fn Protocol(self: @This()) type { - const name = "Protocol" ++ @tagName(self); - return if (@hasDecl(ClassSubclassProtocol, name)) - @field(ClassSubclassProtocol, name) - else - ClassSubclassProtocol.ProtocolDefault; + return @field(ClassSubclassProtocol.Protocol, @tagName(self)); } }; - pub const SubclassDefault = enum(u8) { - Unspecified = 0x00, - VendorSpecific = 0xFF, - _, - }; + pub const Subclass = struct { + pub const Default = enum(u8) { + Unspecified = 0x00, + VendorSpecific = 0xFF, + _, + }; - pub const ProtocolDefault = enum(u8) { - NoneRequired = 0x00, - VendorSpecific = 0xFF, - _, - }; + pub const Unspecified = Default; + pub const Audio = Default; + + pub const Cdc = enum(u8) { + Unspecified = 0x00, + VendorSpecific = 0xFF, + /// Direct Line Control Model + DirectLine = 0x01, + /// Abstract Control Model + Abstract = 0x02, + /// Telephone Control Model + Telephone = 0x03, + /// Multi-Channel Control Model + MultChannel = 0x04, + /// CAPI Control Model + CAPI = 0x05, + /// Ethernet Networking Control Model + Ethernet = 0x06, + /// ATM Networking Control Model + ATM_Networking = 0x07, + /// Wireless Handset Control Model + WirelessHeadest = 0x08, + /// Device Management + DeviceManagement = 0x09, + /// Mobile Direct Line Model + MobileDirect = 0x0A, + /// OBEX + OBEX = 0x0B, + /// Ethernet Emulation Model + EthernetEmulation = 0x0C, + /// Network Control Model + Network = 0x0D, + _, + }; - pub const SubclassCdc = enum(u8) { - Unspecified = 0x00, - VendorSpecific = 0xFF, - /// Direct Line Control Model - DirectLine = 0x01, - /// Abstract Control Model - Abstract = 0x02, - /// Telephone Control Model - Telephone = 0x03, - /// Multi-Channel Control Model - MultChannel = 0x04, - /// CAPI Control Model - CAPI = 0x05, - /// Ethernet Networking Control Model - Ethernet = 0x06, - /// ATM Networking Control Model - ATM_Networking = 0x07, - /// Wireless Handset Control Model - WirelessHeadest = 0x08, - /// Device Management - DeviceManagement = 0x09, - /// Mobile Direct Line Model - MobileDirect = 0x0A, - /// OBEX - OBEX = 0x0B, - /// Ethernet Emulation Model - EthernetEmulation = 0x0C, - /// Network Control Model - Network = 0x0D, - _, - }; + pub const Hid = enum(u8) { + Unspecified = 0x00, + VendorSpecific = 0xFF, + /// + Boot = 0x01, + _, + }; - pub const ProtocolCdc = enum(u8) { - /// USB specification No class specific protocol required - NoneRequired = 0x00, - /// ITU-T V.250 AT Commands: V.250 etc - AT_ITU_T_V_250 = 0x01, - /// PCCA-101 AT Commands defined by PCCA-101 - AT_PCCA_101 = 0x02, - /// PCCA-101 AT Commands defined by PCCA-101 & Annex O - AT_PCCA_101_O = 0x03, - /// GSM 7.07 AT Commands defined by GSM 07.07 - AT_GSM_7_07 = 0x04, - /// 3GPP 27.07 AT Commands defined by 3GPP 27.007 - AT_3GPP_27_07 = 0x05, - /// C-S0017-0 AT Commands defined by TIA for CDMA - AT_C_S0017_0 = 0x06, - /// USB EEM Ethernet Emulation module - USB_EEM = 0x07, - /// External Protocol: Commands defined by Command Set Functional Descriptor - External = 0xFE, - /// USB Specification Vendor-specific - VendorSpecific = 0xFF, - _, - }; + pub const Physical = Default; + pub const Image = Default; + pub const Printer = Default; + + pub const MassStorage = enum(u8) { + /// SCSI command set not reported. De facto use. + Unspecified = 0x00, + /// Allocated by USB-IF for RBC. RBC is defined outside of USB. + RBC = 0x01, + /// Allocated by USB-IF for MMC-5 (ATAPI). MMC-5 is defined outside of USB. + MMC_5 = 0x02, + /// Obsolete. Was QIC-157 + QIC_157 = 0x03, + /// Specifies how to interface Floppy Disk Drives to USB + UFI = 0x04, + /// Obsolete. Was SFF-8070i + SFF_8070i = 0x05, + /// SCSI transparent command set. Allocated by USB-IF for SCSI. SCSI standards are defined outside of USB. + SCSI = 0x06, + /// LSDFS. specifies how host has to negotiate access before trying SCSI + LSD_FS = 0x07, + /// Allocated by USB-IF for IEEE 1667. IEEE 1667 is defined outside of USB. + IEEE_1667 = 0x08, + /// Specific to device vendor. De facto use + VendorSpecific = 0xFF, + _, + }; - pub const SubclassCdcData = enum(u8) { - Unused = 0, - VendorSpecific = 0xFF, - _, - }; + pub const Hub = Default; - pub const ProtocolCdcData = enum(u8) { - NoneRequired = 0, - VendorSpecific = 0xFF, - /// Network Transfer Block - NetworkTransferBlock = 0x01, - /// Physical interface protocol for ISDN BRI - ISDN_BRI = 0x30, - /// HDLC - HDLC = 0x31, - /// Transparent - Transparent = 0x32, - /// Management protocol for Q.921 data link protocol - Management_Q_921 = 0x50, - /// Data link protocol for Q.931 - DataLink_Q_931 = 0x51, - /// TEI-multiplexor for Q.921 data link protocol - TEI_Multiplexor_Q_921 = 0x52, - /// Data compression procedures - DataCompressionProcedures = 0x90, - /// Euro-ISDN protocol control - Euro_ISDN = 0x91, - /// V.24 rate adaptation to ISDN - RateAdaptation_V_24 = 0x92, - /// CAPI Commands - CAPI = 0x93, - /// Host based driver. Note: This protocol code should only be used - /// in messages between host and device to identify the host driver - /// portion of a protocol stack. - HostBasedDriver = 0xFD, - /// CDC Specification The protocol(s) are described using a Protocol - /// Unit Functional Descriptors on Communications Class Interface - SpecifiedIn_PUF_Descriptor = 0xFE, - _, - }; + pub const CdcData = enum(u8) { + Unused = 0, + VendorSpecific = 0xFF, + _, + }; - pub const SubclassHid = enum(u8) { - Unspecified = 0x00, - VendorSpecific = 0xFF, - /// - Boot = 0x01, - _, + pub const SmartCard = Default; + pub const ContentSecurity = Default; + pub const Video = Default; + pub const PersonalHealthcare = Default; + pub const AudioVideoDevice = Default; + pub const BillboardDevice = Default; + pub const USBTypeCBridge = Default; + pub const USBBulkDisplayProtocol = Default; + pub const MCTPoverUSBProtocolEndpoint = Default; + pub const I3C = Default; + pub const DiagnosticDevice = Default; + pub const WirelessController = Default; + pub const Miscellaneous = Default; + pub const ApplicationSpecific = Default; + pub const VendorSpecific = Default; }; - pub const ProtocolHid = enum(u8) { - NoneRequired = 0x00, - VendorSpecific = 0xFF, - /// - Boot = 0x01, - _, + pub const Protocol = struct { + pub const Default = enum(u8) { + NoneRequired = 0x00, + VendorSpecific = 0xFF, + _, + }; + + pub const Unspecified = Default; + pub const Audio = Default; + + pub const Cdc = enum(u8) { + /// USB specification No class specific protocol required + NoneRequired = 0x00, + /// ITU-T V.250 AT Commands: V.250 etc + AT_ITU_T_V_250 = 0x01, + /// PCCA-101 AT Commands defined by PCCA-101 + AT_PCCA_101 = 0x02, + /// PCCA-101 AT Commands defined by PCCA-101 & Annex O + AT_PCCA_101_O = 0x03, + /// GSM 7.07 AT Commands defined by GSM 07.07 + AT_GSM_7_07 = 0x04, + /// 3GPP 27.07 AT Commands defined by 3GPP 27.007 + AT_3GPP_27_07 = 0x05, + /// C-S0017-0 AT Commands defined by TIA for CDMA + AT_C_S0017_0 = 0x06, + /// USB EEM Ethernet Emulation module + USB_EEM = 0x07, + /// External Protocol: Commands defined by Command Set Functional Descriptor + External = 0xFE, + /// USB Specification Vendor-specific + VendorSpecific = 0xFF, + _, + }; + + pub const Hid = enum(u8) { + NoneRequired = 0x00, + VendorSpecific = 0xFF, + /// + Boot = 0x01, + _, + }; + + pub const Physical = Default; + pub const Image = Default; + pub const Printer = Default; + + pub const MassStorage = enum(u8) { + /// USB Mass Storage Class Control/Bulk/Interrupt (CBI) Transport with command completion interrupt + CBI_with_interrupt = 0x00, + /// USB Mass Storage Class Control/Bulk/Interrupt (CBI) Transport with no command completion interrupt + CBI_no_interrupt = 0x01, + /// USB Mass Storage Class Bulk-Only (BBB) Transport + BulkOnly = 0x50, + /// Allocated by USB-IF for UAS. UAS is defined outside of USB. + UAS = 0x62, + /// Specific to device vendor De facto use + VendorSpecific = 0xFF, + _, + }; + + pub const Hub = Default; + + pub const CdcData = enum(u8) { + NoneRequired = 0, + VendorSpecific = 0xFF, + /// Network Transfer Block + NetworkTransferBlock = 0x01, + /// Physical interface protocol for ISDN BRI + ISDN_BRI = 0x30, + /// HDLC + HDLC = 0x31, + /// Transparent + Transparent = 0x32, + /// Management protocol for Q.921 data link protocol + Management_Q_921 = 0x50, + /// Data link protocol for Q.931 + DataLink_Q_931 = 0x51, + /// TEI-multiplexor for Q.921 data link protocol + TEI_Multiplexor_Q_921 = 0x52, + /// Data compression procedures + DataCompressionProcedures = 0x90, + /// Euro-ISDN protocol control + Euro_ISDN = 0x91, + /// V.24 rate adaptation to ISDN + RateAdaptation_V_24 = 0x92, + /// CAPI Commands + CAPI = 0x93, + /// Host based driver. Note: This protocol code should only be used + /// in messages between host and device to identify the host driver + /// portion of a protocol stack. + HostBasedDriver = 0xFD, + /// CDC Specification The protocol(s) are described using a Protocol + /// Unit Functional Descriptors on Communications Class Interface + SpecifiedIn_PUF_Descriptor = 0xFE, + _, + }; + + pub const SmartCard = Default; + pub const ContentSecurity = Default; + pub const Video = Default; + pub const PersonalHealthcare = Default; + pub const AudioVideoDevice = Default; + pub const BillboardDevice = Default; + pub const USBTypeCBridge = Default; + pub const USBBulkDisplayProtocol = Default; + pub const MCTPoverUSBProtocolEndpoint = Default; + pub const I3C = Default; + pub const DiagnosticDevice = Default; + pub const WirelessController = Default; + pub const Miscellaneous = Default; + pub const ApplicationSpecific = Default; + pub const VendorSpecific = Default; }; /// Class code, distinguishing the type of interface. From c40f9075be01abc1ce760cc90f8f5dc5fdf66428 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 17 Jan 2026 14:42:59 +0100 Subject: [PATCH 30/38] adhere more to style guidelines --- core/src/core/usb.zig | 2 +- core/src/core/usb/descriptor/hid.zig | 4 ++-- core/src/core/usb/drivers/cdc.zig | 4 ++-- core/src/core/usb/drivers/hid.zig | 10 ++++---- core/src/core/usb/types.zig | 36 ++++++++++++++-------------- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 5b13350f5..74efc30ae 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -161,7 +161,7 @@ pub fn DeviceController(config: Config) type { descriptor.cdc.CallManagement, descriptor.cdc.AbstractControlModel, descriptor.cdc.Union, - descriptor.hid.Hid, + descriptor.hid.HID, => {}, else => @compileLog(fld), } diff --git a/core/src/core/usb/descriptor/hid.zig b/core/src/core/usb/descriptor/hid.zig index a77224545..cb5995e1f 100644 --- a/core/src/core/usb/descriptor/hid.zig +++ b/core/src/core/usb/descriptor/hid.zig @@ -14,7 +14,7 @@ pub const RequestType = enum(u8) { }; /// USB HID descriptor -pub const Hid = extern struct { +pub const HID = extern struct { /// HID country codes pub const CountryCode = enum(u8) { NotSupported = 0, @@ -56,7 +56,7 @@ pub const Hid = extern struct { }; pub const Type = enum(u8) { - Hid = 0x21, + HID = 0x21, Report = 0x22, Physical = 0x23, }; diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index fcad72f28..934419782 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -63,7 +63,7 @@ pub fn CdcClassDriver(options: Options) type { .interface_number = itf_notifi, .alternate_setting = 0, .num_endpoints = 1, - .interface_triple = .from(.Cdc, .Abstract, .NoneRequired), + .interface_triple = .from(.CDC, .Abstract, .NoneRequired), .interface_s = first_string, }, .cdc_header = .{ .bcd_cdc = .from(0x0120) }, @@ -81,7 +81,7 @@ pub fn CdcClassDriver(options: Options) type { .interface_number = itf_data, .alternate_setting = 0, .num_endpoints = 2, - .interface_triple = .from(.CdcData, .Unused, .NoneRequired), + .interface_triple = .from(.CDC_Data, .Unused, .NoneRequired), .interface_s = 0, }, .ep_out = .bulk(alloc.next_ep(.Out), max_supported_packet_size), diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index 1052e11af..63529c42d 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -12,7 +12,7 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { return struct { pub const Descriptor = extern struct { interface: usb.descriptor.Interface, - hid: usb.descriptor.hid.Hid, + hid: usb.descriptor.hid.HID, ep_out: usb.descriptor.Endpoint, ep_in: usb.descriptor.Endpoint, @@ -27,7 +27,7 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { .alternate_setting = 0, .num_endpoints = 2, .interface_triple = .from( - .Hid, + .HID, if (options.boot_protocol) .Boot else .Unspecified, if (options.boot_protocol) .Boot else .None, ), @@ -40,7 +40,7 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { } }; - const hid_descriptor: usb.descriptor.hid.Hid = .{ + const hid_descriptor: usb.descriptor.hid.HID = .{ .bcd_hid = .from(0x0111), .country_code = .NotSupported, .num_descriptors = 1, @@ -68,10 +68,10 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { _ = self; switch (setup.request_type.type) { .Standard => { - const hid_desc_type = std.meta.intToEnum(usb.descriptor.hid.Hid.Type, setup.value.into() >> 8) catch return usb.nak; + const hid_desc_type = std.meta.intToEnum(usb.descriptor.hid.HID.Type, setup.value.into() >> 8) catch return usb.nak; const request_code = std.meta.intToEnum(usb.types.SetupRequest, setup.request) catch return usb.nak; - if (request_code == .GetDescriptor and hid_desc_type == .Hid) + if (request_code == .GetDescriptor and hid_desc_type == .HID) return @as([]const u8, @ptrCast(&hid_descriptor)) else if (request_code == .GetDescriptor and hid_desc_type == .Report) return @as([]const u8, @ptrCast(&report_descriptor)); diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index e592e0ed0..60862742a 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -6,23 +6,23 @@ pub const ClassSubclassProtocol = extern struct { pub const ClassCode = enum(u8) { Unspecified = 0x00, Audio = 0x01, - Cdc = 0x02, - Hid = 0x03, + CDC = 0x02, + HID = 0x03, Physical = 0x05, Image = 0x06, Printer = 0x07, MassStorage = 0x08, Hub = 0x09, - CdcData = 0x0A, + CDC_Data = 0x0A, SmartCard = 0x0B, ContentSecurity = 0x0D, Video = 0x0E, PersonalHealthcare = 0x0F, AudioVideoDevice = 0x10, BillboardDevice = 0x11, - USBTypeCBridge = 0x12, - USBBulkDisplayProtocol = 0x13, - MCTPoverUSBProtocolEndpoint = 0x14, + TypeCBridge = 0x12, + BulkDisplayProtocol = 0x13, + MCTP_over_USB_ProtocolEndpoint = 0x14, I3C = 0x3C, DiagnosticDevice = 0xDC, WirelessController = 0xE0, @@ -50,7 +50,7 @@ pub const ClassSubclassProtocol = extern struct { pub const Unspecified = Default; pub const Audio = Default; - pub const Cdc = enum(u8) { + pub const CDC = enum(u8) { Unspecified = 0x00, VendorSpecific = 0xFF, /// Direct Line Control Model @@ -82,7 +82,7 @@ pub const ClassSubclassProtocol = extern struct { _, }; - pub const Hid = enum(u8) { + pub const HID = enum(u8) { Unspecified = 0x00, VendorSpecific = 0xFF, /// @@ -120,7 +120,7 @@ pub const ClassSubclassProtocol = extern struct { pub const Hub = Default; - pub const CdcData = enum(u8) { + pub const CDC_Data = enum(u8) { Unused = 0, VendorSpecific = 0xFF, _, @@ -132,9 +132,9 @@ pub const ClassSubclassProtocol = extern struct { pub const PersonalHealthcare = Default; pub const AudioVideoDevice = Default; pub const BillboardDevice = Default; - pub const USBTypeCBridge = Default; - pub const USBBulkDisplayProtocol = Default; - pub const MCTPoverUSBProtocolEndpoint = Default; + pub const TypeCBridge = Default; + pub const BulkDisplayProtocol = Default; + pub const MCTP_over_USB_ProtocolEndpoint = Default; pub const I3C = Default; pub const DiagnosticDevice = Default; pub const WirelessController = Default; @@ -153,7 +153,7 @@ pub const ClassSubclassProtocol = extern struct { pub const Unspecified = Default; pub const Audio = Default; - pub const Cdc = enum(u8) { + pub const CDC = enum(u8) { /// USB specification No class specific protocol required NoneRequired = 0x00, /// ITU-T V.250 AT Commands: V.250 etc @@ -177,7 +177,7 @@ pub const ClassSubclassProtocol = extern struct { _, }; - pub const Hid = enum(u8) { + pub const HID = enum(u8) { NoneRequired = 0x00, VendorSpecific = 0xFF, /// @@ -205,7 +205,7 @@ pub const ClassSubclassProtocol = extern struct { pub const Hub = Default; - pub const CdcData = enum(u8) { + pub const CDC_Data = enum(u8) { NoneRequired = 0, VendorSpecific = 0xFF, /// Network Transfer Block @@ -246,9 +246,9 @@ pub const ClassSubclassProtocol = extern struct { pub const PersonalHealthcare = Default; pub const AudioVideoDevice = Default; pub const BillboardDevice = Default; - pub const USBTypeCBridge = Default; - pub const USBBulkDisplayProtocol = Default; - pub const MCTPoverUSBProtocolEndpoint = Default; + pub const TypeCBridge = Default; + pub const BulkDisplayProtocol = Default; + pub const MCTP_over_USB_ProtocolEndpoint = Default; pub const I3C = Default; pub const DiagnosticDevice = Default; pub const WirelessController = Default; From b9f61f5c6eedd2bac547767ff2fe74e7e19c2c9b Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 17 Jan 2026 14:45:05 +0100 Subject: [PATCH 31/38] ...again --- core/src/core/usb/types.zig | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index 60862742a..7a53ede4a 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -20,9 +20,9 @@ pub const ClassSubclassProtocol = extern struct { PersonalHealthcare = 0x0F, AudioVideoDevice = 0x10, BillboardDevice = 0x11, - TypeCBridge = 0x12, + Type_C_Bridge = 0x12, BulkDisplayProtocol = 0x13, - MCTP_over_USB_ProtocolEndpoint = 0x14, + MCTP_Over_USB_ProtocolEndpoint = 0x14, I3C = 0x3C, DiagnosticDevice = 0xDC, WirelessController = 0xE0, @@ -132,9 +132,9 @@ pub const ClassSubclassProtocol = extern struct { pub const PersonalHealthcare = Default; pub const AudioVideoDevice = Default; pub const BillboardDevice = Default; - pub const TypeCBridge = Default; + pub const Type_C_Bridge = Default; pub const BulkDisplayProtocol = Default; - pub const MCTP_over_USB_ProtocolEndpoint = Default; + pub const MCTP_Over_USB_ProtocolEndpoint = Default; pub const I3C = Default; pub const DiagnosticDevice = Default; pub const WirelessController = Default; @@ -246,9 +246,9 @@ pub const ClassSubclassProtocol = extern struct { pub const PersonalHealthcare = Default; pub const AudioVideoDevice = Default; pub const BillboardDevice = Default; - pub const TypeCBridge = Default; + pub const Type_C_Bridge = Default; pub const BulkDisplayProtocol = Default; - pub const MCTP_over_USB_ProtocolEndpoint = Default; + pub const MCTP_Over_USB_ProtocolEndpoint = Default; pub const I3C = Default; pub const DiagnosticDevice = Default; pub const WirelessController = Default; From 4c399efbfeb708a47b28480fec21f4b3ef5ebc15 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sun, 18 Jan 2026 13:55:53 +0100 Subject: [PATCH 32/38] add enums to CDC --- core/src/core/usb.zig | 5 ++- core/src/core/usb/descriptor/cdc.zig | 30 ++++++++++++- core/src/core/usb/drivers/cdc.zig | 56 ++++++++++++++++++++----- core/src/core/usb/drivers/example.zig | 2 +- core/src/core/usb/drivers/hid.zig | 2 +- port/raspberrypi/rp2xxx/src/hal/usb.zig | 3 +- 6 files changed, 81 insertions(+), 17 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 74efc30ae..e7d318c43 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -385,11 +385,14 @@ pub fn DeviceController(config: Config) type { } fn process_interface_setup(self: *@This(), setup: *const types.SetupPacket) ?[]const u8 { + if (setup.request_type.type != .Class) + log.warn("Non-class ({t}) interface request", .{setup.request_type.type}); + const itf_num: u8 = @truncate(setup.index.into()); switch (itf_num) { inline else => |itf| if (comptime itf < handlers.itf.len) { const drv = handlers.itf[itf]; - return @field(self.driver_data.?, @tagName(drv)).interface_setup(setup); + return @field(self.driver_data.?, @tagName(drv)).class_request(setup); } else { log.warn("Interface index ({}) out of range ({})", .{ itf_num, handlers.itf.len }); return nak; diff --git a/core/src/core/usb/descriptor/cdc.zig b/core/src/core/usb/descriptor/cdc.zig index bf6478f59..780fa3eb3 100644 --- a/core/src/core/usb/descriptor/cdc.zig +++ b/core/src/core/usb/descriptor/cdc.zig @@ -26,6 +26,17 @@ pub const Header = extern struct { }; pub const CallManagement = extern struct { + pub const Capabilities = packed struct(u8) { + handles_call_mgmt: bool, + call_mgmt_over_data: bool, + reserved: u6 = 0, + + pub const none: @This() = .{ + .handles_call_mgmt = false, + .call_mgmt_over_data = false, + }; + }; + comptime { assert(@alignOf(@This()) == 1); assert(@sizeOf(@This()) == 5); @@ -37,12 +48,27 @@ pub const CallManagement = extern struct { // Subtype of this descriptor, must be `CallManagement`. descriptor_subtype: SubType = .CallManagement, // Capabilities. Should be 0x00 for use as a serial device. - capabilities: u8, + capabilities: Capabilities, // Data interface number. data_interface: u8, }; pub const AbstractControlModel = extern struct { + pub const Capabilities = packed struct(u8) { + /// Device supports the request combination of Set_Comm_Feature, + /// Clear_Comm_Feature, and Get_Comm_Feature + comm_feature: bool, + /// Device supports the request combination of + /// Set_Line_Coding, Set_Control_Line_State, Get_Line_Coding, + /// and the notification Serial_State + line_coding: bool, + /// Device supports the request Send_Break + send_break: bool, + /// Device supports the notification Network_Connection + network_connection: bool, + reserved1: u4 = 0, + }; + comptime { assert(@alignOf(@This()) == 1); assert(@sizeOf(@This()) == 4); @@ -54,7 +80,7 @@ pub const AbstractControlModel = extern struct { // Subtype of this descriptor, must be `AbstractControlModel`. descriptor_subtype: SubType = .AbstractControlModel, // Capabilities. Should be 0x02 for use as a serial device. - capabilities: u8, + capabilities: Capabilities, }; pub const Union = extern struct { diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 934419782..484923122 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -4,17 +4,43 @@ const assert = std.debug.assert; const log = std.log.scoped(.usb_cdc); pub const ManagementRequestType = enum(u8) { + SetCommFeature = 0x02, + GetCommFeature = 0x03, + ClearCommFeature = 0x04, + SetAuxLineState = 0x10, + SetHookState = 0x11, + PulseSetup = 0x12, + SendPulse = 0x13, + SetPulseTime = 0x14, + RingAuxJack = 0x15, SetLineCoding = 0x20, GetLineCoding = 0x21, SetControlLineState = 0x22, SendBreak = 0x23, + SetRingerParams = 0x30, + GetRingerParams = 0x31, + SetOperationParams = 0x32, + GetOperationParams = 0x33, + SetLineParams = 0x34, + GetLineParams = 0x35, + DialDigits = 0x36, _, }; pub const LineCoding = extern struct { - bit_rate: u32 align(1), - stop_bits: u8, - parity: u8, + pub const StopBits = enum(u8) { @"1" = 0, @"1.5" = 1, @"2" = 2, _ }; + pub const Parity = enum(u8) { + none = 0, + odd = 1, + even = 2, + mark = 3, + space = 4, + _, + }; + + bit_rate: usb.types.U32_Le, + stop_bits: StopBits, + parity: Parity, data_bits: u8, pub const init: @This() = .{ @@ -68,15 +94,23 @@ pub fn CdcClassDriver(options: Options) type { }, .cdc_header = .{ .bcd_cdc = .from(0x0120) }, .cdc_call_mgmt = .{ - .capabilities = 0, + .capabilities = .none, .data_interface = itf_data, }, - .cdc_acm = .{ .capabilities = 6 }, + .cdc_acm = .{ + .capabilities = .{ + .comm_feature = false, + .send_break = false, + // Line coding requests get sent regardless of this bit + .line_coding = true, + .network_connection = false, + }, + }, .cdc_union = .{ .master_interface = itf_notifi, .slave_interface_0 = itf_data, }, - .ep_notifi = .interrupt(alloc.next_ep(.In), 8, 16), + .ep_notifi = .interrupt(alloc.next_ep(.In), 16, 16), .itf_data = .{ .interface_number = itf_data, .alternate_setting = 0, @@ -171,9 +205,9 @@ pub fn CdcClassDriver(options: Options) type { .device = device, .ep_notifi = desc.ep_notifi.endpoint.num, .line_coding = .{ - .bit_rate = 115200, - .stop_bits = 0, - .parity = 0, + .bit_rate = .from(115200), + .stop_bits = .@"1", + .parity = .none, .data_bits = 8, }, @@ -189,9 +223,9 @@ pub fn CdcClassDriver(options: Options) type { device.ep_listen(desc.ep_out.endpoint.num, options.max_packet_size); } - pub fn interface_setup(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { + pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { const mgmt_request: ManagementRequestType = @enumFromInt(setup.request); - log.debug("cdc setup: {t}", .{mgmt_request}); + log.debug("cdc setup: {t} {} {}", .{ mgmt_request, setup.length.into(), setup.value.into() }); return switch (mgmt_request) { .SetLineCoding => usb.ack, // we should handle data phase somehow to read sent line_coding diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index 156f3ba41..70bf49bf3 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -59,7 +59,7 @@ pub const EchoExampleDriver = struct { /// Used for interface configuration through endpoint 0. /// Data returned by this function is sent on endpoint 0. - pub fn interface_setup(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { + pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { _ = self; _ = setup; return usb.ack; diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index 63529c42d..97d1e8079 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -64,7 +64,7 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { }; } - pub fn interface_setup(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { + pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { _ = self; switch (setup.request_type.type) { .Standard => { diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 4033f7484..6d7ccb02d 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -236,7 +236,7 @@ pub fn Polled(config: Config) type { peripherals.USB.SIE_CTRL.modify(.{ .PULLUP_EN = 1 }); // Listen for ACKs - self.interface.ep_listen(.ep0, 0); + self.interface.ep_listen(.ep0, max_supported_packet_size); return self; } @@ -410,6 +410,7 @@ pub fn Polled(config: Config) type { .BUFFER_ADDRESS = rp2xxx_buffers.data_offset(ep_hard.data_buffer), }); } + @memset(ep_hard.data_buffer, 0); } fn endpoint_alloc(self: *@This(), desc: *const usb.descriptor.Endpoint) ![]align(64) u8 { From 3f494d030a560ffcd63944da23f582b40a7f1e79 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sun, 18 Jan 2026 14:19:30 +0100 Subject: [PATCH 33/38] cleanup --- core/src/core/usb/descriptor.zig | 8 ++-- core/src/core/usb/descriptor/cdc.zig | 2 +- core/src/core/usb/descriptor/hid.zig | 2 + core/src/core/usb/drivers/cdc.zig | 55 ++++++++++++++------------- core/src/core/usb/drivers/example.zig | 21 +++++----- core/src/core/usb/drivers/hid.zig | 30 ++++++++------- 6 files changed, 64 insertions(+), 54 deletions(-) diff --git a/core/src/core/usb/descriptor.zig b/core/src/core/usb/descriptor.zig index 8baada416..3d6109a1a 100644 --- a/core/src/core/usb/descriptor.zig +++ b/core/src/core/usb/descriptor.zig @@ -152,13 +152,13 @@ pub const String = struct { data: []const u8, - pub fn from_lang(lang: Language) @This() { + pub fn from_lang(comptime lang: Language) @This() { const ret: *const extern struct { length: u8 = @sizeOf(@This()), descriptor_type: Type = .String, lang: types.U16_Le, } = comptime &.{ .lang = .from(@intFromEnum(lang)) }; - return .{ .data = @ptrCast(ret) }; + return .{ .data = std.mem.asBytes(ret) }; } pub fn from_str(comptime string: []const u8) @This() { @@ -294,9 +294,9 @@ pub const BOS = struct { data: []const u8, - pub fn from(objects: []const Object) @This() { + pub fn from(comptime objects: []const Object) @This() { const data: []const u8 = ""; - const header: []const u8 = @ptrCast(&extern struct { + const header: []const u8 = std.mem.asBytes(&extern struct { length: u8 = @sizeOf(@This()), descriptor_type: Type = .BOS, total_length: types.U16_Le = .from(@sizeOf(@This()) + data.len), diff --git a/core/src/core/usb/descriptor/cdc.zig b/core/src/core/usb/descriptor/cdc.zig index 780fa3eb3..1230b29cd 100644 --- a/core/src/core/usb/descriptor/cdc.zig +++ b/core/src/core/usb/descriptor/cdc.zig @@ -66,7 +66,7 @@ pub const AbstractControlModel = extern struct { send_break: bool, /// Device supports the notification Network_Connection network_connection: bool, - reserved1: u4 = 0, + reserved: u4 = 0, }; comptime { diff --git a/core/src/core/usb/descriptor/hid.zig b/core/src/core/usb/descriptor/hid.zig index cb5995e1f..a7a8ae1a0 100644 --- a/core/src/core/usb/descriptor/hid.zig +++ b/core/src/core/usb/descriptor/hid.zig @@ -11,6 +11,7 @@ pub const RequestType = enum(u8) { SetReport = 0x09, SetIdle = 0x0a, SetProtocol = 0x0b, + _, }; /// USB HID descriptor @@ -59,6 +60,7 @@ pub const HID = extern struct { HID = 0x21, Report = 0x22, Physical = 0x23, + _, }; comptime { diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 484923122..b3060dfd2 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -1,6 +1,7 @@ const std = @import("std"); const usb = @import("../../usb.zig"); const assert = std.debug.assert; +const EpNum = usb.types.Endpoint.Num; const log = std.log.scoped(.usb_cdc); pub const ManagementRequestType = enum(u8) { @@ -58,16 +59,18 @@ pub const Options = struct { pub fn CdcClassDriver(options: Options) type { return struct { pub const Descriptor = extern struct { - itf_assoc: usb.descriptor.InterfaceAssociation, - itf_notifi: usb.descriptor.Interface, - cdc_header: usb.descriptor.cdc.Header, - cdc_call_mgmt: usb.descriptor.cdc.CallManagement, - cdc_acm: usb.descriptor.cdc.AbstractControlModel, - cdc_union: usb.descriptor.cdc.Union, - ep_notifi: usb.descriptor.Endpoint, - itf_data: usb.descriptor.Interface, - ep_out: usb.descriptor.Endpoint, - ep_in: usb.descriptor.Endpoint, + const desc = usb.descriptor; + + itf_assoc: desc.InterfaceAssociation, + itf_notifi: desc.Interface, + cdc_header: desc.cdc.Header, + cdc_call_mgmt: desc.cdc.CallManagement, + cdc_acm: desc.cdc.AbstractControlModel, + cdc_union: desc.cdc.Union, + ep_notifi: desc.Endpoint, + itf_data: desc.Interface, + ep_out: desc.Endpoint, + ep_in: desc.Endpoint, pub fn create( alloc: *usb.DescriptorAllocator, @@ -131,19 +134,19 @@ pub fn CdcClassDriver(options: Options) type { }; device: *usb.DeviceInterface, - ep_notifi: usb.types.Endpoint.Num, + ep_notifi: EpNum, line_coding: LineCoding align(4), /// OUT endpoint on which there is data ready to be read, /// or .ep0 when no data is available. - ep_out: usb.types.Endpoint.Num, + ep_out: EpNum, rx_data: [options.max_packet_size]u8, rx_seek: usb.types.Len, rx_end: usb.types.Len, /// IN endpoint where data can be sent, /// or .ep0 when data is being sent. - ep_in: usb.types.Endpoint.Num, + ep_in: EpNum, tx_data: [options.max_packet_size]u8, tx_end: usb.types.Len, @@ -159,11 +162,11 @@ pub fn CdcClassDriver(options: Options) type { if (self.available() > 0) return len; // request more data - const ep_out = @atomicLoad(usb.types.Endpoint.Num, &self.ep_out, .seq_cst); + const ep_out = @atomicLoad(EpNum, &self.ep_out, .seq_cst); if (ep_out != .ep0) { self.rx_end = self.device.ep_readv(ep_out, &.{&self.rx_data}); self.rx_seek = 0; - @atomicStore(usb.types.Endpoint.Num, &self.ep_out, .ep0, .seq_cst); + @atomicStore(EpNum, &self.ep_out, .ep0, .seq_cst); self.device.ep_listen(ep_out, options.max_packet_size); } @@ -189,11 +192,11 @@ pub fn CdcClassDriver(options: Options) type { if (self.tx_end == 0) return true; - const ep_in = @atomicLoad(usb.types.Endpoint.Num, &self.ep_in, .seq_cst); + const ep_in = @atomicLoad(EpNum, &self.ep_in, .seq_cst); if (ep_in == .ep0) return false; - @atomicStore(usb.types.Endpoint.Num, &self.ep_in, .ep0, .seq_cst); + @atomicStore(EpNum, &self.ep_in, .ep0, .seq_cst); assert(self.tx_end == self.device.ep_writev(ep_in, &.{self.tx_data[0..self.tx_end]})); self.tx_end = 0; @@ -241,19 +244,19 @@ pub fn CdcClassDriver(options: Options) type { }; } - pub fn on_rx(self: *@This(), ep_num: usb.types.Endpoint.Num) void { - assert(.ep0 == @atomicLoad(usb.types.Endpoint.Num, &self.ep_out, .seq_cst)); - @atomicStore(usb.types.Endpoint.Num, &self.ep_out, ep_num, .seq_cst); + pub fn on_rx(self: *@This(), ep_num: EpNum) void { + assert(.ep0 == @atomicLoad(EpNum, &self.ep_out, .seq_cst)); + @atomicStore(EpNum, &self.ep_out, ep_num, .seq_cst); } - pub fn on_tx_ready(self: *@This(), ep_num: usb.types.Endpoint.Num) void { - assert(.ep0 == @atomicLoad(usb.types.Endpoint.Num, &self.ep_in, .seq_cst)); - @atomicStore(usb.types.Endpoint.Num, &self.ep_in, ep_num, .seq_cst); + pub fn on_tx_ready(self: *@This(), ep_num: EpNum) void { + assert(.ep0 == @atomicLoad(EpNum, &self.ep_in, .seq_cst)); + @atomicStore(EpNum, &self.ep_in, ep_num, .seq_cst); } - pub fn on_notifi_ready(self: *@This(), ep_num: usb.types.Endpoint.Num) void { - assert(.ep0 == @atomicLoad(usb.types.Endpoint.Num, &self.ep_notifi, .seq_cst)); - @atomicStore(usb.types.Endpoint.Num, &self.ep_notifi, ep_num, .seq_cst); + pub fn on_notifi_ready(self: *@This(), ep_num: EpNum) void { + assert(.ep0 == @atomicLoad(EpNum, &self.ep_notifi, .seq_cst)); + @atomicStore(EpNum, &self.ep_notifi, ep_num, .seq_cst); } }; } diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index 70bf49bf3..a8caa8054 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -1,14 +1,17 @@ const std = @import("std"); const usb = @import("../../usb.zig"); +const EpNum = usb.types.Endpoint.Num; const log = std.log.scoped(.usb_echo); /// This is an example driver that sends any data received on ep2 to ep1. pub const EchoExampleDriver = struct { /// The descriptors need to have the same memory layout as the sent data. pub const Descriptor = extern struct { - example_interface: usb.descriptor.Interface, - ep_out: usb.descriptor.Endpoint, - ep_in: usb.descriptor.Endpoint, + const desc = usb.descriptor; + + example_interface: desc.Interface, + ep_out: desc.Endpoint, + ep_in: desc.Endpoint, /// This function is used during descriptor creation. If multiple instances /// of a driver are used, a descriptor will be created for each. @@ -42,7 +45,7 @@ pub const EchoExampleDriver = struct { }; device: *usb.DeviceInterface, - ep_tx: usb.types.Endpoint.Num, + ep_tx: EpNum, /// This function is called when the host chooses a configuration /// that contains this driver. `self` points to undefined memory. @@ -68,22 +71,22 @@ pub const EchoExampleDriver = struct { /// Each endpoint (as defined in the descriptor) has its own handler. /// Endpoint number is passed as an argument so that it does not need /// to be stored in the driver. - pub fn on_tx_ready(self: *@This(), ep_tx: usb.types.Endpoint.Num) void { + pub fn on_tx_ready(self: *@This(), ep_tx: EpNum) void { log.info("tx ready", .{}); // Mark transmission as available - @atomicStore(usb.types.Endpoint.Num, &self.ep_tx, ep_tx, .seq_cst); + @atomicStore(EpNum, &self.ep_tx, ep_tx, .seq_cst); } - pub fn on_rx(self: *@This(), ep_rx: usb.types.Endpoint.Num) void { + pub fn on_rx(self: *@This(), ep_rx: EpNum) void { var buf: [64]u8 = undefined; // Read incoming packet into a local buffer const len_rx = self.device.ep_readv(ep_rx, &.{&buf}); log.info("Received: {s}", .{buf[0..len_rx]}); // Check if we can transmit - const ep_tx = @atomicLoad(usb.types.Endpoint.Num, &self.ep_tx, .seq_cst); + const ep_tx = @atomicLoad(EpNum, &self.ep_tx, .seq_cst); if (ep_tx != .ep0) { // Mark transmission as not available - @atomicStore(usb.types.Endpoint.Num, &self.ep_tx, .ep0, .seq_cst); + @atomicStore(EpNum, &self.ep_tx, .ep0, .seq_cst); // Send received packet log.info("Sending {} bytes", .{len_rx}); const len_tx = self.device.ep_writev(ep_tx, &.{buf[0..len_rx]}); diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index 97d1e8079..922eac6ea 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -1,6 +1,6 @@ const std = @import("std"); const usb = @import("../../usb.zig"); -const assert = std.debug.assert; +const EpNum = usb.types.Endpoint.Num; const log = std.log.scoped(.usb_hid); pub const Options = struct { @@ -11,10 +11,12 @@ pub const Options = struct { pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { return struct { pub const Descriptor = extern struct { - interface: usb.descriptor.Interface, - hid: usb.descriptor.hid.HID, - ep_out: usb.descriptor.Endpoint, - ep_in: usb.descriptor.Endpoint, + const desc = usb.descriptor; + + interface: desc.Interface, + hid: desc.hid.HID, + ep_out: desc.Endpoint, + ep_in: desc.Endpoint, pub fn create( alloc: *usb.DescriptorAllocator, @@ -53,8 +55,8 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { }; device: *usb.DeviceInterface, - ep_in: usb.types.Endpoint.Num, - ep_out: usb.types.Endpoint.Num, + ep_in: EpNum, + ep_out: EpNum, pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { self.* = .{ @@ -68,16 +70,16 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { _ = self; switch (setup.request_type.type) { .Standard => { - const hid_desc_type = std.meta.intToEnum(usb.descriptor.hid.HID.Type, setup.value.into() >> 8) catch return usb.nak; - const request_code = std.meta.intToEnum(usb.types.SetupRequest, setup.request) catch return usb.nak; + const hid_desc_type: usb.descriptor.hid.HID.Type = @enumFromInt(setup.value.into() >> 8); + const request_code: usb.types.SetupRequest = @enumFromInt(setup.request); if (request_code == .GetDescriptor and hid_desc_type == .HID) - return @as([]const u8, @ptrCast(&hid_descriptor)) + return std.mem.asBytes(&hid_descriptor) else if (request_code == .GetDescriptor and hid_desc_type == .Report) - return @as([]const u8, @ptrCast(&report_descriptor)); + return std.mem.asBytes(&report_descriptor); }, .Class => { - const hid_request_type = std.meta.intToEnum(usb.descriptor.hid.RequestType, setup.request) catch return usb.nak; + const hid_request_type: usb.descriptor.hid.RequestType = @enumFromInt(setup.request); switch (hid_request_type) { .SetIdle => { // TODO: https://github.com/ZigEmbeddedGroup/microzig/issues/454 @@ -118,12 +120,12 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { return usb.nak; } - pub fn on_tx_ready(self: *@This(), ep: usb.types.Endpoint.Num) void { + pub fn on_tx_ready(self: *@This(), ep: EpNum) void { _ = self; _ = ep; } - pub fn on_rx(self: *@This(), ep: usb.types.Endpoint.Num) void { + pub fn on_rx(self: *@This(), ep: EpNum) void { _ = self; _ = ep; } From 6474eab39fbace45a2646a71c7c4965d70512e12 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sun, 18 Jan 2026 14:31:36 +0100 Subject: [PATCH 34/38] EpNum -> EP_Num and fix pins in examples --- core/src/core/usb/descriptor/hid.zig | 1 + core/src/core/usb/drivers/cdc.zig | 34 ++++++++++----------- core/src/core/usb/drivers/example.zig | 14 ++++----- core/src/core/usb/drivers/hid.zig | 10 +++--- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 4 +-- examples/raspberrypi/rp2xxx/src/usb_hid.zig | 4 +-- 6 files changed, 32 insertions(+), 35 deletions(-) diff --git a/core/src/core/usb/descriptor/hid.zig b/core/src/core/usb/descriptor/hid.zig index a7a8ae1a0..641a715e5 100644 --- a/core/src/core/usb/descriptor/hid.zig +++ b/core/src/core/usb/descriptor/hid.zig @@ -54,6 +54,7 @@ pub const HID = extern struct { Us, Yugoslavia, TurkishF, + _, }; pub const Type = enum(u8) { diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index b3060dfd2..6e4b9bea9 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -1,7 +1,7 @@ const std = @import("std"); const usb = @import("../../usb.zig"); const assert = std.debug.assert; -const EpNum = usb.types.Endpoint.Num; +const EP_Num = usb.types.Endpoint.Num; const log = std.log.scoped(.usb_cdc); pub const ManagementRequestType = enum(u8) { @@ -134,19 +134,19 @@ pub fn CdcClassDriver(options: Options) type { }; device: *usb.DeviceInterface, - ep_notifi: EpNum, + ep_notifi: EP_Num, line_coding: LineCoding align(4), /// OUT endpoint on which there is data ready to be read, /// or .ep0 when no data is available. - ep_out: EpNum, + ep_out: EP_Num, rx_data: [options.max_packet_size]u8, rx_seek: usb.types.Len, rx_end: usb.types.Len, /// IN endpoint where data can be sent, /// or .ep0 when data is being sent. - ep_in: EpNum, + ep_in: EP_Num, tx_data: [options.max_packet_size]u8, tx_end: usb.types.Len, @@ -162,11 +162,11 @@ pub fn CdcClassDriver(options: Options) type { if (self.available() > 0) return len; // request more data - const ep_out = @atomicLoad(EpNum, &self.ep_out, .seq_cst); + const ep_out = @atomicLoad(EP_Num, &self.ep_out, .seq_cst); if (ep_out != .ep0) { self.rx_end = self.device.ep_readv(ep_out, &.{&self.rx_data}); self.rx_seek = 0; - @atomicStore(EpNum, &self.ep_out, .ep0, .seq_cst); + @atomicStore(EP_Num, &self.ep_out, .ep0, .seq_cst); self.device.ep_listen(ep_out, options.max_packet_size); } @@ -192,11 +192,11 @@ pub fn CdcClassDriver(options: Options) type { if (self.tx_end == 0) return true; - const ep_in = @atomicLoad(EpNum, &self.ep_in, .seq_cst); + const ep_in = @atomicLoad(EP_Num, &self.ep_in, .seq_cst); if (ep_in == .ep0) return false; - @atomicStore(EpNum, &self.ep_in, .ep0, .seq_cst); + @atomicStore(EP_Num, &self.ep_in, .ep0, .seq_cst); assert(self.tx_end == self.device.ep_writev(ep_in, &.{self.tx_data[0..self.tx_end]})); self.tx_end = 0; @@ -244,19 +244,19 @@ pub fn CdcClassDriver(options: Options) type { }; } - pub fn on_rx(self: *@This(), ep_num: EpNum) void { - assert(.ep0 == @atomicLoad(EpNum, &self.ep_out, .seq_cst)); - @atomicStore(EpNum, &self.ep_out, ep_num, .seq_cst); + pub fn on_rx(self: *@This(), ep_num: EP_Num) void { + assert(.ep0 == @atomicLoad(EP_Num, &self.ep_out, .seq_cst)); + @atomicStore(EP_Num, &self.ep_out, ep_num, .seq_cst); } - pub fn on_tx_ready(self: *@This(), ep_num: EpNum) void { - assert(.ep0 == @atomicLoad(EpNum, &self.ep_in, .seq_cst)); - @atomicStore(EpNum, &self.ep_in, ep_num, .seq_cst); + pub fn on_tx_ready(self: *@This(), ep_num: EP_Num) void { + assert(.ep0 == @atomicLoad(EP_Num, &self.ep_in, .seq_cst)); + @atomicStore(EP_Num, &self.ep_in, ep_num, .seq_cst); } - pub fn on_notifi_ready(self: *@This(), ep_num: EpNum) void { - assert(.ep0 == @atomicLoad(EpNum, &self.ep_notifi, .seq_cst)); - @atomicStore(EpNum, &self.ep_notifi, ep_num, .seq_cst); + pub fn on_notifi_ready(self: *@This(), ep_num: EP_Num) void { + assert(.ep0 == @atomicLoad(EP_Num, &self.ep_notifi, .seq_cst)); + @atomicStore(EP_Num, &self.ep_notifi, ep_num, .seq_cst); } }; } diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index a8caa8054..59cc59163 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -1,6 +1,6 @@ const std = @import("std"); const usb = @import("../../usb.zig"); -const EpNum = usb.types.Endpoint.Num; +const EP_Num = usb.types.Endpoint.Num; const log = std.log.scoped(.usb_echo); /// This is an example driver that sends any data received on ep2 to ep1. @@ -45,7 +45,7 @@ pub const EchoExampleDriver = struct { }; device: *usb.DeviceInterface, - ep_tx: EpNum, + ep_tx: EP_Num, /// This function is called when the host chooses a configuration /// that contains this driver. `self` points to undefined memory. @@ -71,22 +71,22 @@ pub const EchoExampleDriver = struct { /// Each endpoint (as defined in the descriptor) has its own handler. /// Endpoint number is passed as an argument so that it does not need /// to be stored in the driver. - pub fn on_tx_ready(self: *@This(), ep_tx: EpNum) void { + pub fn on_tx_ready(self: *@This(), ep_tx: EP_Num) void { log.info("tx ready", .{}); // Mark transmission as available - @atomicStore(EpNum, &self.ep_tx, ep_tx, .seq_cst); + @atomicStore(EP_Num, &self.ep_tx, ep_tx, .seq_cst); } - pub fn on_rx(self: *@This(), ep_rx: EpNum) void { + pub fn on_rx(self: *@This(), ep_rx: EP_Num) void { var buf: [64]u8 = undefined; // Read incoming packet into a local buffer const len_rx = self.device.ep_readv(ep_rx, &.{&buf}); log.info("Received: {s}", .{buf[0..len_rx]}); // Check if we can transmit - const ep_tx = @atomicLoad(EpNum, &self.ep_tx, .seq_cst); + const ep_tx = @atomicLoad(EP_Num, &self.ep_tx, .seq_cst); if (ep_tx != .ep0) { // Mark transmission as not available - @atomicStore(EpNum, &self.ep_tx, .ep0, .seq_cst); + @atomicStore(EP_Num, &self.ep_tx, .ep0, .seq_cst); // Send received packet log.info("Sending {} bytes", .{len_rx}); const len_tx = self.device.ep_writev(ep_tx, &.{buf[0..len_rx]}); diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index 922eac6ea..c8ce11c5b 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -1,6 +1,6 @@ const std = @import("std"); const usb = @import("../../usb.zig"); -const EpNum = usb.types.Endpoint.Num; +const EP_Num = usb.types.Endpoint.Num; const log = std.log.scoped(.usb_hid); pub const Options = struct { @@ -55,8 +55,8 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { }; device: *usb.DeviceInterface, - ep_in: EpNum, - ep_out: EpNum, + ep_in: EP_Num, + ep_out: EP_Num, pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { self.* = .{ @@ -120,12 +120,12 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { return usb.nak; } - pub fn on_tx_ready(self: *@This(), ep: EpNum) void { + pub fn on_tx_ready(self: *@This(), ep: EP_Num) void { _ = self; _ = ep; } - pub fn on_rx(self: *@This(), ep: EpNum) void { + pub fn on_rx(self: *@This(), ep: EP_Num) void { _ = self; _ = ep; } diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 30949bc32..81018ab47 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -61,10 +61,8 @@ const pin_config: rp2xxx.pins.GlobalConfiguration = .{ .GPIO25 = .{ .name = "led", .direction = .out }, }; -const pins = pin_config.pins(); - pub fn main() !void { - pin_config.apply(); + const pins = pin_config.apply(); const uart = rp2xxx.uart.instance.num(0); uart.apply(.{ diff --git a/examples/raspberrypi/rp2xxx/src/usb_hid.zig b/examples/raspberrypi/rp2xxx/src/usb_hid.zig index 2105a9b34..03fd4548d 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -64,10 +64,8 @@ const pin_config: rp2xxx.pins.GlobalConfiguration = .{ .GPIO25 = .{ .name = "led", .direction = .out }, }; -const pins = pin_config.pins(); - pub fn main() !void { - pin_config.apply(); + const pins = pin_config.apply(); const uart = rp2xxx.uart.instance.num(0); uart.apply(.{ From 6f6ba02a5db50e38c25c9e17c83cd848d321cd6e Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sun, 18 Jan 2026 22:12:01 +0100 Subject: [PATCH 35/38] implement GetStatus --- core/src/core/usb.zig | 16 +++++++++------- core/src/core/usb/drivers/cdc.zig | 2 +- core/src/core/usb/types.zig | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index e7d318c43..e76298d60 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -356,8 +356,13 @@ pub fn DeviceController(config: Config) type { switch (setup.request_type.type) { .Standard => { const request: types.SetupRequest = @enumFromInt(setup.request); - log.debug("Device setup: {t}", .{request}); + log.debug("Device setup: {any}", .{request}); switch (request) { + .GetStatus => { + const attr = config_descriptor.__configuration_descriptor.attributes; + const status: types.DeviceStatus = comptime .create(attr.self_powered, false); + return std.mem.asBytes(&status); + }, .SetAddress => self.new_address = @truncate(setup.value.into()), .SetConfiguration => self.process_set_config(device_itf, setup.value.into()), .GetDescriptor => return get_descriptor(setup.value.into()), @@ -370,7 +375,7 @@ pub fn DeviceController(config: Config) type { else => return nak, } }, - _ => { + else => { log.warn("Unsupported standard request: {}", .{setup.request}); return nak; }, @@ -378,16 +383,13 @@ pub fn DeviceController(config: Config) type { return ack; }, else => |t| { - log.warn("Unhandled device setup request: {t}", .{t}); + log.warn("Unhandled device setup request: {any}", .{t}); return nak; }, } } fn process_interface_setup(self: *@This(), setup: *const types.SetupPacket) ?[]const u8 { - if (setup.request_type.type != .Class) - log.warn("Non-class ({t}) interface request", .{setup.request_type.type}); - const itf_num: u8 = @truncate(setup.index.into()); switch (itf_num) { inline else => |itf| if (comptime itf < handlers.itf.len) { @@ -404,7 +406,7 @@ pub fn DeviceController(config: Config) type { const asBytes = std.mem.asBytes; const desc_type: descriptor.Type = @enumFromInt(value >> 8); const desc_idx: u8 = @truncate(value); - log.debug("Request for {t} descriptor {}", .{ desc_type, desc_idx }); + log.debug("Request for {any} descriptor {}", .{ desc_type, desc_idx }); return switch (desc_type) { .Device => asBytes(&config.device_descriptor), .DeviceQualifier => asBytes(comptime &config.device_descriptor.qualifier()), diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 6e4b9bea9..ecbc70a05 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -228,7 +228,7 @@ pub fn CdcClassDriver(options: Options) type { pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { const mgmt_request: ManagementRequestType = @enumFromInt(setup.request); - log.debug("cdc setup: {t} {} {}", .{ mgmt_request, setup.length.into(), setup.value.into() }); + log.debug("cdc setup: {any} {} {}", .{ mgmt_request, setup.length.into(), setup.value.into() }); return switch (mgmt_request) { .SetLineCoding => usb.ack, // we should handle data phase somehow to read sent line_coding diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index 7a53ede4a..bfcd04ba0 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -293,13 +293,32 @@ pub const TransferType = enum(u2) { /// The types of USB SETUP requests that we understand. pub const SetupRequest = enum(u8) { + GetStatus = 0x00, + ClearFeature = 0x02, SetFeature = 0x03, SetAddress = 0x05, GetDescriptor = 0x06, + SetDescriptor = 0x07, + GetConfiguration = 0x08, SetConfiguration = 0x09, _, }; +pub const DeviceStatus = extern struct { + const Flags = packed struct(u8) { + self_powered: bool, + remote_wakeup: bool, + reserved: u6 = 0, + }; + + flags: Flags, + reserved: u8 = 0, + + pub fn create(self_powered: bool, remote_wakeup: bool) @This() { + return .{ .flags = .{ .self_powered = self_powered, .remote_wakeup = remote_wakeup } }; + } +}; + pub const FeatureSelector = enum(u8) { EndpointHalt = 0x00, DeviceRemoteWakeup = 0x01, From 872e79466c92e4aa179447890e4ba95021ef49c7 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Mon, 19 Jan 2026 01:05:19 +0100 Subject: [PATCH 36/38] driver handler type safety --- core/src/core/usb.zig | 110 ++++++++++++++++++++------ core/src/core/usb/drivers/cdc.zig | 8 +- core/src/core/usb/drivers/example.zig | 6 +- core/src/core/usb/drivers/hid.zig | 6 +- 4 files changed, 95 insertions(+), 35 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index e76298d60..b4ff02188 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -91,6 +91,30 @@ pub const DeviceInterface = struct { } }; +pub fn DriverHadlers(Driver: type) type { + const EndpointHandler = fn (*Driver, types.Endpoint.Num) void; + + const Field = std.builtin.Type.StructField; + var ret_fields: []const Field = &.{}; + const desc_fields = @typeInfo(Driver.Descriptor).@"struct".fields; + for (desc_fields) |fld| switch (fld.type) { + descriptor.Endpoint => ret_fields = ret_fields ++ &[1]Field{.{ + .alignment = @alignOf(usize), + .default_value_ptr = null, + .is_comptime = false, + .name = fld.name, + .type = EndpointHandler, + }}, + else => {}, + }; + return @Type(.{ .@"struct" = .{ + .decls = &.{}, + .fields = ret_fields, + .is_tuple = false, + .layout = .auto, + } }); +} + pub const Config = struct { pub const Configuration = struct { num: u8, @@ -208,17 +232,32 @@ pub fn DeviceController(config: Config) type { }; const handlers = blk: { - const Handler = struct { - driver: []const u8, - function: []const u8, - }; + @setEvalBranchQuota(10000); + + const Field = std.builtin.Type.StructField; + var drivers_ep: struct { + In: [16][]const u8 = @splat(""), + Out: [16][]const u8 = @splat(""), + } = .{}; + var handlers_ep: struct { + const default_field: Field = .{ + .alignment = 1, + .default_value_ptr = null, + .is_comptime = false, + .name = "", + .type = void, + }; + In: [16]Field = @splat(default_field), + Out: [16]Field = @splat(default_field), + } = .{}; + var itf_handlers: []const DriverEnum = &.{}; + + for (0..16) |i| { + const name = std.fmt.comptimePrint("{}", .{i}); + handlers_ep.In[i].name = name; + handlers_ep.Out[i].name = name; + } - var ret: struct { In: [16]Handler, Out: [16]Handler, itf: []const DriverEnum } = .{ - .In = @splat(.{ .driver = "", .function = "" }), - .Out = @splat(.{ .driver = "", .function = "" }), - .itf = &.{}, - }; - var itf_handlers = ret.itf; for (driver_fields) |fld_drv| { const cfg = @field(config_descriptor, fld_drv.name); const fields = @typeInfo(@TypeOf(cfg)).@"struct".fields; @@ -237,22 +276,43 @@ pub fn DeviceController(config: Config) type { for (fields) |fld| { if (fld.type != descriptor.Endpoint) continue; const desc: descriptor.Endpoint = @field(cfg, fld.name); + const tag = @tagName(desc.endpoint.dir); const ep_num = @intFromEnum(desc.endpoint.num); - const handler = &@field(ret, @tagName(desc.endpoint.dir))[ep_num]; - const function = @field(fld_drv.type.handlers, fld.name); - if (handler.driver.len != 0 or handler.function.len != 0) + + const driver = &@field(drivers_ep, tag)[ep_num]; + if (driver.*.len != 0) @compileError(std.fmt.comptimePrint( - "ep{} {t}: multiple handlers: {s}.{s} and {s}.{s}", - .{ ep_num, desc.endpoint.dir, handler.driver, handler.function, fld_drv.name, function }, + "ep{} {t}: multiple handlers: {s} and {s}", + .{ ep_num, desc.endpoint.dir, driver.*, fld_drv.name }, )); - handler.* = .{ - .driver = fld_drv.name, - .function = function, - }; + + driver.* = fld_drv.name; + const func = @field(fld_drv.type.handlers, fld.name); + const handler = &@field(handlers_ep, tag)[ep_num]; + handler.alignment = @alignOf(@TypeOf(func)); + handler.default_value_ptr = &func; + handler.type = @TypeOf(func); + handler.is_comptime = true; } } - ret.itf = itf_handlers; - break :blk ret; + break :blk .{ + .ep_drv = drivers_ep, + .ep_han = .{ + .In = @Type(.{ .@"struct" = .{ + .decls = &.{}, + .fields = &handlers_ep.In, + .is_tuple = true, + .layout = .auto, + } }){}, + .Out = @Type(.{ .@"struct" = .{ + .decls = &.{}, + .fields = &handlers_ep.Out, + .is_tuple = true, + .layout = .auto, + } }){}, + }, + .itf = itf_handlers, + }; }; /// If not zero, change the device address at the next opportunity. @@ -305,7 +365,8 @@ pub fn DeviceController(config: Config) type { pub fn on_buffer(self: *@This(), device_itf: *DeviceInterface, comptime ep: types.Endpoint) void { log.debug("on_buffer {t} {t}", .{ ep.num, ep.dir }); - const handler = comptime @field(handlers, @tagName(ep.dir))[@intFromEnum(ep.num)]; + const driver = comptime @field(handlers.ep_drv, @tagName(ep.dir))[@intFromEnum(ep.num)]; + const function = comptime @field(handlers.ep_han, @tagName(ep.dir))[@intFromEnum(ep.num)]; if (comptime ep == types.Endpoint.in(.ep0)) { // We use this opportunity to finish the delayed @@ -334,9 +395,8 @@ pub fn DeviceController(config: Config) type { } else if (comptime ep == types.Endpoint.out(.ep0)) log.warn("Unhandled packet on ep0 Out", .{}); - if (comptime handler.driver.len != 0) { - const drv = &@field(self.driver_data.?, handler.driver); - @field(@FieldType(config0.Drivers, handler.driver), handler.function)(drv, ep.num); + if (comptime driver.len != 0) { + function(&@field(self.driver_data.?, driver), ep.num); } } diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index ecbc70a05..001a229b2 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -127,10 +127,10 @@ pub fn CdcClassDriver(options: Options) type { } }; - pub const handlers = .{ - .ep_notifi = "on_notifi_ready", - .ep_out = "on_rx", - .ep_in = "on_tx_ready", + pub const handlers: usb.DriverHadlers(@This()) = .{ + .ep_notifi = on_notifi_ready, + .ep_out = on_rx, + .ep_in = on_tx_ready, }; device: *usb.DeviceInterface, diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index 59cc59163..67cfd5dee 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -39,9 +39,9 @@ pub const EchoExampleDriver = struct { /// This is a mapping from endpoint descriptor field names to handler /// function names. Counterintuitively, usb devices send data on 'in' /// endpoints and receive on 'out' endpoints. - pub const handlers = .{ - .ep_in = "on_tx_ready", - .ep_out = "on_rx", + pub const handlers: usb.DriverHadlers(@This()) = .{ + .ep_in = on_tx_ready, + .ep_out = on_rx, }; device: *usb.DeviceInterface, diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index c8ce11c5b..8f6a0d734 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -49,9 +49,9 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { .report_length = .from(@sizeOf(@TypeOf(report_descriptor))), }; - pub const handlers = .{ - .ep_out = "on_rx", - .ep_in = "on_tx_ready", + pub const handlers: usb.DriverHadlers(@This()) = .{ + .ep_out = on_rx, + .ep_in = on_tx_ready, }; device: *usb.DeviceInterface, From aa8034d6a6a80946e7e359a8ae89fdcb97e5a04e Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Mon, 19 Jan 2026 17:55:40 +0100 Subject: [PATCH 37/38] inline get_setup_packet() --- port/raspberrypi/rp2xxx/src/hal/usb.zig | 33 +++++++------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 6d7ccb02d..b2bb29d7b 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -106,11 +106,17 @@ pub fn Polled(config: Config) type { // Setup request received? if (ints.SETUP_REQ != 0) { // Reset PID to 1 for EP0 IN. Every DATA packet we send in response - // to an IN on EP0 needs to use PID DATA1, and this line will ensure - // that. + // to an IN on EP0 needs to use PID DATA1. buffer_control[0].in.modify(.{ .PID_0 = 0 }); - const setup = get_setup_packet(); + // Clear the status flag (write-one-to-clear) + peripherals.USB.SIE_STATUS.modify(.{ .SETUP_REC = 1 }); + + // The PAC models this buffer as two 32-bit registers. + const setup: usb.types.SetupPacket = @bitCast([2]u32{ + peripherals.USB_DPRAM.SETUP_PACKET_LOW.raw, + peripherals.USB_DPRAM.SETUP_PACKET_HIGH.raw, + }); log.debug("setup {any}", .{setup}); controller.on_setup_req(&self.interface, &setup); @@ -349,27 +355,6 @@ pub fn Polled(config: Config) type { bufctrl_ptr.write(bufctrl); } - /// Returns a received USB setup packet - /// - /// Side effect: The setup request status flag will be cleared - /// - /// One can assume that this function is only called if the - /// setup request flag is set. - fn get_setup_packet() usb.types.SetupPacket { - // Clear the status flag (write-one-to-clear) - peripherals.USB.SIE_STATUS.modify(.{ .SETUP_REC = 1 }); - - // This assumes that the setup packet is arriving on EP0, our - // control endpoint. Which it should be. We don't have any other - // Control endpoints. - - // The PAC models this buffer as two 32-bit registers. - return @bitCast([2]u32{ - peripherals.USB_DPRAM.SETUP_PACKET_LOW.raw, - peripherals.USB_DPRAM.SETUP_PACKET_HIGH.raw, - }); - } - fn set_address(_: *usb.DeviceInterface, addr: u7) void { log.debug("set addr {}", .{addr}); From 7667ea7226bfe50cf87c162f514a15410cb608d3 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Mon, 19 Jan 2026 18:19:06 +0100 Subject: [PATCH 38/38] fix ep_writev only using the first buffer --- port/raspberrypi/rp2xxx/src/hal/usb.zig | 52 ++++++++++++++++--------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index b2bb29d7b..acd867145 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -263,20 +263,16 @@ pub fn Polled(config: Config) type { const bufctrl_ptr = &buffer_control[@intFromEnum(ep_num)].in; const ep = self.hardware_endpoint_get_by_address(.in(ep_num)); + var hw_buf: []align(1) u8 = ep.data_buffer; - const len = @min(data[0].len, ep.data_buffer.len); - switch (chip) { - .RP2040 => @memcpy(ep.data_buffer[0..len], data[0][0..len]), - .RP2350 => { - const dst: [*]align(4) u32 = @ptrCast(ep.data_buffer.ptr); - const src: [*]align(1) const u32 = @ptrCast(data[0].ptr); - for (0..len / 4) |i| - dst[i] = src[i]; - for (0..len % 4) |i| - ep.data_buffer[len - i - 1] = data[0][len - i - 1]; - }, + for (data) |src| { + const len = @min(src.len, hw_buf.len); + dpram_memcpy(hw_buf[0..len], src[0..len]); + hw_buf = hw_buf[len..]; } + const len: usb.types.Len = @intCast(ep.data_buffer.len - hw_buf.len); + var bufctrl = bufctrl_ptr.read(); assert(bufctrl.AVAILABLE_0 == 0); // Write the buffer information to the buffer control register @@ -296,32 +292,36 @@ pub fn Polled(config: Config) type { bufctrl.AVAILABLE_0 = 1; bufctrl_ptr.write(bufctrl); - return @intCast(len); + return len; } + /// Copies the last sent packet from USB SRAM into the provided buffer. + /// Slices in `data` must collectively be long enough to store the full packet. fn ep_readv( itf: *usb.DeviceInterface, ep_num: usb.types.Endpoint.Num, data: []const []u8, ) usb.types.Len { - log.debug("readv {t} {}: {any}", .{ ep_num, data[0].len, data[0] }); + var total_len: usize = data[0].len; + for (data[1..]) |d| total_len += d.len; + log.debug("readv {t} {}", .{ ep_num, total_len }); const self: *@This() = @fieldParentPtr("interface", itf); assert(data.len > 0); - const bufctrl = &buffer_control[@intFromEnum(ep_num)].out.read(); + const bufctrl = buffer_control[@intFromEnum(ep_num)].out.read(); const ep = self.hardware_endpoint_get_by_address(.out(ep_num)); var hw_buf: []align(1) u8 = ep.data_buffer[0..bufctrl.LENGTH_0]; for (data) |dst| { const len = @min(dst.len, hw_buf.len); - // make sure reads from device memory of size 1 - for (dst[0..len], hw_buf[0..len]) |*d, *s| - @atomicStore(u8, d, @atomicLoad(u8, s, .unordered), .unordered); + dpram_memcpy(dst[0..len], hw_buf[0..len]); + hw_buf = hw_buf[len..]; if (hw_buf.len == 0) return @intCast(hw_buf.ptr - ep.data_buffer.ptr); } - unreachable; + log.warn("discarding rx data on ep {t}, {} bytes received", .{ ep_num, bufctrl.LENGTH_0 }); + return @intCast(total_len); } fn ep_listen( @@ -361,6 +361,22 @@ pub fn Polled(config: Config) type { peripherals.USB.ADDR_ENDP.write(.{ .ADDRESS = addr }); } + fn dpram_memcpy(dst: []u8, src: []const u8) void { + assert(dst.len == src.len); + switch (chip) { + .RP2040 => @memcpy(dst, src), + .RP2350 => { + // Could be optimized for aligned data, for now just copy + // byte by byte. Atomic operations are used so that + // the compiler does not try to optimize this. + for (dst, src) |*d, *s| { + const tmp = @atomicLoad(u8, s, .unordered); + @atomicStore(u8, d, tmp, .unordered); + } + }, + } + } + fn hardware_endpoint_get_by_address(self: *@This(), ep: usb.types.Endpoint) *HardwareEndpointData { return &self.endpoints[@intFromEnum(ep.num)][@intFromEnum(ep.dir)]; }