diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index e1cd56409..4d1446b48 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -20,15 +20,16 @@ pub const StructFieldAttributes = struct { default_value_ptr: ?*const anyopaque = null, }; -/// Meant to make transition to zig 0.16 easier +/// Helper to create a struct, wrapping around @Type, 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, + layout: std.builtin.Type.ContainerLayout, + BackingInt: ?type, + field_names: []const [:0]const u8, + field_types: *const [field_names.len]type, + field_attrs: *const [field_names.len]StructFieldAttributes, ) type { - comptime var fields: []const std.builtin.Type.StructField = &.{}; + var fields: []const std.builtin.Type.StructField = &.{}; + // Iterate over the names, field types, and attributes, creating a new struct field entry for (field_names, field_types, field_attrs) |n, T, a| { fields = fields ++ &[1]std.builtin.Type.StructField{.{ .name = n, @@ -47,6 +48,7 @@ pub fn Struct( } }); } +// What does this do? It lets you iterate through interfaces and endpoints? pub const DescriptorAllocator = struct { next_ep_num: [2]u8, next_itf_num: u8, @@ -96,6 +98,7 @@ pub const DescriptorAllocator = struct { } }; +/// Wraps a Descriptor type. Returned by the `create` method of the Descriptor. pub fn DescriptorCreateResult(Descriptor: type) type { return struct { descriptor: Descriptor, @@ -108,7 +111,7 @@ pub fn DescriptorCreateResult(Descriptor: type) type { } /// USB Device interface -/// Any device implementation used with DeviceController must implement those functions +/// Any device implementation used with DeviceController must implement these functions pub const DeviceInterface = struct { pub const VTable = struct { ep_writev: *const fn (*DeviceInterface, types.Endpoint.Num, []const []const u8) types.Len, @@ -191,16 +194,24 @@ pub const Config = struct { max_current_ma: u9, Drivers: type, + /// Generate A struct with a field for each field in Drivers, where the type is the third + /// arg of the Drivers' Descriptor's 'create' method. 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| { + // Collect field names field_names[i] = fld.name; + // Collect the type info for the Descriptor.create function parameter const params = @typeInfo(@TypeOf(fld.type.Descriptor.create)).@"fn".params; + // Ensure it takes 3 parameters assert(params.len == 3); + // The first must be a DescriptorAllocator assert(params[0].type == *DescriptorAllocator); + // The second is usb.types.Len assert(params[1].type == types.Len); + // And save the type of the third field_types[i] = params[2].type.?; } return Struct(.auto, null, &field_names, &field_types, &@splat(.{})); @@ -251,11 +262,14 @@ pub fn validate_controller(T: type) void { /// USB device controller /// -/// This code handles usb enumeration and configuration and routes packets to drivers. +/// Responds to host requests and dispatches to the appropriate drivers. +/// When this type is build (at comptime), it builds descriptor and handler tables based on the +/// provided config. pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { std.debug.assert(config.configurations.len == 1); return struct { + // We only support one configuration const config0 = config.configurations[0]; const driver_fields = @typeInfo(config0.Drivers).@"struct".fields; const DriverEnum = std.meta.FieldEnum(config0.Drivers); @@ -298,14 +312,18 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { var driver_alloc_attrs: []const StructFieldAttributes = &.{}; for (driver_fields, 0..) |drv, drv_id| { + // Get descriptor type for the current driver const Descriptors = drv.type.Descriptor; + // Call the create method on the descriptor, which wraps it with the allow stuff. const result = Descriptors.create(&alloc, max_psize, @field(driver_args[0], drv.name)); + // Get the descriptor instance from the result. const descriptors = result.descriptor; + // If the driver requests memory, then collect the names, types, and attrs if (result.alloc_bytes) |len| { - driver_alloc_names = driver_alloc_names ++ &[1][:0]const u8{drv.name}; - driver_alloc_types = driver_alloc_types ++ &[1]type{[len]u8}; - driver_alloc_attrs = driver_alloc_attrs ++ &[1]StructFieldAttributes{.{ .@"align" = result.alloc_align }}; + driver_alloc_names = driver_alloc_names ++ &[_][:0]const u8{drv.name}; + driver_alloc_types = driver_alloc_types ++ &[_]type{[len]u8}; + driver_alloc_attrs = driver_alloc_attrs ++ &[_]StructFieldAttributes{.{ .@"align" = result.alloc_align }}; } else { assert(result.alloc_align == null); } @@ -313,9 +331,14 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { assert(@alignOf(Descriptors) == 1); size += @sizeOf(Descriptors); + // Collect handler types, names, and drivers, to later be bound to the appropriate + // endpoints for (@typeInfo(Descriptors).@"struct".fields, 0..) |fld, desc_num| { const desc = @field(descriptors, fld.name); + // Determine which interface numbers this driver owns. If it is an + // InterfaceAssociation, then use the interface count. If it is an Interface, + // then the driver owns just that one interface. if (desc_num == 0) { const itf_start, const itf_count = switch (fld.type) { descriptor.InterfaceAssociation => .{ desc.first_interface, desc.interface_count }, @@ -329,10 +352,11 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { }; if (itf_start != itf_handlers.len) @compileError("interface numbering mismatch"); - itf_handlers = itf_handlers ++ &[1]DriverEnum{@field(DriverEnum, drv.name)} ** itf_count; + itf_handlers = itf_handlers ++ &[_]DriverEnum{@field(DriverEnum, drv.name)} ** itf_count; } switch (fld.type) { + // Register handler for endpoints descriptor.Endpoint => { const ep_dir = @intFromEnum(desc.endpoint.dir); const ep_num = @intFromEnum(desc.endpoint.num); @@ -347,6 +371,7 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { ep_handler_drivers[ep_dir][ep_num] = drv_id; ep_handler_names[ep_dir][ep_num] = fld.name; }, + // Interface association must be first descriptor.InterfaceAssociation => assert(desc_num == 0), else => {}, } @@ -356,9 +381,12 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { field_attrs[drv_id] = .{ .default_value_ptr = &descriptors }; } + // Finally, bind the handler functions based on the data collected above. const Tuple = std.meta.Tuple; + // Create a tuple with the appropriate types const ep_handlers_types: [2]type = .{ Tuple(&ep_handler_types[0]), Tuple(&ep_handler_types[1]) }; var ep_handlers: Tuple(&ep_handlers_types) = undefined; + // Iterate over all IN and OUT endpoints and bind the handler for any that are set. 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) @@ -558,6 +586,7 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type { } } + // Return the appropriate descriptor type as determined by the top 8 bits of the value. fn get_descriptor(value: u16) ?[]const u8 { const asBytes = std.mem.asBytes; const desc_type: descriptor.Type = @enumFromInt(value >> 8); diff --git a/core/src/core/usb/drivers/CDC.zig b/core/src/core/usb/drivers/CDC.zig index d48a75ec9..e9ffe1c68 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 log = std.log.scoped(.usb_cdc); +/// CDC PSTN Subclass Management Element Requests pub const ManagementRequestType = enum(u8) { SetCommFeature = 0x02, GetCommFeature = 0x03, @@ -27,6 +28,7 @@ pub const ManagementRequestType = enum(u8) { _, }; +/// Line coding structure for SetLineCoding/GetLineCoding requests pub const LineCoding = extern struct { pub const StopBits = enum(u8) { @"1" = 0, @"1.5" = 1, @"2" = 2, _ }; pub const Parity = enum(u8) { @@ -44,13 +46,14 @@ pub const LineCoding = extern struct { data_bits: u8, pub const init: @This() = .{ - .bit_rate = 115200, - .stop_bits = 0, - .parity = 0, + .bit_rate = .from(115200), + .stop_bits = .@"1", + .parity = .none, .data_bits = 8, }; }; +// This struct bundles all the descriptors CDC needs into one Configuration. pub const Descriptor = extern struct { const desc = usb.descriptor; @@ -70,6 +73,11 @@ pub const Descriptor = extern struct { itf_data: []const u8 = "", }; + /// 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. If multiple instances of a driver + /// are used, this function is called for each, with different arguments. Passing arguments + /// through this function is preferred to making the whole driver generic. pub fn create( alloc: *usb.DescriptorAllocator, max_supported_packet_size: usb.types.Len, @@ -130,6 +138,8 @@ pub const Descriptor = extern struct { } }; +// These field names are matched (at comptime) to the field names in the descriptor returned from +// `create` when binding the endpoints. pub const handlers: usb.DriverHandlers(@This()) = .{ .ep_notifi = on_notifi_ready, .ep_out = on_rx, @@ -202,6 +212,7 @@ pub fn flush(self: *@This()) bool { return true; } +// Called when the host selects a configuration. 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()); @@ -209,14 +220,10 @@ pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterfac self.* = .{ .device = device, .descriptor = desc, - .line_coding = .{ - .bit_rate = .from(115200), - .stop_bits = .@"1", - .parity = .none, - .data_bits = 8, - }, + .line_coding = .init, .notifi_ready = .init(true), + // `init` provides a data buffer, which we split in half for rx and tx data. .rx_data = data[0..len_half], .rx_seek = 0, .rx_end = 0, @@ -229,6 +236,8 @@ pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterfac device.ep_listen(desc.ep_out.endpoint.num, @intCast(self.rx_data.len)); } +/// Handle class-specific SETUP requests where recipient=Interface +/// Called by DeviceController when the interface number matches this driver. 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() }); diff --git a/core/src/core/usb/drivers/EchoExample.zig b/core/src/core/usb/drivers/EchoExample.zig index 6105bd96f..51c40c9d0 100644 --- a/core/src/core/usb/drivers/EchoExample.zig +++ b/core/src/core/usb/drivers/EchoExample.zig @@ -12,13 +12,11 @@ pub const Descriptor = extern struct { ep_out: desc.Endpoint, ep_in: desc.Endpoint, - /// 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. 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. + /// 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. 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, @@ -43,9 +41,9 @@ pub const Descriptor = extern 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. +/// This is a mapping from endpoint descriptor field names to handler function names. +/// 'in' and 'out' are from the perspective of the host, so, usb devices send data on 'in' endpoints +/// and receive on 'out' endpoints. pub const handlers: usb.DriverHandlers(@This()) = .{ .ep_in = on_tx_ready, .ep_out = on_rx, @@ -56,9 +54,8 @@ 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. -/// `data` is of the length specified in `Descriptor.create()`. +/// This function is called when the host chooses a configuration that contains this driver. `self` +/// points to undefined memory. `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.* = .{ @@ -77,19 +74,21 @@ pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterfac /// Data returned by this function is sent on endpoint 0. pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { _ = self; - _ = setup; + log.debug("setup: {x}, {}, {}", .{ setup.request, setup.length.into(), setup.value.into() }); return usb.ack; } +/// Transmit handler callback. /// 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. +/// 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 { log.info("tx ready ({t})", .{ep_tx}); // Mark transmission as available self.tx_ready.store(true, .seq_cst); } +/// Receive handler callback. +/// Endpoint number is passed as an argument so that it does not need to be stored in the driver. 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 diff --git a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index a04f60788..da3f57c4b 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -5,11 +5,16 @@ const rp2xxx = microzig.hal; const time = rp2xxx.time; const gpio = rp2xxx.gpio; const usb = microzig.core.usb; +// Port-specific type which implements the DeviceInterface interface, used by the USB core to +// read from/write to the peripheral. const USB_Device = rp2xxx.usb.Polled(.{}); +// USB class driver const USB_Serial = usb.drivers.CDC; var usb_device: USB_Device = undefined; +// Generate a device controller with descriptor and handlers setup for CDC (USB_Serial) and +// picotool-controlled reset (ResetDriver). var usb_controller: usb.DeviceController(.{ .bcd_usb = USB_Device.max_supported_bcd_usb, .device_triple = .unspecified, @@ -71,6 +76,7 @@ pub fn main() !void { // You can now poll for USB events usb_device.poll(&usb_controller); + // Ensure that the host as finished enumerating our USB device if (usb_controller.drivers()) |drivers| { new = time.get_time_since_boot().to_us(); if (new - old > 500000) { @@ -93,8 +99,9 @@ pub fn main() !void { 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 +/// 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 { var tx = std.fmt.bufPrint(&usb_tx_buff, fmt, args) catch &.{}; @@ -109,11 +116,10 @@ pub fn usb_cdc_write(serial: *USB_Serial, comptime fmt: []const u8, args: anytyp 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: *USB_Serial, -) []const u8 { +/// 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: *USB_Serial) []const u8 { var rx_len: usize = 0; while (true) { diff --git a/port/raspberrypi/rp2xxx/src/hal/usb.zig b/port/raspberrypi/rp2xxx/src/hal/usb.zig index 24c7afc2c..31747a31a 100644 --- a/port/raspberrypi/rp2xxx/src/hal/usb.zig +++ b/port/raspberrypi/rp2xxx/src/hal/usb.zig @@ -26,8 +26,10 @@ const HardwareEndpointData = struct { }; const rp2xxx_buffers = struct { - // Address 0x100-0xfff (3840 bytes) can be used for data buffers - const USB_DPRAM_DATA_BUFFER_BASE = 0x50100100; + // Address 0x100-0xfff (3840 bytes) can be used for data buffers. + // The first 0x100 bytes are registers (last one at offset 0xfc), the rest is available for + // endpoint data buffers. + const USB_DPRAM_DATA_BUFFER_BASE = @intFromPtr(peripherals.USB_DPRAM) + 0x100; const CTRL_EP_BUFFER_SIZE = 64; @@ -66,8 +68,8 @@ fn PerEndpoint(T: type) type { const BufferControlMmio = microzig.mmio.Mmio(@TypeOf(peripherals.USB_DPRAM.EP0_IN_BUFFER_CONTROL).underlying_type); const buffer_control: *volatile [16]PerEndpoint(BufferControlMmio) = @ptrCast(&peripherals.USB_DPRAM.EP0_IN_BUFFER_CONTROL); -const EndpointControlMimo = microzig.mmio.Mmio(@TypeOf(peripherals.USB_DPRAM.EP1_IN_CONTROL).underlying_type); -const endpoint_control: *volatile [15]PerEndpoint(EndpointControlMimo) = @ptrCast(&peripherals.USB_DPRAM.EP1_IN_CONTROL); +const EndpointControlMmio = microzig.mmio.Mmio(@TypeOf(peripherals.USB_DPRAM.EP1_IN_CONTROL).underlying_type); +const endpoint_control: *volatile [15]PerEndpoint(EndpointControlMmio) = @ptrCast(&peripherals.USB_DPRAM.EP1_IN_CONTROL); // +++++++++++++++++++++++++++++++++++++++++++++++++ // Code @@ -102,6 +104,8 @@ pub fn Polled(config: Config) type { data_buffer: []align(64) u8, interface: usb.DeviceInterface, + /// Poll to see if the host has sent anything. Delegate to the appropriate handler in the + /// controller based on the interrupt field set. pub fn poll(self: *@This(), controller: anytype) void { comptime usb.validate_controller(@TypeOf(controller)); @@ -117,13 +121,13 @@ pub fn Polled(config: Config) type { // 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. + // The SVD exposes 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}); + log.debug("setup {any}", .{setup}); controller.on_setup_req(&self.interface, &setup); } @@ -239,8 +243,9 @@ pub fn Polled(config: Config) type { }; @memset(std.mem.asBytes(&self.endpoints), 0); - ep_open(&self.interface, &.control(.in(.ep0), max_supported_packet_size)); - ep_open(&self.interface, &.control(.out(.ep0), max_supported_packet_size)); + // Set up endpoints. + self.interface.ep_open(&.control(.in(.ep0), max_supported_packet_size)); + self.interface.ep_open(&.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.