From d2725893fdf697b65ffdf29d810d82524df32441 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 10 Jan 2026 14:40:46 +0100 Subject: [PATCH 01/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] ...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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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)]; } From 1a1aab8d48e81b12a072c20ca9da2a6c302c2192 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Wed, 21 Jan 2026 20:09:47 +0100 Subject: [PATCH 39/50] add rp2xxx reset interface --- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 2 +- examples/raspberrypi/rp2xxx/src/usb_hid.zig | 2 +- port/raspberrypi/rp2xxx/src/hal/usb.zig | 53 +++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 81018ab47..4ce452b5d 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -35,7 +35,7 @@ var usb_controller: usb.DeviceController(.{ .configuration_s = 0, .attributes = .{ .self_powered = false }, .max_current_ma = 50, - .Drivers = struct { serial: USB_Serial }, + .Drivers = struct { serial: USB_Serial, reset: rp2xxx.usb.ResetDriver(null, 0) }, }}, .max_supported_packet_size = USB_Device.max_supported_packet_size, }) = .init; diff --git a/examples/raspberrypi/rp2xxx/src/usb_hid.zig b/examples/raspberrypi/rp2xxx/src/usb_hid.zig index 03fd4548d..de89a1119 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -38,7 +38,7 @@ var usb_controller: usb.DeviceController(.{ .configuration_s = 0, .attributes = .{ .self_powered = false }, .max_current_ma = 50, - .Drivers = struct { hid: HID_Driver }, + .Drivers = struct { hid: HID_Driver, reset: rp2xxx.usb.ResetDriver(null, 0) }, }}, .max_supported_packet_size = USB_Device.max_supported_packet_size, }) = .init; diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index acd867145..730a73fac 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -428,3 +428,56 @@ pub fn Polled(config: Config) type { } }; } + +pub fn ResetDriver(bootsel_activity_led: ?u5, interface_disable_mask: u32) type { + return struct { + const log_drv = std.log.scoped(.pico_reset); + + pub const Descriptor = extern struct { + reset_interface: usb.descriptor.Interface, + + pub fn create(alloc: *usb.DescriptorAllocator, _: u8, _: usb.types.Len) @This() { + return .{ .reset_interface = .{ + .interface_number = alloc.next_itf(), + .alternate_setting = 0, + .num_endpoints = 0, + .interface_triple = .from( + .VendorSpecific, + @enumFromInt(0x00), + @enumFromInt(0x01), + ), + .interface_s = 0, + } }; + } + }; + + pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { + _ = desc; + _ = device; + self.* = .{}; + } + + pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { + _ = self; + switch (setup.request) { + 0x01 => { + const value = setup.value.into(); + const mask = @as(u32, 1) << if (value & 0x100 != 0) + @intCast(value >> 9) + else + bootsel_activity_led orelse 0; + const itf_disable = (value & 0x7F) | interface_disable_mask; + log_drv.debug("Resetting to bootsel. Mask: {} Itf disable: {}", .{ mask, itf_disable }); + microzig.hal.rom.reset_to_usb_boot(); + }, + 0x02 => { + log_drv.debug("Resetting to flash", .{}); + // Not implemented yet + microzig.hal.rom.reset_to_usb_boot(); + }, + else => return usb.nak, + } + return usb.ack; + } + }; +} From 6205e4f2d46d0c4afb335b217d6ef9f0a4d2542b Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Wed, 21 Jan 2026 20:19:57 +0100 Subject: [PATCH 40/50] add rp2xxx default usb vid and pid --- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 4 ++-- examples/raspberrypi/rp2xxx/src/usb_hid.zig | 4 ++-- port/raspberrypi/rp2xxx/src/hal/usb.zig | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 4ce452b5d..235b72d3a 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -15,8 +15,8 @@ var usb_controller: usb.DeviceController(.{ .bcd_usb = USB_Device.max_supported_bcd_usb, .device_triple = .from(.Miscellaneous, @enumFromInt(2), @enumFromInt(1)), .max_packet_size0 = USB_Device.max_supported_packet_size, - .vendor = .from(0x2E8A), - .product = .from(0x000A), + .vendor = USB_Device.default_vendor_id, + .product = USB_Device.default_product_id, .bcd_device = .from(0x0100), .manufacturer_s = 1, .product_s = 2, diff --git a/examples/raspberrypi/rp2xxx/src/usb_hid.zig b/examples/raspberrypi/rp2xxx/src/usb_hid.zig index de89a1119..5bc7d8052 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -18,8 +18,8 @@ var usb_controller: usb.DeviceController(.{ .bcd_usb = USB_Device.max_supported_bcd_usb, .device_triple = .unspecified, .max_packet_size0 = USB_Device.max_supported_packet_size, - .vendor = .from(0x2E8A), - .product = .from(0x000A), + .vendor = USB_Device.default_vendor_id, + .product = USB_Device.default_product_id, .bcd_device = .from(0x0100), .manufacturer_s = 1, .product_s = 2, diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 730a73fac..5d13ed93d 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -84,6 +84,11 @@ 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(0x0110); + pub const default_vendor_id: usb.types.U16_Le = .from(0x2E8A); + pub const default_product_id: usb.types.U16_Le = switch (chip) { + .RP2040 => .from(0x000A), + .RP2350 => .from(0x000F), + }; const vtable: usb.DeviceInterface.VTable = .{ .ep_writev = ep_writev, From d29f52d99b9379f806c32ffd701e13ae33bb722b Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Thu, 22 Jan 2026 22:05:00 +0100 Subject: [PATCH 41/50] try flushing in cdc driver even if no data was written --- core/src/core/usb/drivers/cdc.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 001a229b2..64b39e8b3 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -176,8 +176,6 @@ pub fn CdcClassDriver(options: Options) type { pub fn write(self: *@This(), data: []const u8) usize { const len = @min(self.tx_data.len - self.tx_end, data.len); - if (len == 0) return 0; - @memcpy(self.tx_data[self.tx_end..][0..len], data[0..len]); self.tx_end += @intCast(len); From b2cf7a43e8f616cb7359638f5d996743e0cefa73 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Thu, 22 Jan 2026 22:09:24 +0100 Subject: [PATCH 42/50] change packet size assert --- core/src/core/usb.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index b4ff02188..18d8abe6d 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -159,7 +159,8 @@ pub fn DeviceController(config: Config) type { 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); + assert(max_psize >= 8); + assert(std.math.isPowerOfTwo(max_psize)); var alloc: DescriptorAllocator = .init(config.unique_endpoints); var next_string = 4; From a414fb8b8fa482cb1a3528cb727677057a9f8239 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Fri, 23 Jan 2026 01:27:27 +0100 Subject: [PATCH 43/50] prepare for 0.16 migration --- core/src/core/usb.zig | 286 +++++++++++++++++++++--------------------- 1 file changed, 142 insertions(+), 144 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 18d8abe6d..c8dc1c521 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -13,6 +13,42 @@ pub const types = @import("usb/types.zig"); pub const ack: []const u8 = ""; pub const nak: ?[]const u8 = null; +/// Meant to make transition to zig 0.16 easier +pub const StructFieldAttributes = struct { + @"comptime": bool = false, + @"align": ?usize = null, + default_value_ptr: ?*const anyopaque = null, +}; + +/// Meant to make transition to zig 0.16 easier +pub fn Struct( + comptime layout: std.builtin.Type.ContainerLayout, + comptime BackingInt: ?type, + comptime field_names: []const [:0]const u8, + comptime field_types: *const [field_names.len]type, + comptime field_attrs: *const [field_names.len]StructFieldAttributes, +) type { + comptime var fields: []const std.builtin.Type.StructField = &.{}; + for (field_names, field_types, field_attrs) |n, T, a| { + fields = fields ++ &[1]std.builtin.Type.StructField{.{ + .name = n, + .type = T, + .alignment = a.@"align" orelse @alignOf(T), + .default_value_ptr = a.default_value_ptr, + .is_comptime = a.@"comptime", + }}; + } + return @Type(.{ .@"struct" = .{ + .layout = layout, + .backing_integer = BackingInt, + .decls = &.{}, + .fields = fields, + .is_tuple = false, + } }); +} + +const drivers_alias = drivers; + pub const DescriptorAllocator = struct { next_ep_num: [2]u8, next_itf_num: u8, @@ -91,28 +127,26 @@ pub const DeviceInterface = struct { } }; +pub fn EndpointHandler(Driver: type) type { + return fn (*Driver, types.Endpoint.Num) void; +} + pub fn DriverHadlers(Driver: type) type { - const EndpointHandler = fn (*Driver, types.Endpoint.Num) void; + var field_names: []const [:0]const u8 = &.{}; - 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, - }}, + descriptor.Endpoint => field_names = field_names ++ .{fld.name}, else => {}, }; - return @Type(.{ .@"struct" = .{ - .decls = &.{}, - .fields = ret_fields, - .is_tuple = false, - .layout = .auto, - } }); + + return Struct( + .auto, + null, + field_names, + &@splat(EndpointHandler(Driver)), + &@splat(.{}), + ); } pub const Config = struct { @@ -157,7 +191,12 @@ pub fn DeviceController(config: Config) type { const config0 = config.configurations[0]; const driver_fields = @typeInfo(config0.Drivers).@"struct".fields; const DriverEnum = std.meta.FieldEnum(config0.Drivers); - const config_descriptor = blk: { + + /// This parses the drivers' descriptors and creates: + /// * the configuration descriptor (currently only one configuration is supported) + /// * table of handlers for endpoints + /// * table of handlers for interfaces + const descriptor_parse_result = blk: { const max_psize = config.max_supported_packet_size; assert(max_psize >= 8); assert(std.math.isPowerOfTwo(max_psize)); @@ -166,39 +205,64 @@ pub fn DeviceController(config: Config) type { var next_string = 4; var size = @sizeOf(descriptor.Configuration); - var fields: [driver_fields.len + 1]std.builtin.Type.StructField = undefined; + var field_names: [driver_fields.len + 1][:0]const u8 = undefined; + var field_types: [field_names.len]type = undefined; + var field_attrs: [field_names.len]StructFieldAttributes = undefined; + var ep_handler_types: [2][16]type = @splat(@splat(void)); + var ep_handler_names: [2][16][:0]const u8 = undefined; + var ep_handler_drivers: [2][16]?usize = @splat(@splat(null)); + var itf_handlers: []const DriverEnum = &.{}; - for (driver_fields, 1..) |drv, i| { - const payload = drv.type.Descriptor.create(&alloc, next_string, max_psize); - const Payload = @TypeOf(payload); - size += @sizeOf(Payload); + for (driver_fields, 0..) |drv, drv_id| { + const Descriptors = drv.type.Descriptor; + const descriptors = Descriptors.create(&alloc, next_string, max_psize); + assert(@alignOf(Descriptors) == 1); + size += @sizeOf(Descriptors); + + for (@typeInfo(Descriptors).@"struct".fields, 0..) |fld, desc_num| { + const desc = @field(descriptors, fld.name); + + if (desc_num == 0) { + const itf_start, const itf_count = switch (fld.type) { + descriptor.InterfaceAssociation => .{ desc.first_interface, desc.interface_count }, + descriptor.Interface => .{ desc.interface_number, 1 }, + else => |T| @compileError( + "Expected first descriptor of driver " ++ + @typeName(drv.type) ++ + " to be of type Interface or InterfaceAssociation descriptor, got: " ++ + @typeName(T), + ), + }; + if (itf_start != itf_handlers.len) + @compileError("interface numbering mismatch"); + itf_handlers = itf_handlers ++ &[1]DriverEnum{@field(DriverEnum, drv.name)} ** itf_count; + } - for (@typeInfo(Payload).@"struct".fields) |fld| { - const desc = @field(payload, fld.name); switch (fld.type) { descriptor.Interface => { - if (desc.interface_s != 0) - next_string += 1; + if (desc.interface_s != 0) next_string += 1; + }, + descriptor.Endpoint => { + const ep_dir = @intFromEnum(desc.endpoint.dir); + const ep_num = @intFromEnum(desc.endpoint.num); + + if (ep_handler_types[ep_dir][ep_num] != void) + @compileError(std.fmt.comptimePrint( + "ep{} {t}: multiple handlers: {s} and {s}", + .{ ep_num, desc.endpoint.dir, ep_handler_drivers[ep_dir][ep_num], drv.name }, + )); + + ep_handler_types[ep_dir][ep_num] = EndpointHandler(drv.type); + ep_handler_drivers[ep_dir][ep_num] = drv_id; + ep_handler_names[ep_dir][ep_num] = fld.name; }, - descriptor.Endpoint, - descriptor.InterfaceAssociation, - descriptor.cdc.Header, - descriptor.cdc.CallManagement, - descriptor.cdc.AbstractControlModel, - descriptor.cdc.Union, - descriptor.hid.HID, - => {}, - else => @compileLog(fld), + descriptor.InterfaceAssociation => assert(desc_num == 0), + else => {}, } } - - fields[i] = .{ - .name = drv.name, - .type = Payload, - .default_value_ptr = &payload, - .is_comptime = false, - .alignment = 1, - }; + field_names[drv_id + 1] = drv.name; + field_types[drv_id + 1] = Descriptors; + field_attrs[drv_id + 1] = .{ .default_value_ptr = &descriptors }; } if (next_string != config.string_descriptors.len) @@ -216,106 +280,40 @@ pub fn DeviceController(config: Config) type { .max_current = .from_ma(config0.max_current_ma), }; - fields[0] = .{ - .name = "__configuration_descriptor", - .type = descriptor.Configuration, - .default_value_ptr = &desc_cfg, - .is_comptime = false, - .alignment = 1, - }; - - break :blk @Type(.{ .@"struct" = .{ - .decls = &.{}, - .fields = &fields, - .is_tuple = false, - .layout = .auto, - } }){}; - }; - - const handlers = blk: { - @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; - } - - 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); - const tag = @tagName(desc.endpoint.dir); - const ep_num = @intFromEnum(desc.endpoint.num); - - const driver = &@field(drivers_ep, tag)[ep_num]; - if (driver.*.len != 0) - @compileError(std.fmt.comptimePrint( - "ep{} {t}: multiple handlers: {s} and {s}", - .{ ep_num, desc.endpoint.dir, driver.*, fld_drv.name }, - )); - - 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; + field_names[0] = "__configuration_descriptor"; + field_types[0] = descriptor.Configuration; + field_attrs[0] = .{ .default_value_ptr = &desc_cfg }; + + const Tuple = std.meta.Tuple; + const ep_handlers_types: [2]type = .{ Tuple(&ep_handler_types[0]), Tuple(&ep_handler_types[1]) }; + var ep_handlers: Tuple(&ep_handlers_types) = undefined; + for (&ep_handler_types, &ep_handler_names, &ep_handler_drivers, 0..) |htypes, hnames, hdrivers, dir| { + for (&htypes, &hnames, &hdrivers, 0..) |T, name, drv_id, ep| { + if (T != void) + ep_handlers[dir][ep] = @field(driver_fields[drv_id.?].type.handlers, name); } } + + const idx_in = @intFromEnum(types.Dir.In); + const idx_out = @intFromEnum(types.Dir.Out); 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, + .config_descriptor = Struct(.auto, null, &field_names, &field_types, &field_attrs){}, + .handlers_itf = itf_handlers, + .handlers_ep = struct { + In: ep_handlers_types[idx_in] = ep_handlers[idx_in], + Out: ep_handlers_types[idx_out] = ep_handlers[idx_out], + }{}, + .drivers_ep = ep_handler_drivers, }; }; + const config_descriptor = descriptor_parse_result.config_descriptor; + const handlers_itf = descriptor_parse_result.handlers_itf; + const handlers_ep = .{ + .han = descriptor_parse_result.handlers_ep, + .drv = descriptor_parse_result.drivers_ep, + }; + /// 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. @@ -366,8 +364,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 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)]; + const driver_opt = comptime handlers_ep.drv[@intFromEnum(ep.dir)][@intFromEnum(ep.num)]; + const handler = 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 @@ -396,8 +394,8 @@ pub fn DeviceController(config: Config) type { } else if (comptime ep == types.Endpoint.out(.ep0)) log.warn("Unhandled packet on ep0 Out", .{}); - if (comptime driver.len != 0) { - function(&@field(self.driver_data.?, driver), ep.num); + if (comptime driver_opt) |driver| { + handler(&@field(self.driver_data.?, driver_fields[driver].name), ep.num); } } @@ -453,11 +451,11 @@ pub fn DeviceController(config: Config) type { 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]; + inline else => |itf| if (comptime itf < handlers_itf.len) { + const drv = handlers_itf[itf]; return @field(self.driver_data.?, @tagName(drv)).class_request(setup); } else { - log.warn("Interface index ({}) out of range ({})", .{ itf_num, handlers.itf.len }); + log.warn("Interface index ({}) out of range ({})", .{ itf_num, handlers_itf.len }); return nak; }, } From 5e74463f1b7e5e4e54e01e36a924575c03e67326 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Fri, 23 Jan 2026 01:58:57 +0100 Subject: [PATCH 44/50] make usb drivers more comprehensible --- core/src/core/usb/drivers/cdc.zig | 51 ++++++++++++++------------- core/src/core/usb/drivers/example.zig | 23 +++++++----- core/src/core/usb/drivers/hid.zig | 6 ++-- 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 64b39e8b3..d89b11a4d 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -77,6 +77,7 @@ pub fn CdcClassDriver(options: Options) type { first_string: u8, max_supported_packet_size: usb.types.Len, ) @This() { + assert(options.max_packet_size <= max_supported_packet_size); const itf_notifi = alloc.next_itf(); const itf_data = alloc.next_itf(); return .{ @@ -134,21 +135,18 @@ pub fn CdcClassDriver(options: Options) type { }; device: *usb.DeviceInterface, - ep_notifi: EP_Num, + descriptor: *const Descriptor, line_coding: LineCoding align(4), + notifi_ready: std.atomic.Value(bool), - /// OUT endpoint on which there is data ready to be read, - /// or .ep0 when no data is available. - ep_out: EP_Num, rx_data: [options.max_packet_size]u8, rx_seek: usb.types.Len, rx_end: usb.types.Len, + rx_ready: std.atomic.Value(bool), - /// IN endpoint where data can be sent, - /// or .ep0 when data is being sent. - ep_in: EP_Num, tx_data: [options.max_packet_size]u8, tx_end: usb.types.Len, + tx_ready: std.atomic.Value(bool), pub fn available(self: *@This()) usb.types.Len { return self.rx_end - self.rx_seek; @@ -162,11 +160,11 @@ pub fn CdcClassDriver(options: Options) type { if (self.available() > 0) return len; // request more data - const ep_out = @atomicLoad(EP_Num, &self.ep_out, .seq_cst); - if (ep_out != .ep0) { + if (self.rx_ready.load(.seq_cst)) { + const ep_out = self.descriptor.ep_out.endpoint.num; + self.rx_ready.store(false, .seq_cst); self.rx_end = self.device.ep_readv(ep_out, &.{&self.rx_data}); self.rx_seek = 0; - @atomicStore(EP_Num, &self.ep_out, .ep0, .seq_cst); self.device.ep_listen(ep_out, options.max_packet_size); } @@ -190,13 +188,14 @@ pub fn CdcClassDriver(options: Options) type { if (self.tx_end == 0) return true; - const ep_in = @atomicLoad(EP_Num, &self.ep_in, .seq_cst); - if (ep_in == .ep0) + if (!self.tx_ready.load(.seq_cst)) return false; + self.tx_ready.store(false, .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]})); + assert(self.tx_end == self.device.ep_writev( + self.descriptor.ep_in.endpoint.num, + &.{self.tx_data[0..self.tx_end]}, + )); self.tx_end = 0; return true; } @@ -204,22 +203,23 @@ pub fn CdcClassDriver(options: Options) type { pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { self.* = .{ .device = device, - .ep_notifi = desc.ep_notifi.endpoint.num, + .descriptor = desc, .line_coding = .{ .bit_rate = .from(115200), .stop_bits = .@"1", .parity = .none, .data_bits = 8, }, + .notifi_ready = .init(true), - .ep_out = .ep0, .rx_data = undefined, .rx_seek = 0, .rx_end = 0, + .rx_ready = .init(false), - .ep_in = desc.ep_in.endpoint.num, .tx_data = undefined, .tx_end = 0, + .tx_ready = .init(true), }; device.ep_listen(desc.ep_out.endpoint.num, options.max_packet_size); } @@ -243,18 +243,21 @@ pub fn CdcClassDriver(options: Options) type { } 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); + if (self.rx_ready.load(.seq_cst)) + log.warn("{s}({t}) called before buffer was consumed", .{ "on_rx", ep_num }); + self.rx_ready.store(true, .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); + if (self.tx_ready.load(.seq_cst)) + log.warn("{s}({t}) called before buffer was consumed", .{ "on_tx_ready", ep_num }); + self.tx_ready.store(true, .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); + if (self.notifi_ready.load(.seq_cst)) + log.warn("{s}({t}) called before buffer was consumed", .{ "on_notifi_ready", ep_num }); + self.notifi_ready.store(true, .seq_cst); } }; } diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index 67cfd5dee..52860319e 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -45,14 +45,16 @@ pub const EchoExampleDriver = struct { }; device: *usb.DeviceInterface, - ep_tx: EP_Num, + descriptor: *const Descriptor, + tx_ready: std.atomic.Value(bool), /// This function is called when the host chooses a configuration /// 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, + .descriptor = desc, + .tx_ready = .init(false), }; device.ep_listen( desc.ep_out.endpoint.num, @@ -72,24 +74,27 @@ pub const EchoExampleDriver = struct { /// 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: EP_Num) void { - log.info("tx ready", .{}); + log.info("tx ready ({t})", .{ep_tx}); // Mark transmission as available - @atomicStore(EP_Num, &self.ep_tx, ep_tx, .seq_cst); + self.tx_ready.store(true, .seq_cst); } 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]}); + log.info("Received on {t}: {s}", .{ ep_rx, buf[0..len_rx] }); // Check if we can transmit - const ep_tx = @atomicLoad(EP_Num, &self.ep_tx, .seq_cst); - if (ep_tx != .ep0) { + + if (self.tx_ready.load(.seq_cst)) { // Mark transmission as not available - @atomicStore(EP_Num, &self.ep_tx, .ep0, .seq_cst); + self.tx_ready.store(false, .seq_cst); // Send received packet log.info("Sending {} bytes", .{len_rx}); - const len_tx = self.device.ep_writev(ep_tx, &.{buf[0..len_rx]}); + const len_tx = self.device.ep_writev( + self.descriptor.ep_in.endpoint.num, + &.{buf[0..len_rx]}, + ); if (len_tx != len_rx) log.err("Only sent {} bytes", .{len_tx}); } diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index 8f6a0d734..35b3da963 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -55,14 +55,12 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { }; device: *usb.DeviceInterface, - ep_in: EP_Num, - ep_out: EP_Num, + descriptor: *const Descriptor, 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, + .descriptor = desc, }; } From 98b56299666c4ea1a1cf08da8228711004a736a6 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Fri, 23 Jan 2026 02:04:13 +0100 Subject: [PATCH 45/50] limit max_packet_size0 to 64 --- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 235b72d3a..29386b25d 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -14,7 +14,8 @@ var usb_controller: usb.DeviceController(.{ .device_descriptor = .{ .bcd_usb = USB_Device.max_supported_bcd_usb, .device_triple = .from(.Miscellaneous, @enumFromInt(2), @enumFromInt(1)), - .max_packet_size0 = USB_Device.max_supported_packet_size, + // Can ep0 packet size be larger than 64 bytes? + .max_packet_size0 = @min(USB_Device.max_supported_packet_size, 64), .vendor = USB_Device.default_vendor_id, .product = USB_Device.default_product_id, .bcd_device = .from(0x0100), From 45854901a2f291bc75c71e19c4ea0a75176c693d Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Fri, 23 Jan 2026 02:15:21 +0100 Subject: [PATCH 46/50] fix config descriptor not marked extern --- core/src/core/usb.zig | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index c8dc1c521..e75bf9149 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -205,7 +205,7 @@ pub fn DeviceController(config: Config) type { var next_string = 4; var size = @sizeOf(descriptor.Configuration); - var field_names: [driver_fields.len + 1][:0]const u8 = undefined; + var field_names: [driver_fields.len][:0]const u8 = undefined; var field_types: [field_names.len]type = undefined; var field_attrs: [field_names.len]StructFieldAttributes = undefined; var ep_handler_types: [2][16]type = @splat(@splat(void)); @@ -260,9 +260,9 @@ pub fn DeviceController(config: Config) type { else => {}, } } - field_names[drv_id + 1] = drv.name; - field_types[drv_id + 1] = Descriptors; - field_attrs[drv_id + 1] = .{ .default_value_ptr = &descriptors }; + field_names[drv_id] = drv.name; + field_types[drv_id] = Descriptors; + field_attrs[drv_id] = .{ .default_value_ptr = &descriptors }; } if (next_string != config.string_descriptors.len) @@ -271,19 +271,6 @@ pub fn DeviceController(config: Config) type { .{ next_string, config.string_descriptors.len }, )); - const desc_cfg: descriptor.Configuration = .{ - .total_length = .from(size), - .num_interfaces = alloc.next_itf_num, - .configuration_value = config0.num, - .configuration_s = config0.configuration_s, - .attributes = config0.attributes, - .max_current = .from_ma(config0.max_current_ma), - }; - - field_names[0] = "__configuration_descriptor"; - field_types[0] = descriptor.Configuration; - field_attrs[0] = .{ .default_value_ptr = &desc_cfg }; - const Tuple = std.meta.Tuple; const ep_handlers_types: [2]type = .{ Tuple(&ep_handler_types[0]), Tuple(&ep_handler_types[1]) }; var ep_handlers: Tuple(&ep_handlers_types) = undefined; @@ -294,10 +281,21 @@ pub fn DeviceController(config: Config) type { } } + const DriverConfig = Struct(.@"extern", null, &field_names, &field_types, &field_attrs); const idx_in = @intFromEnum(types.Dir.In); const idx_out = @intFromEnum(types.Dir.Out); break :blk .{ - .config_descriptor = Struct(.auto, null, &field_names, &field_types, &field_attrs){}, + .config_descriptor = extern struct { + first: descriptor.Configuration = .{ + .total_length = .from(size), + .num_interfaces = alloc.next_itf_num, + .configuration_value = config0.num, + .configuration_s = config0.configuration_s, + .attributes = config0.attributes, + .max_current = .from_ma(config0.max_current_ma), + }, + drv: DriverConfig = .{}, + }{}, .handlers_itf = itf_handlers, .handlers_ep = struct { In: ep_handlers_types[idx_in] = ep_handlers[idx_in], @@ -418,7 +416,7 @@ pub fn DeviceController(config: Config) type { log.debug("Device setup: {any}", .{request}); switch (request) { .GetStatus => { - const attr = config_descriptor.__configuration_descriptor.attributes; + const attr = config_descriptor.first.attributes; const status: types.DeviceStatus = comptime .create(attr.self_powered, false); return std.mem.asBytes(&status); }, @@ -500,7 +498,7 @@ pub fn DeviceController(config: Config) type { // 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); + const cfg = @field(config_descriptor.drv, fld_drv.name); const desc_fields = @typeInfo(@TypeOf(cfg)).@"struct".fields; // Open OUT endpoint first so that the driver can call ep_listen in init From ac69a738949d6d9a830a4d49561956a77a9865c6 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Fri, 23 Jan 2026 02:18:03 +0100 Subject: [PATCH 47/50] change cdc example device class to unspecified --- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 29386b25d..a885fe1fc 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -13,7 +13,7 @@ var usb_device: USB_Device = undefined; var usb_controller: usb.DeviceController(.{ .device_descriptor = .{ .bcd_usb = USB_Device.max_supported_bcd_usb, - .device_triple = .from(.Miscellaneous, @enumFromInt(2), @enumFromInt(1)), + .device_triple = .unspecified, // Can ep0 packet size be larger than 64 bytes? .max_packet_size0 = @min(USB_Device.max_supported_packet_size, 64), .vendor = USB_Device.default_vendor_id, From 85af64038e25ff4579c824004c3037e0d8d79099 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Fri, 23 Jan 2026 04:43:51 +0100 Subject: [PATCH 48/50] new type of driver configuration --- core/src/core/usb.zig | 119 +++++++++++++++----- core/src/core/usb/descriptor.zig | 6 +- core/src/core/usb/drivers/cdc.zig | 11 +- core/src/core/usb/drivers/example.zig | 11 +- core/src/core/usb/drivers/hid.zig | 17 +-- core/src/core/usb/types.zig | 23 ++++ examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 35 ++---- examples/raspberrypi/rp2xxx/src/usb_hid.zig | 35 ++---- port/raspberrypi/rp2xxx/src/hal/usb.zig | 18 +-- 9 files changed, 176 insertions(+), 99 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index e75bf9149..e87c1d666 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -53,12 +53,14 @@ pub const DescriptorAllocator = struct { next_ep_num: [2]u8, next_itf_num: u8, unique_endpoints: bool, + strings: []const []const u8, pub fn init(unique_endpoints: bool) @This() { return .{ .next_ep_num = @splat(1), .next_itf_num = 0, .unique_endpoints = unique_endpoints, + .strings = &.{}, }; } @@ -75,6 +77,25 @@ pub const DescriptorAllocator = struct { defer self.next_itf_num += 1; return self.next_itf_num; } + + pub fn string(self: *@This(), str: []const u8) u8 { + if (str.len == 0) return 0; + // deduplicate + for (self.strings, 1..) |s, i| { + if (std.mem.eql(u8, s, str)) + return i; + } + + self.strings = self.strings ++ .{str}; + return @intCast(self.strings.len); + } + + pub fn string_descriptors(self: @This(), lang: descriptor.String.Language) []const descriptor.String { + var ret: []const descriptor.String = &.{.from_lang(lang)}; + for (self.strings) |s| + ret = ret ++ [1]descriptor.String{.from_str(s)}; + return ret; + } }; /// USB Device interface @@ -150,24 +171,62 @@ pub fn DriverHadlers(Driver: type) type { } pub const Config = struct { + pub const IdStringPair = struct { + id: u16, + str: []const u8, + }; + pub const Configuration = struct { - num: u8, - configuration_s: u8, + name: []const u8 = "", attributes: descriptor.Configuration.Attributes, max_current_ma: u9, Drivers: type, + + pub fn Args(self: @This()) type { + const fields = @typeInfo(self.Drivers).@"struct".fields; + var field_names: [fields.len][:0]const u8 = undefined; + var field_types: [fields.len]type = undefined; + for (fields, 0..) |fld, i| { + field_names[i] = fld.name; + const params = @typeInfo(@TypeOf(fld.type.Descriptor.create)).@"fn".params; + assert(params.len == 3); + assert(params[0].type == *DescriptorAllocator); + assert(params[1].type == types.Len); + field_types[i] = params[2].type.?; + } + return Struct(.auto, null, &field_names, &field_types, &@splat(.{})); + } }; - device_descriptor: descriptor.Device, - string_descriptors: []const descriptor.String, + /// Usb specification version. + bcd_usb: types.Version, + /// Class, subclass and protocol of this device. + device_triple: types.ClassSubclassProtocol, + /// Vendor id and string + vendor: IdStringPair, + /// Product id and string. + product: IdStringPair, + /// Device version. + bcd_device: types.Version, + /// String descriptor 0. + language: descriptor.String.Language = .English, + /// Serial number string. + serial: []const u8, + /// Largest packet length the hardware supports. Must be a power of 2 and at least 8. + max_supported_packet_size: types.Len, /// 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, - /// Device specific, either 8, 16, 32 or 64. - max_supported_packet_size: types.Len, + + pub fn DriverArgs(self: @This()) type { + var field_types: [self.configurations.len]type = undefined; + for (self.configurations, &field_types) |cfg, *dst| + dst.* = cfg.Args(); + return std.meta.Tuple(&field_types); + } }; pub fn validate_controller(T: type) void { @@ -184,7 +243,7 @@ pub fn validate_controller(T: type) void { /// USB device controller /// /// This code handles usb enumeration and configuration and routes packets to drivers. -pub fn DeviceController(config: Config) type { +pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { std.debug.assert(config.configurations.len == 1); return struct { @@ -202,7 +261,20 @@ pub fn DeviceController(config: Config) type { assert(std.math.isPowerOfTwo(max_psize)); var alloc: DescriptorAllocator = .init(config.unique_endpoints); - var next_string = 4; + + const desc_device: descriptor.Device = .{ + .bcd_usb = config.bcd_usb, + .device_triple = config.device_triple, + .max_packet_size0 = @max(config.max_supported_packet_size, 64), + .vendor = .from(config.vendor.id), + .product = .from(config.product.id), + .bcd_device = config.bcd_device, + .manufacturer_s = alloc.string(config.vendor.str), + .product_s = alloc.string(config.product.str), + .serial_s = alloc.string(config.serial), + .num_configurations = config.configurations.len, + }; + const configuration_s = alloc.string(config0.name); var size = @sizeOf(descriptor.Configuration); var field_names: [driver_fields.len][:0]const u8 = undefined; @@ -215,7 +287,7 @@ pub fn DeviceController(config: Config) type { for (driver_fields, 0..) |drv, drv_id| { const Descriptors = drv.type.Descriptor; - const descriptors = Descriptors.create(&alloc, next_string, max_psize); + const descriptors = Descriptors.create(&alloc, max_psize, @field(driver_args[0], drv.name)); assert(@alignOf(Descriptors) == 1); size += @sizeOf(Descriptors); @@ -239,9 +311,6 @@ pub fn DeviceController(config: Config) type { } switch (fld.type) { - descriptor.Interface => { - if (desc.interface_s != 0) next_string += 1; - }, descriptor.Endpoint => { const ep_dir = @intFromEnum(desc.endpoint.dir); const ep_num = @intFromEnum(desc.endpoint.num); @@ -265,12 +334,6 @@ pub fn DeviceController(config: Config) type { field_attrs[drv_id] = .{ .default_value_ptr = &descriptors }; } - if (next_string != config.string_descriptors.len) - @compileError(std.fmt.comptimePrint( - "expected {} string descriptros, got {}", - .{ next_string, config.string_descriptors.len }, - )); - const Tuple = std.meta.Tuple; const ep_handlers_types: [2]type = .{ Tuple(&ep_handler_types[0]), Tuple(&ep_handler_types[1]) }; var ep_handlers: Tuple(&ep_handlers_types) = undefined; @@ -285,17 +348,19 @@ pub fn DeviceController(config: Config) type { const idx_in = @intFromEnum(types.Dir.In); const idx_out = @intFromEnum(types.Dir.Out); break :blk .{ + .device_descriptor = desc_device, .config_descriptor = extern struct { first: descriptor.Configuration = .{ .total_length = .from(size), .num_interfaces = alloc.next_itf_num, - .configuration_value = config0.num, - .configuration_s = config0.configuration_s, + .configuration_value = 1, + .configuration_s = configuration_s, .attributes = config0.attributes, .max_current = .from_ma(config0.max_current_ma), }, drv: DriverConfig = .{}, }{}, + .string_descriptors = alloc.string_descriptors(config.language), .handlers_itf = itf_handlers, .handlers_ep = struct { In: ep_handlers_types[idx_in] = ep_handlers[idx_in], @@ -306,6 +371,8 @@ pub fn DeviceController(config: Config) type { }; const config_descriptor = descriptor_parse_result.config_descriptor; + const device_descriptor = descriptor_parse_result.device_descriptor; + const string_descriptors = descriptor_parse_result.string_descriptors; const handlers_itf = descriptor_parse_result.handlers_itf; const handlers_ep = .{ .han = descriptor_parse_result.handlers_ep, @@ -352,7 +419,7 @@ pub fn DeviceController(config: Config) type { 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); + assert(len <= device_descriptor.max_packet_size0); self.tx_slice = limited[len..]; } } @@ -465,15 +532,15 @@ pub fn DeviceController(config: Config) type { const desc_idx: u8 = @truncate(value); 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()), + .Device => asBytes(&device_descriptor), + .DeviceQualifier => asBytes(comptime &device_descriptor.qualifier()), .Configuration => asBytes(&config_descriptor), - .String => if (desc_idx < config.string_descriptors.len) - config.string_descriptors[desc_idx].data + .String => if (desc_idx < string_descriptors.len) + string_descriptors[desc_idx].data else { log.warn( "Descriptor index ({}) out of range ({})", - .{ desc_idx, config.string_descriptors.len }, + .{ desc_idx, string_descriptors.len }, ); return nak; }, diff --git a/core/src/core/usb/descriptor.zig b/core/src/core/usb/descriptor.zig index 3d6109a1a..b1daeb8ae 100644 --- a/core/src/core/usb/descriptor.zig +++ b/core/src/core/usb/descriptor.zig @@ -42,7 +42,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.U16_Le, + bcd_usb: types.Version, /// Class, subclass and protocol of device. device_triple: types.ClassSubclassProtocol, /// Maximum unit of data this device can move. @@ -62,7 +62,7 @@ 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.U16_Le, + bcd_usb: types.Version, /// Class, subclass and protocol of device. device_triple: types.ClassSubclassProtocol, /// Maximum length of data this device can move. @@ -72,7 +72,7 @@ pub const Device = extern struct { /// ID of product. product: types.U16_Le, /// Device version number as Binary Coded Decimal. - bcd_device: types.U16_Le, + bcd_device: types.Version, /// Index of manufacturer name in string descriptor table. manufacturer_s: u8, /// Index of product name in string descriptor table. diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index d89b11a4d..3173883b8 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -72,10 +72,15 @@ pub fn CdcClassDriver(options: Options) type { ep_out: desc.Endpoint, ep_in: desc.Endpoint, + const Strings = struct { + itf_notifi: []const u8 = "", + itf_data: []const u8 = "", + }; + pub fn create( alloc: *usb.DescriptorAllocator, - first_string: u8, max_supported_packet_size: usb.types.Len, + strings: Strings, ) @This() { assert(options.max_packet_size <= max_supported_packet_size); const itf_notifi = alloc.next_itf(); @@ -94,7 +99,7 @@ pub fn CdcClassDriver(options: Options) type { .alternate_setting = 0, .num_endpoints = 1, .interface_triple = .from(.CDC, .Abstract, .NoneRequired), - .interface_s = first_string, + .interface_s = alloc.string(strings.itf_notifi), }, .cdc_header = .{ .bcd_cdc = .from(0x0120) }, .cdc_call_mgmt = .{ @@ -120,7 +125,7 @@ pub fn CdcClassDriver(options: Options) type { .alternate_setting = 0, .num_endpoints = 2, .interface_triple = .from(.CDC_Data, .Unused, .NoneRequired), - .interface_s = 0, + .interface_s = alloc.string(strings.itf_data), }, .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 52860319e..9a18574a2 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -9,7 +9,7 @@ pub const EchoExampleDriver = struct { pub const Descriptor = extern struct { const desc = usb.descriptor; - example_interface: desc.Interface, + itf: desc.Interface, ep_out: desc.Endpoint, ep_in: desc.Endpoint, @@ -17,18 +17,21 @@ pub const EchoExampleDriver = struct { /// 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. + /// Third argument can be of any type, it's passed by the user when + /// creating the device controller type. Passing arguments this way + /// is preffered to making the whole driver generic. pub fn create( alloc: *usb.DescriptorAllocator, - first_string: u8, max_supported_packet_size: usb.types.Len, + itf_string: []const u8, ) @This() { return .{ - .example_interface = .{ + .itf = .{ .interface_number = alloc.next_itf(), .alternate_setting = 0, .num_endpoints = 2, .interface_triple = .vendor_specific, - .interface_s = first_string, + .interface_s = alloc.string(itf_string), }, .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 35b3da963..dcff16324 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -3,12 +3,7 @@ const usb = @import("../../usb.zig"); const EP_Num = usb.types.Endpoint.Num; const log = std.log.scoped(.usb_hid); -pub const Options = struct { - boot_protocol: bool, - poll_interval: u8, -}; - -pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { +pub fn HidClassDriver(report_descriptor: anytype) type { return struct { pub const Descriptor = extern struct { const desc = usb.descriptor; @@ -18,10 +13,16 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { ep_out: desc.Endpoint, ep_in: desc.Endpoint, + pub const Options = struct { + itf_string: []const u8 = "", + boot_protocol: bool, + poll_interval: u8, + }; + pub fn create( alloc: *usb.DescriptorAllocator, - first_string: u8, max_supported_packet_size: usb.types.Len, + options: Options, ) @This() { return .{ .interface = .{ @@ -33,7 +34,7 @@ pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { if (options.boot_protocol) .Boot else .Unspecified, if (options.boot_protocol) .Boot else .None, ), - .interface_s = first_string, + .interface_s = alloc.string(options.itf_string), }, .hid = hid_descriptor, .ep_out = .interrupt(alloc.next_ep(.Out), max_supported_packet_size, options.poll_interval), diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index bfcd04ba0..f030f013a 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -407,6 +407,29 @@ pub const SetupPacket = extern struct { length: U16_Le, }; +/// Represents USB or device version, in binary coded decimal. +pub const Version = extern struct { + pub const DigitPair = packed struct(u8) { + low: u4, + high: u4, + }; + + low: DigitPair, + high: DigitPair, + + pub const v1_0: @This() = .from(1, 0, 0); + pub const v1_1: @This() = .from(1, 1, 0); + pub const v2_0: @This() = .from(2, 0, 0); + pub const v2_1: @This() = .from(2, 1, 0); + + pub fn from(major: u4, minor: u4, revision: u4) @This() { + return .{ + .high = .{ .high = 0, .low = major }, + .low = .{ .high = minor, .low = revision }, + }; + } +}; + /// Represents packet length. pub const Len = u11; diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index a885fe1fc..92952baa4 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -11,35 +11,22 @@ const USB_Serial = usb.drivers.cdc.CdcClassDriver(.{ .max_packet_size = USB_Devi var usb_device: USB_Device = undefined; var usb_controller: usb.DeviceController(.{ - .device_descriptor = .{ - .bcd_usb = USB_Device.max_supported_bcd_usb, - .device_triple = .unspecified, - // Can ep0 packet size be larger than 64 bytes? - .max_packet_size0 = @min(USB_Device.max_supported_packet_size, 64), - .vendor = USB_Device.default_vendor_id, - .product = USB_Device.default_product_id, - .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"), - }, + .bcd_usb = USB_Device.max_supported_bcd_usb, + .device_triple = .unspecified, + .vendor = USB_Device.default_vendor_id, + .product = USB_Device.default_product_id, + .bcd_device = .from(1, 0, 0), + .serial = "someserial", + .max_supported_packet_size = USB_Device.max_supported_packet_size, .configurations = &.{.{ - .num = 1, - .configuration_s = 0, .attributes = .{ .self_powered = false }, .max_current_ma = 50, .Drivers = struct { serial: USB_Serial, reset: rp2xxx.usb.ResetDriver(null, 0) }, }}, - .max_supported_packet_size = USB_Device.max_supported_packet_size, -}) = .init; +}, .{.{ + .serial = .{ .itf_notifi = "Board CDC", .itf_data = "Board CDC Data" }, + .reset = "", +}}) = .init; pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { std.log.err("panic: {s}", .{message}); diff --git a/examples/raspberrypi/rp2xxx/src/usb_hid.zig b/examples/raspberrypi/rp2xxx/src/usb_hid.zig index 5bc7d8052..fbd3cd1c4 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -7,41 +7,28 @@ const gpio = rp2xxx.gpio; const usb = microzig.core.usb; 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: USB_Device = undefined; var usb_controller: usb.DeviceController(.{ - .device_descriptor = .{ - .bcd_usb = USB_Device.max_supported_bcd_usb, - .device_triple = .unspecified, - .max_packet_size0 = USB_Device.max_supported_packet_size, - .vendor = USB_Device.default_vendor_id, - .product = USB_Device.default_product_id, - .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"), - }, + .bcd_usb = USB_Device.max_supported_bcd_usb, + .device_triple = .unspecified, + .vendor = USB_Device.default_vendor_id, + .product = USB_Device.default_product_id, + .bcd_device = .from(1, 0, 0), + .serial = "someserial", + .max_supported_packet_size = USB_Device.max_supported_packet_size, .configurations = &.{.{ - .num = 1, - .configuration_s = 0, .attributes = .{ .self_powered = false }, .max_current_ma = 50, .Drivers = struct { hid: HID_Driver, reset: rp2xxx.usb.ResetDriver(null, 0) }, }}, - .max_supported_packet_size = USB_Device.max_supported_packet_size, -}) = .init; +}, .{.{ + .hid = .{ .itf_string = "Boot Keyboard", .boot_protocol = true, .poll_interval = 0 }, + .reset = "", +}}) = .init; pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { std.log.err("panic: {s}", .{message}); diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 5d13ed93d..e87f58038 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -83,11 +83,11 @@ 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(0x0110); - pub const default_vendor_id: usb.types.U16_Le = .from(0x2E8A); - pub const default_product_id: usb.types.U16_Le = switch (chip) { - .RP2040 => .from(0x000A), - .RP2350 => .from(0x000F), + pub const max_supported_bcd_usb: usb.types.Version = .v1_1; + pub const default_vendor_id: usb.Config.IdStringPair = .{ .id = 0x2E8A, .str = "Raspberry Pi" }; + pub const default_product_id: usb.Config.IdStringPair = switch (chip) { + .RP2040 => .{ .id = 0x000A, .str = "Pico test device" }, + .RP2350 => .{ .id = 0x000F, .str = "Pico 2 test device" }, }; const vtable: usb.DeviceInterface.VTable = .{ @@ -441,7 +441,11 @@ pub fn ResetDriver(bootsel_activity_led: ?u5, interface_disable_mask: u32) type pub const Descriptor = extern struct { reset_interface: usb.descriptor.Interface, - pub fn create(alloc: *usb.DescriptorAllocator, _: u8, _: usb.types.Len) @This() { + pub fn create( + alloc: *usb.DescriptorAllocator, + _: usb.types.Len, + interface_str: []const u8, + ) @This() { return .{ .reset_interface = .{ .interface_number = alloc.next_itf(), .alternate_setting = 0, @@ -451,7 +455,7 @@ pub fn ResetDriver(bootsel_activity_led: ?u5, interface_disable_mask: u32) type @enumFromInt(0x00), @enumFromInt(0x01), ), - .interface_s = 0, + .interface_s = alloc.string(interface_str), } }; } }; From 4c3d8d2fe72d4d9a8294b3a008c69ba482c475df Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 24 Jan 2026 01:08:59 +0100 Subject: [PATCH 49/50] add description-creation-time allocations --- core/src/core/usb.zig | 28 +- core/src/core/usb/drivers/cdc.zig | 401 ++++++++++---------- core/src/core/usb/drivers/example.zig | 42 +- core/src/core/usb/drivers/hid.zig | 10 +- examples/raspberrypi/rp2xxx/src/usb_cdc.zig | 2 +- port/raspberrypi/rp2xxx/src/hal/usb.zig | 9 +- 6 files changed, 264 insertions(+), 228 deletions(-) diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index e87c1d666..e927d524d 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -98,6 +98,14 @@ pub const DescriptorAllocator = struct { } }; +pub fn DescriptorCreateResult(Descriptor: type) type { + return struct { + descriptor: Descriptor, + alloc_bytes: usize = 0, + alloc_align: ?usize = null, + }; +} + /// USB Device interface /// Any device implementation used with DeviceController must implement those functions pub const DeviceInterface = struct { @@ -284,10 +292,17 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { var ep_handler_names: [2][16][:0]const u8 = undefined; var ep_handler_drivers: [2][16]?usize = @splat(@splat(null)); var itf_handlers: []const DriverEnum = &.{}; + var driver_alloc_types: [driver_fields.len]type = undefined; + var driver_alloc_attrs: [driver_fields.len]StructFieldAttributes = @splat(.{}); for (driver_fields, 0..) |drv, drv_id| { const Descriptors = drv.type.Descriptor; - const descriptors = Descriptors.create(&alloc, max_psize, @field(driver_args[0], drv.name)); + const result = Descriptors.create(&alloc, max_psize, @field(driver_args[0], drv.name)); + const descriptors = result.descriptor; + + driver_alloc_types[drv_id] = [result.alloc_bytes]u8; + driver_alloc_attrs[drv_id].@"align" = result.alloc_align; + assert(@alignOf(Descriptors) == 1); size += @sizeOf(Descriptors); @@ -367,6 +382,7 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { Out: ep_handlers_types[idx_out] = ep_handlers[idx_out], }{}, .drivers_ep = ep_handler_drivers, + .DriverAlloc = Struct(.auto, null, &field_names, &driver_alloc_types, &driver_alloc_attrs), }; }; @@ -378,6 +394,7 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { .han = descriptor_parse_result.handlers_ep, .drv = descriptor_parse_result.drivers_ep, }; + const DriverAlloc = descriptor_parse_result.DriverAlloc; /// If not zero, change the device address at the next opportunity. /// Necessary because when the host sets the device address, @@ -389,6 +406,8 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { tx_slice: ?[]const u8, /// Driver state driver_data: ?config0.Drivers, + /// + driver_alloc: DriverAlloc, /// Initial values pub const init: @This() = .{ @@ -396,6 +415,7 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { .cfg_num = 0, .tx_slice = null, .driver_data = null, + .driver_alloc = undefined, }; /// Returns a pointer to the drivers @@ -575,7 +595,11 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { device_itf.ep_open(desc); } - @field(self.driver_data.?, fld_drv.name).init(&cfg, device_itf); + @field(self.driver_data.?, fld_drv.name).init( + &cfg, + device_itf, + &@field(self.driver_alloc, fld_drv.name), + ); // Open IN endpoint last so that callbacks can happen inline for (desc_fields) |fld| { diff --git a/core/src/core/usb/drivers/cdc.zig b/core/src/core/usb/drivers/cdc.zig index 3173883b8..69bbf8f44 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -4,88 +4,83 @@ const assert = std.debug.assert; const EP_Num = usb.types.Endpoint.Num; 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 { - 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, +pub const CdcClassDriver = struct { + 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, _, }; - bit_rate: usb.types.U32_Le, - stop_bits: StopBits, - parity: Parity, - data_bits: u8, + pub const LineCoding = extern struct { + 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, + _, + }; - pub const init: @This() = .{ - .bit_rate = 115200, - .stop_bits = 0, - .parity = 0, - .data_bits = 8, - }; -}; + bit_rate: usb.types.U32_Le, + stop_bits: StopBits, + parity: Parity, + data_bits: u8, -pub const Options = struct { - max_packet_size: u16, -}; + pub const init: @This() = .{ + .bit_rate = 115200, + .stop_bits = 0, + .parity = 0, + .data_bits = 8, + }; + }; -pub fn CdcClassDriver(options: Options) type { - return struct { - pub const Descriptor = extern struct { - 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, - - const Strings = struct { - itf_notifi: []const u8 = "", - itf_data: []const u8 = "", - }; + pub const Descriptor = extern struct { + 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, + + const Strings = struct { + itf_notifi: []const u8 = "", + itf_data: []const u8 = "", + }; - pub fn create( - alloc: *usb.DescriptorAllocator, - max_supported_packet_size: usb.types.Len, - strings: Strings, - ) @This() { - assert(options.max_packet_size <= max_supported_packet_size); - const itf_notifi = alloc.next_itf(); - const itf_data = alloc.next_itf(); - return .{ + pub fn create( + alloc: *usb.DescriptorAllocator, + max_supported_packet_size: usb.types.Len, + strings: Strings, + ) usb.DescriptorCreateResult(@This()) { + const itf_notifi = alloc.next_itf(); + const itf_data = alloc.next_itf(); + return .{ + .descriptor = .{ .itf_assoc = .{ .first_interface = itf_notifi, .interface_count = 2, @@ -119,7 +114,7 @@ pub fn CdcClassDriver(options: Options) type { .master_interface = itf_notifi, .slave_interface_0 = itf_data, }, - .ep_notifi = .interrupt(alloc.next_ep(.In), 16, 16), + .ep_notifi = .interrupt(alloc.next_ep(.In), 8, 16), .itf_data = .{ .interface_number = itf_data, .alternate_setting = 0, @@ -129,140 +124,144 @@ pub fn CdcClassDriver(options: Options) type { }, .ep_out = .bulk(alloc.next_ep(.Out), max_supported_packet_size), .ep_in = .bulk(alloc.next_ep(.In), max_supported_packet_size), - }; - } - }; - - pub const handlers: usb.DriverHadlers(@This()) = .{ - .ep_notifi = on_notifi_ready, - .ep_out = on_rx, - .ep_in = on_tx_ready, - }; - - device: *usb.DeviceInterface, - descriptor: *const Descriptor, - line_coding: LineCoding align(4), - notifi_ready: std.atomic.Value(bool), - - rx_data: [options.max_packet_size]u8, - rx_seek: usb.types.Len, - rx_end: usb.types.Len, - rx_ready: std.atomic.Value(bool), - - tx_data: [options.max_packet_size]u8, - tx_end: usb.types.Len, - tx_ready: std.atomic.Value(bool), - - pub fn available(self: *@This()) usb.types.Len { - return self.rx_end - self.rx_seek; + }, + .alloc_bytes = 2 * max_supported_packet_size, + }; } + }; - pub fn read(self: *@This(), dst: []u8) usize { - 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.available() > 0) return len; - - // request more data - if (self.rx_ready.load(.seq_cst)) { - const ep_out = self.descriptor.ep_out.endpoint.num; - self.rx_ready.store(false, .seq_cst); - 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); - } + pub const handlers: usb.DriverHadlers(@This()) = .{ + .ep_notifi = on_notifi_ready, + .ep_out = on_rx, + .ep_in = on_tx_ready, + }; - return len; + device: *usb.DeviceInterface, + descriptor: *const Descriptor, + line_coding: LineCoding align(4), + notifi_ready: std.atomic.Value(bool), + + rx_data: []u8, + rx_seek: usb.types.Len, + rx_end: usb.types.Len, + rx_ready: std.atomic.Value(bool), + + tx_data: []u8, + tx_end: usb.types.Len, + tx_ready: std.atomic.Value(bool), + + pub fn available(self: *@This()) usb.types.Len { + return self.rx_end - self.rx_seek; + } + + pub fn read(self: *@This(), dst: []u8) usize { + 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.available() > 0) return len; + + // request more data + if (self.rx_ready.load(.seq_cst)) { + const ep_out = self.descriptor.ep_out.endpoint.num; + self.rx_ready.store(false, .seq_cst); + self.rx_end = self.device.ep_readv(ep_out, &.{self.rx_data}); + self.rx_seek = 0; + self.device.ep_listen(ep_out, @intCast(self.rx_data.len)); } - pub fn write(self: *@This(), data: []const u8) usize { - const len = @min(self.tx_data.len - self.tx_end, data.len); + return len; + } - @memcpy(self.tx_data[self.tx_end..][0..len], data[0..len]); - self.tx_end += @intCast(len); + pub fn write(self: *@This(), data: []const u8) usize { + const len = @min(self.tx_data.len - self.tx_end, data.len); - if (self.tx_end == self.tx_data.len) - _ = self.flush(); + @memcpy(self.tx_data[self.tx_end..][0..len], data[0..len]); + self.tx_end += @intCast(len); - return len; - } - - /// Returns true if flush operation succeded. - pub fn flush(self: *@This()) bool { - if (self.tx_end == 0) - return true; + if (self.tx_end == self.tx_data.len) + _ = self.flush(); - if (!self.tx_ready.load(.seq_cst)) - return false; - self.tx_ready.store(false, .seq_cst); + return len; + } - assert(self.tx_end == self.device.ep_writev( - self.descriptor.ep_in.endpoint.num, - &.{self.tx_data[0..self.tx_end]}, - )); - self.tx_end = 0; + /// Returns true if flush operation succeded. + pub fn flush(self: *@This()) bool { + if (self.tx_end == 0) return true; - } - - pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { - self.* = .{ - .device = device, - .descriptor = desc, - .line_coding = .{ - .bit_rate = .from(115200), - .stop_bits = .@"1", - .parity = .none, - .data_bits = 8, - }, - .notifi_ready = .init(true), - - .rx_data = undefined, - .rx_seek = 0, - .rx_end = 0, - .rx_ready = .init(false), - - .tx_data = undefined, - .tx_end = 0, - .tx_ready = .init(true), - }; - device.ep_listen(desc.ep_out.endpoint.num, options.max_packet_size); - } - - pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { - const mgmt_request: ManagementRequestType = @enumFromInt(setup.request); - 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 - .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: EP_Num) void { - if (self.rx_ready.load(.seq_cst)) - log.warn("{s}({t}) called before buffer was consumed", .{ "on_rx", ep_num }); - self.rx_ready.store(true, .seq_cst); - } - - pub fn on_tx_ready(self: *@This(), ep_num: EP_Num) void { - if (self.tx_ready.load(.seq_cst)) - log.warn("{s}({t}) called before buffer was consumed", .{ "on_tx_ready", ep_num }); - self.tx_ready.store(true, .seq_cst); - } - pub fn on_notifi_ready(self: *@This(), ep_num: EP_Num) void { - if (self.notifi_ready.load(.seq_cst)) - log.warn("{s}({t}) called before buffer was consumed", .{ "on_notifi_ready", ep_num }); - self.notifi_ready.store(true, .seq_cst); - } - }; -} + if (!self.tx_ready.load(.seq_cst)) + return false; + self.tx_ready.store(false, .seq_cst); + + assert(self.tx_end == self.device.ep_writev( + self.descriptor.ep_in.endpoint.num, + &.{self.tx_data[0..self.tx_end]}, + )); + self.tx_end = 0; + return true; + } + + pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface, data: []u8) void { + const len_half = @divExact(data.len, 2); + assert(len_half == desc.ep_in.max_packet_size.into()); + assert(len_half == desc.ep_out.max_packet_size.into()); + self.* = .{ + .device = device, + .descriptor = desc, + .line_coding = .{ + .bit_rate = .from(115200), + .stop_bits = .@"1", + .parity = .none, + .data_bits = 8, + }, + .notifi_ready = .init(true), + + .rx_data = data[0..len_half], + .rx_seek = 0, + .rx_end = 0, + .rx_ready = .init(false), + + .tx_data = data[len_half..], + .tx_end = 0, + .tx_ready = .init(true), + }; + device.ep_listen(desc.ep_out.endpoint.num, @intCast(self.rx_data.len)); + } + + pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { + const mgmt_request: ManagementRequestType = @enumFromInt(setup.request); + 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 + .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: EP_Num) void { + if (self.rx_ready.load(.seq_cst)) + log.warn("{s}({t}) called before buffer was consumed", .{ "on_rx", ep_num }); + self.rx_ready.store(true, .seq_cst); + } + + pub fn on_tx_ready(self: *@This(), ep_num: EP_Num) void { + if (self.tx_ready.load(.seq_cst)) + log.warn("{s}({t}) called before buffer was consumed", .{ "on_tx_ready", ep_num }); + self.tx_ready.store(true, .seq_cst); + } + + pub fn on_notifi_ready(self: *@This(), ep_num: EP_Num) void { + if (self.notifi_ready.load(.seq_cst)) + log.warn("{s}({t}) called before buffer was consumed", .{ "on_notifi_ready", ep_num }); + self.notifi_ready.store(true, .seq_cst); + } +}; diff --git a/core/src/core/usb/drivers/example.zig b/core/src/core/usb/drivers/example.zig index 9a18574a2..2ddfc5284 100644 --- a/core/src/core/usb/drivers/example.zig +++ b/core/src/core/usb/drivers/example.zig @@ -1,5 +1,6 @@ const std = @import("std"); const usb = @import("../../usb.zig"); +const assert = std.debug.assert; const EP_Num = usb.types.Endpoint.Num; const log = std.log.scoped(.usb_echo); @@ -13,28 +14,33 @@ pub const EchoExampleDriver = struct { 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. - /// Endpoint numbers are allocated automatically, this function should - /// use placeholder .ep0 values on all endpoints. + /// This function is used during descriptor creation. Endpoint and + /// interface numbers are allocated through the `alloc` parameter. /// Third argument can be of any type, it's passed by the user when - /// creating the device controller type. Passing arguments this way - /// is preffered to making the whole driver generic. + /// creating the device controller type. If multiple instances of + /// a driver are used, this function is called for each, with different + /// arguments. Passing arguments through this function is preffered to + /// making the whole driver generic. pub fn create( alloc: *usb.DescriptorAllocator, max_supported_packet_size: usb.types.Len, itf_string: []const u8, - ) @This() { + ) usb.DescriptorCreateResult(@This()) { return .{ - .itf = .{ - .interface_number = alloc.next_itf(), - .alternate_setting = 0, - .num_endpoints = 2, - .interface_triple = .vendor_specific, - .interface_s = alloc.string(itf_string), + .descriptor = .{ + .itf = .{ + .interface_number = alloc.next_itf(), + .alternate_setting = 0, + .num_endpoints = 2, + .interface_triple = .vendor_specific, + .interface_s = alloc.string(itf_string), + }, + .ep_out = .bulk(alloc.next_ep(.Out), max_supported_packet_size), + .ep_in = .bulk(alloc.next_ep(.In), max_supported_packet_size), }, - .ep_out = .bulk(alloc.next_ep(.Out), max_supported_packet_size), - .ep_in = .bulk(alloc.next_ep(.In), max_supported_packet_size), + // Buffers whose length is only known after creating the + // descriptor can be allocated at this stage. + .alloc_bytes = max_supported_packet_size, }; } }; @@ -49,14 +55,18 @@ pub const EchoExampleDriver = struct { device: *usb.DeviceInterface, descriptor: *const Descriptor, + packet_buffer: []u8, tx_ready: std.atomic.Value(bool), /// This function is called when the host chooses a configuration /// that contains this driver. `self` points to undefined memory. - pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { + /// `data` is of the length specified in `Descriptor.create()`. + pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface, data: []u8) void { + assert(data.len == desc.ep_in.max_packet_size.into()); self.* = .{ .device = device, .descriptor = desc, + .packet_buffer = data, .tx_ready = .init(false), }; device.ep_listen( diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index dcff16324..bd65ab5b9 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -1,5 +1,6 @@ const std = @import("std"); const usb = @import("../../usb.zig"); +const assert = std.debug.assert; const EP_Num = usb.types.Endpoint.Num; const log = std.log.scoped(.usb_hid); @@ -23,8 +24,8 @@ pub fn HidClassDriver(report_descriptor: anytype) type { alloc: *usb.DescriptorAllocator, max_supported_packet_size: usb.types.Len, options: Options, - ) @This() { - return .{ + ) usb.DescriptorCreateResult(@This()) { + return .{ .descriptor = .{ .interface = .{ .interface_number = alloc.next_itf(), .alternate_setting = 0, @@ -39,7 +40,7 @@ pub fn HidClassDriver(report_descriptor: anytype) type { .hid = hid_descriptor, .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), - }; + } }; } }; @@ -58,7 +59,8 @@ pub fn HidClassDriver(report_descriptor: anytype) type { device: *usb.DeviceInterface, descriptor: *const Descriptor, - pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { + pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface, data: []const u8) void { + assert(data.len == 0); self.* = .{ .device = device, .descriptor = desc, diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 92952baa4..fbc96c319 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -6,7 +6,7 @@ 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 USB_Serial = usb.drivers.cdc.CdcClassDriver; var usb_device: USB_Device = undefined; diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index e87f58038..a6b6e2207 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -445,8 +445,8 @@ pub fn ResetDriver(bootsel_activity_led: ?u5, interface_disable_mask: u32) type alloc: *usb.DescriptorAllocator, _: usb.types.Len, interface_str: []const u8, - ) @This() { - return .{ .reset_interface = .{ + ) usb.DescriptorCreateResult(@This()) { + return .{ .descriptor = .{ .reset_interface = .{ .interface_number = alloc.next_itf(), .alternate_setting = 0, .num_endpoints = 0, @@ -456,11 +456,12 @@ pub fn ResetDriver(bootsel_activity_led: ?u5, interface_disable_mask: u32) type @enumFromInt(0x01), ), .interface_s = alloc.string(interface_str), - } }; + } } }; } }; - pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface) void { + pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface, data: []u8) void { + assert(data.len == 0); _ = desc; _ = device; self.* = .{}; From bf978f5ff4214a92ae476b92badba15b4a46e9a5 Mon Sep 17 00:00:00 2001 From: Piotr Fila Date: Sat, 24 Jan 2026 02:57:34 +0100 Subject: [PATCH 50/50] initial HID driver --- core/src/core/usb/drivers/hid.zig | 111 +++++++++++++++----- examples/raspberrypi/rp2xxx/src/usb_hid.zig | 26 +++-- 2 files changed, 104 insertions(+), 33 deletions(-) diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index bd65ab5b9..b144db784 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -4,78 +4,131 @@ const assert = std.debug.assert; const EP_Num = usb.types.Endpoint.Num; const log = std.log.scoped(.usb_hid); -pub fn HidClassDriver(report_descriptor: anytype) type { +pub const KeyboardOptions = struct { + boot_protocol: bool = true, +}; + +pub fn Keyboard(options: KeyboardOptions) type { return struct { + pub const report_descriptor = usb.descriptor.hid.ReportDescriptorKeyboard; + + pub const Modifiers = packed struct(u8) { + lctrl: bool, + lshift: bool, + lalt: bool, + lgui: bool, + rctrl: bool, + rshift: bool, + ralt: bool, + rgui: bool, + + pub const none: @This() = @bitCast(@as(u8, 0)); + }; + + pub const Code = enum(u8) { + // Codes taken from https://gist.github.com/mildsunrise/4e231346e2078f440969cdefb6d4caa3 + // zig fmt: off + reserved = 0x00, error_roll_over, post_fail, error_undefined, + a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, + @"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"0", + enter, escape, delete, tab, space, + @"-", @"=", @"[", @"]", @"\\", @"non_us_#", @";", @"'", @"`", @",", @".", @"/", + caps_lock, + f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, + print_screen, scroll_lock, pause, insert, home, page_up, delete_forward, end, page_down, + right_arrow, left_arrow, down_arrow, up_arrow, num_lock, + kpad_div, kpad_mul, kpad_sub, kpad_add, kpad_enter, + kpad_1, kpad_2, kpad_3, kpad_4, kpad_5, kpad_6, kpad_7, kpad_8, kpad_9, kpad_0, + kpad_delete, @"non_us_\\", application, power, @"kpad_=", + f13, f14, f15, f16, f17, f18, f19, f20, f21, f22, f23, f24, + lctrl = 224, lshift, lalt, lgui, rctrl, rshift, ralt, rgui, + // zig fmt: on + _, + }; + + pub const Report = extern struct { + modifiers: Modifiers, + reserved: u8 = 0, + keys: [6]Code, + + comptime { + assert(@sizeOf(@This()) == 8); + } + + pub const empty: @This() = .{ .modifiers = .none, .keys = @splat(.reserved) }; + }; + pub const Descriptor = extern struct { const desc = usb.descriptor; interface: desc.Interface, hid: desc.hid.HID, - ep_out: desc.Endpoint, ep_in: desc.Endpoint, pub const Options = struct { itf_string: []const u8 = "", - boot_protocol: bool, poll_interval: u8, }; pub fn create( alloc: *usb.DescriptorAllocator, max_supported_packet_size: usb.types.Len, - options: Options, + desc_options: Options, ) usb.DescriptorCreateResult(@This()) { + _ = max_supported_packet_size; return .{ .descriptor = .{ .interface = .{ .interface_number = alloc.next_itf(), .alternate_setting = 0, - .num_endpoints = 2, + .num_endpoints = 1, .interface_triple = .from( .HID, if (options.boot_protocol) .Boot else .Unspecified, if (options.boot_protocol) .Boot else .None, ), - .interface_s = alloc.string(options.itf_string), + .interface_s = alloc.string(desc_options.itf_string), + }, + .hid = .{ + .bcd_hid = .from(0x0111), + .country_code = .NotSupported, + .num_descriptors = 1, + .report_length = .from(@sizeOf(@TypeOf(report_descriptor))), }, - .hid = hid_descriptor, - .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), + .ep_in = .interrupt( + alloc.next_ep(.In), + if (options.boot_protocol) 8 else unreachable, + desc_options.poll_interval, + ), } }; } }; - const hid_descriptor: usb.descriptor.hid.HID = .{ - .bcd_hid = .from(0x0111), - .country_code = .NotSupported, - .num_descriptors = 1, - .report_length = .from(@sizeOf(@TypeOf(report_descriptor))), - }; - pub const handlers: usb.DriverHadlers(@This()) = .{ - .ep_out = on_rx, .ep_in = on_tx_ready, }; device: *usb.DeviceInterface, descriptor: *const Descriptor, + tx_ready: std.atomic.Value(bool), pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface, data: []const u8) void { + log.debug("Keyboard init", .{}); assert(data.len == 0); self.* = .{ .device = device, .descriptor = desc, + .tx_ready = .init(true), }; } pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { - _ = self; switch (setup.request_type.type) { .Standard => { 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 std.mem.asBytes(&hid_descriptor) + return std.mem.asBytes(&self.descriptor.hid) else if (request_code == .GetDescriptor and hid_desc_type == .Report) return std.mem.asBytes(&report_descriptor); }, @@ -122,13 +175,23 @@ pub fn HidClassDriver(report_descriptor: anytype) type { } pub fn on_tx_ready(self: *@This(), ep: EP_Num) void { - _ = self; - _ = ep; + log.debug("tx ready ({t})", .{ep}); + self.tx_ready.store(true, .seq_cst); } - pub fn on_rx(self: *@This(), ep: EP_Num) void { - _ = self; - _ = ep; + pub fn send_report(self: *@This(), report: *const Report) bool { + if (!self.tx_ready.load(.seq_cst)) return false; + + self.tx_ready.store(false, .seq_cst); + + const len = self.device.ep_writev( + self.descriptor.ep_in.endpoint.num, + &.{std.mem.asBytes(report)}, + ); + + log.debug("sent report {} {any}", .{ len, report }); + + return true; } }; } diff --git a/examples/raspberrypi/rp2xxx/src/usb_hid.zig b/examples/raspberrypi/rp2xxx/src/usb_hid.zig index fbd3cd1c4..3dc7ec1e2 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -6,9 +6,7 @@ const time = rp2xxx.time; const gpio = rp2xxx.gpio; const usb = microzig.core.usb; const USB_Device = rp2xxx.usb.Polled(.{}); -const HID_Driver = usb.drivers.hid.HidClassDriver( - usb.descriptor.hid.ReportDescriptorKeyboard, -); +const Keyboard = usb.drivers.hid.Keyboard(.{}); var usb_device: USB_Device = undefined; @@ -23,10 +21,10 @@ var usb_controller: usb.DeviceController(.{ .configurations = &.{.{ .attributes = .{ .self_powered = false }, .max_current_ma = 50, - .Drivers = struct { hid: HID_Driver, reset: rp2xxx.usb.ResetDriver(null, 0) }, + .Drivers = struct { keyboard: Keyboard, reset: rp2xxx.usb.ResetDriver(null, 0) }, }}, }, .{.{ - .hid = .{ .itf_string = "Boot Keyboard", .boot_protocol = true, .poll_interval = 0 }, + .keyboard = .{ .itf_string = "Boot Keyboard", .poll_interval = 1 }, .reset = "", }}) = .init; @@ -41,7 +39,7 @@ pub const microzig_options = microzig.Options{ .log_scope_levels = &.{ .{ .scope = .usb_dev, .level = .warn }, .{ .scope = .usb_ctrl, .level = .warn }, - .{ .scope = .usb_hid, .level = .warn }, + // .{ .scope = .usb_hid, .level = .warn }, }, .logFn = rp2xxx.uart.log, }; @@ -67,16 +65,26 @@ pub fn main() !void { var old: u64 = time.get_time_since_boot().to_us(); var new: u64 = 0; + const message: []const Keyboard.Code = &.{ .h, .e, .l, .l, .o, .space, .w, .o, .r, .l, .d, .caps_lock, .enter }; + var idx: usize = 0; + while (true) { // You can now poll for USB events usb_device.poll(&usb_controller); if (usb_controller.drivers()) |drivers| { - _ = drivers; // TODO - new = time.get_time_since_boot().to_us(); - if (new - old > 500000) { + const time_since_last = new - old; + if (time_since_last < 2_000_000) { + idx += @intFromBool(if (idx & 1 == 0 and idx < 2 * message.len) + drivers.keyboard.send_report( + &.{ .modifiers = .none, .keys = .{message[@intCast(idx / 2)]} ++ .{.reserved} ** 5 }, + ) + else + drivers.keyboard.send_report(&.empty)); + } else { old = new; + idx = 0; pins.led.toggle(); } }