diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index d5e04641b..e927d524d 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -1,9 +1,11 @@ const std = @import("std"); -const log = std.log.scoped(.usb); +const assert = std.debug.assert; +const log = std.log.scoped(.usb_ctrl); pub const descriptor = @import("usb/descriptor.zig"); pub const drivers = struct { pub const cdc = @import("usb/drivers/cdc.zig"); + pub const echo = @import("usb/drivers/example.zig"); pub const hid = @import("usb/drivers/hid.zig"); }; pub const types = @import("usb/types.zig"); @@ -11,37 +13,141 @@ 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, + 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 = &.{}, + }; + } + + 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; + } + + 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; + } +}; + +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 { 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, - 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, + ep_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); + } + + /// 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 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. - /// 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); + /// 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); } /// Opens an endpoint according to the descriptor. Note that if the endpoint /// 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. @@ -50,125 +156,266 @@ pub const DeviceInterface = struct { } }; +pub fn EndpointHandler(Driver: type) type { + return fn (*Driver, types.Endpoint.Num) void; +} + +pub fn DriverHadlers(Driver: type) type { + var field_names: []const [:0]const u8 = &.{}; + + const desc_fields = @typeInfo(Driver.Descriptor).@"struct".fields; + for (desc_fields) |fld| switch (fld.type) { + descriptor.Endpoint => field_names = field_names ++ .{fld.name}, + else => {}, + }; + + return Struct( + .auto, + null, + field_names, + &@splat(EndpointHandler(Driver)), + &@splat(.{}), + ); +} + 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, - debug: bool = false, + /// 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, + + 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 { + 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. -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 { const config0 = config.configurations[0]; 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; + + /// 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)); + + var alloc: DescriptorAllocator = .init(config.unique_endpoints); + + 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 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 = @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); + 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)); + 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 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); + + 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; + } + switch (fld.type) { - descriptor.Interface => { - num_interfaces += 1; - if (desc.interface_s != 0) - num_strings += 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 => switch (desc.endpoint.dir) { - .In => num_ep_in += 1, - .Out => num_ep_out += 1, - }, - 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 => {}, } } + field_names[drv_id] = drv.name; + field_types[drv_id] = Descriptors; + field_attrs[drv_id] = .{ .default_value_ptr = &descriptors }; } - const desc_cfg: descriptor.Configuration = .{ - .total_length = .from(size), - .num_interfaces = num_interfaces, - .configuration_value = config0.num, - .configuration_s = config0.configuration_s, - .attributes = config0.attributes, - .max_current = .from_ma(config0.max_current_ma), - }; + 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); + } + } - fields[0] = .{ - .name = "__configuration_descriptor", - .type = descriptor.Configuration, - .default_value_ptr = &desc_cfg, - .is_comptime = false, - .alignment = 1, + 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 .{ + .device_descriptor = desc_device, + .config_descriptor = extern struct { + first: descriptor.Configuration = .{ + .total_length = .from(size), + .num_interfaces = alloc.next_itf_num, + .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], + 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), }; + }; - break :blk @Type(.{ .@"struct" = .{ - .decls = &.{}, - .fields = &fields, - .is_tuple = false, - .layout = .auto, - } }){}; + 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, + .drv = descriptor_parse_result.drivers_ep, }; + const DriverAlloc = descriptor_parse_result.DriverAlloc; - /// 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, + tx_slice: ?[]const u8, /// Driver state driver_data: ?config0.Drivers, + /// + driver_alloc: DriverAlloc, /// Initial values pub const init: @This() = .{ - .new_address = null, + .new_address = 0, .cfg_num = 0, - .tx_slice = "", - .setup_packet = undefined, - .driver_last = null, + .tx_slice = null, .driver_data = null, + .driver_alloc = undefined, }; /// Returns a pointer to the drivers @@ -179,101 +426,69 @@ 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 => 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; - }; + log.debug("on_setup_req", .{}); - 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); - } - }, - }, - .Endpoint => {}, - else => {}, + 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 <= 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, 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; - } - - 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 { - device_itf.start_rx(.ep0, 0); + 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_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 + // SetAddress request, if there is one: + if (self.new_address != 0) { + device_itf.set_address(@intCast(self.new_address)); + self.new_address = 0; + } - if (self.driver_last) |drv| - self.driver_class_control(device_itf, drv, .Ack, &self.setup_packet); - } + if (self.tx_slice) |slice| { + if (slice.len > 0) { + const len = device_itf.ep_writev(.ep0, &.{slice}); + self.tx_slice = slice[len..]; } 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); + // 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; + + // 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); } - }, - 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 if (comptime ep == types.Endpoint.out(.ep0)) + log.warn("Unhandled packet on ep0 Out", .{}); + + if (comptime driver_opt) |driver| { + handler(&@field(self.driver_data.?, driver_fields[driver].name), ep.num); } } /// Called by the device implementation on bus reset. - pub fn on_bus_reset(self: *@This()) void { - if (config.debug) log.info("bus reset", .{}); + pub fn on_bus_reset(self: *@This(), device_itf: *DeviceInterface) void { + log.debug("on_bus_reset", .{}); + + self.process_set_config(device_itf, 0); // Reset our state. self.* = .init; @@ -281,124 +496,116 @@ 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 { - 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]); - } - - 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); - }, - }; - } - - 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 => { - self.new_address = @as(u8, @intCast(setup.value & 0xff)); - device_itf.start_tx(.ep0, ack); - if (config.debug) log.info(" SetAddress: {}", .{self.new_address.?}); - }, - .SetConfiguration => { - if (config.debug) log.info(" SetConfiguration", .{}); - if (self.cfg_num != setup.value) { - self.cfg_num = setup.value; - - 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.start_tx(.ep0, ack); + const request: types.SetupRequest = @enumFromInt(setup.request); + log.debug("Device setup: {any}", .{request}); + switch (request) { + .GetStatus => { + const attr = config_descriptor.first.attributes; + const status: types.DeviceStatus = comptime .create(attr.self_powered, false); + return std.mem.asBytes(&status); }, - .GetDescriptor => { - const descriptor_type = std.meta.intToEnum(descriptor.Type, setup.value >> 8) catch null; - if (descriptor_type) |dt| { - try self.process_get_descriptor(device_itf, setup, dt); + .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 >> 8)) |feat| { - switch (feat) { - .DeviceRemoteWakeup, .EndpointHalt => device_itf.start_tx(.ep0, ack), - // TODO: https://github.com/ZigEmbeddedGroup/microzig/issues/453 - .TestMode => {}, - } - } else |_| {} + else => { + log.warn("Unsupported standard request: {}", .{setup.request}); + return nak; }, } + return ack; + }, + else => |t| { + log.warn("Unhandled device setup request: {any}", .{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); + 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)).class_request(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); + 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 {any} descriptor {}", .{ desc_type, desc_idx }); + return switch (desc_type) { + .Device => asBytes(&device_descriptor), + .DeviceQualifier => asBytes(comptime &device_descriptor.qualifier()), + .Configuration => asBytes(&config_descriptor), + .String => if (desc_idx < string_descriptors.len) + string_descriptors[desc_idx].data + else { + log.warn( + "Descriptor index ({}) out of range ({})", + .{ desc_idx, string_descriptors.len }, + ); + return nak; }, - .String => { - if (config.debug) log.info(" String", .{}); - // String descriptor index is in bottom 8 bits of - // `value`. - const i: usize = @intCast(setup.value & 0xff); - 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, - ); - }, - .Interface => { - if (config.debug) log.info(" Interface", .{}); - }, - .Endpoint => { - if (config.debug) log.info(" Endpoint", .{}); - }, - .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); - }, - else => {}, - } + else => nak, + }; } - fn process_set_config(self: *@This(), device_itf: *DeviceInterface, _: u16) !void { - // TODO: we support just one config for now so ignore config index + 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 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 + 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); + } - inline for (@typeInfo(@TypeOf(cfg)).@"struct".fields) |fld| { - if (comptime fld.type == descriptor.Endpoint) - device_itf.endpoint_open(&@field(cfg, fld.name)); + @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| { + 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/descriptor.zig b/core/src/core/usb/descriptor.zig index 63777e064..b1daeb8ae 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, @@ -29,22 +30,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 { @@ -57,9 +42,9 @@ 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.Version, /// 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. @@ -77,17 +62,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.Version, /// 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. - 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.Version, /// Index of manufacturer name in string descriptor table. manufacturer_s: u8, /// Index of product name in string descriptor table. @@ -144,7 +129,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 @@ -167,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.U16Le, + 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() { @@ -221,10 +206,37 @@ 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, + + 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. @@ -245,12 +257,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, }; @@ -280,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(comptime objects: []const Object) @This() { + const data: []const u8 = ""; + 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), + num_descriptors: u8 = @intCast(objects.len), + }{}); + return .{ .data = header ++ data }; + } +}; diff --git a/core/src/core/usb/descriptor/cdc.zig b/core/src/core/usb/descriptor/cdc.zig index 0d9f8e5fc..1230b29cd 100644 --- a/core/src/core/usb/descriptor/cdc.zig +++ b/core/src/core/usb/descriptor/cdc.zig @@ -22,10 +22,21 @@ 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 { + 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, + reserved: 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/descriptor/hid.zig b/core/src/core/usb/descriptor/hid.zig index c3ce76afb..641a715e5 100644 --- a/core/src/core/usb/descriptor/hid.zig +++ b/core/src/core/usb/descriptor/hid.zig @@ -11,10 +11,11 @@ pub const RequestType = enum(u8) { SetReport = 0x09, SetIdle = 0x0a, SetProtocol = 0x0b, + _, }; /// USB HID descriptor -pub const Hid = extern struct { +pub const HID = extern struct { /// HID country codes pub const CountryCode = enum(u8) { NotSupported = 0, @@ -53,12 +54,14 @@ pub const Hid = extern struct { Us, Yugoslavia, TurkishF, + _, }; pub const Type = enum(u8) { - Hid = 0x21, + HID = 0x21, Report = 0x22, Physical = 0x23, + _, }; comptime { @@ -70,7 +73,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 +81,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 30b80ff7b..69bbf8f44 100644 --- a/core/src/core/usb/drivers/cdc.zig +++ b/core/src/core/usb/drivers/cdc.zig @@ -1,61 +1,88 @@ const std = @import("std"); const usb = @import("../../usb.zig"); -const descriptor = usb.descriptor; -const types = usb.types; +const assert = std.debug.assert; +const EP_Num = usb.types.Endpoint.Num; +const log = std.log.scoped(.usb_cdc); -const utilities = @import("../../../utilities.zig"); +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, + _, + }; -pub const ManagementRequestType = enum(u8) { - SetLineCoding = 0x20, - GetLineCoding = 0x21, - SetControlLineState = 0x22, - SendBreak = 0x23, -}; + 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 LineCoding = extern struct { - bit_rate: u32 align(1), - stop_bits: u8, - parity: u8, - data_bits: u8, - - 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 init: @This() = .{ + .bit_rate = 115200, + .stop_bits = 0, + .parity = 0, + .data_bits = 8, + }; }; -}; -pub const Options = struct { - max_packet_size: u16, -}; + 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 CdcClassDriver(options: Options) type { - const FIFO = utilities.CircularBuffer(u8, options.max_packet_size); - - 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, - - pub fn create( - first_interface: u8, - first_string: u8, - first_endpoint_in: u4, - first_endpoint_out: u4, - ) @This() { - const endpoint_notifi_size = 8; - 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 = first_interface, + .first_interface = itf_notifi, .interface_count = 2, .function_class = 2, .function_subclass = 2, @@ -63,156 +90,178 @@ 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, - .interface_subclass = 2, - .interface_protocol = 0, - .interface_s = first_string, + .interface_triple = .from(.CDC, .Abstract, .NoneRequired), + .interface_s = alloc.string(strings.itf_notifi), }, .cdc_header = .{ .bcd_cdc = .from(0x0120) }, .cdc_call_mgmt = .{ - .capabilities = 0, - .data_interface = first_interface + 1, + .capabilities = .none, + .data_interface = itf_data, }, - .cdc_acm = .{ .capabilities = 6 }, - .cdc_union = .{ - .master_interface = first_interface, - .slave_interface_0 = first_interface + 1, + .cdc_acm = .{ + .capabilities = .{ + .comm_feature = false, + .send_break = false, + // Line coding requests get sent regardless of this bit + .line_coding = true, + .network_connection = false, + }, }, - .ep_notifi = .{ - .endpoint = .in(@enumFromInt(first_endpoint_in)), - .attributes = .{ .transfer_type = .Interrupt, .usage = .data }, - .max_packet_size = .from(endpoint_notifi_size), - .interval = 16, + .cdc_union = .{ + .master_interface = itf_notifi, + .slave_interface_0 = itf_data, }, + .ep_notifi = .interrupt(alloc.next_ep(.In), 8, 16), .itf_data = .{ - .interface_number = first_interface + 1, + .interface_number = itf_data, .alternate_setting = 0, .num_endpoints = 2, - .interface_class = 10, - .interface_subclass = 0, - .interface_protocol = 0, - .interface_s = 0, - }, - .ep_out = .{ - .endpoint = .out(@enumFromInt(first_endpoint_out)), - .attributes = .{ .transfer_type = .Bulk, .usage = .data }, - .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 }, - .max_packet_size = .from(options.max_packet_size), - .interval = 0, + .interface_triple = .from(.CDC_Data, .Unused, .NoneRequired), + .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), + }, + .alloc_bytes = 2 * max_supported_packet_size, + }; + } + }; - device: *usb.DeviceInterface, - ep_notif: types.Endpoint.Num, - ep_in: types.Endpoint.Num, - ep_out: types.Endpoint.Num, - line_coding: LineCoding align(4), + pub const handlers: usb.DriverHadlers(@This()) = .{ + .ep_notifi = on_notifi_ready, + .ep_out = on_rx, + .ep_in = on_tx_ready, + }; - rx: FIFO = .empty, - tx: FIFO = .empty, + device: *usb.DeviceInterface, + descriptor: *const Descriptor, + line_coding: LineCoding align(4), + notifi_ready: std.atomic.Value(bool), - epin_buf: [options.max_packet_size]u8 = undefined, + rx_data: []u8, + rx_seek: usb.types.Len, + rx_end: usb.types.Len, + rx_ready: std.atomic.Value(bool), - pub fn available(self: *@This()) usize { - return self.rx.get_readable_len(); - } + 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 read_count = self.rx.read(dst); - self.prep_out_transaction(); - return read_count; + 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) []const u8 { - const write_count = @min(self.tx.get_writable_len(), data.len); + return len; + } - if (write_count > 0) { - self.tx.write_assume_capacity(data[0..write_count]); - } else { - return data[0..]; - } + pub fn write(self: *@This(), data: []const u8) usize { + const len = @min(self.tx_data.len - self.tx_end, data.len); - 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]); - return len; - } + 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.start_rx(self.ep_out, options.max_packet_size); - } - } + /// Returns true if flush operation succeded. + pub fn flush(self: *@This()) bool { + if (self.tx_end == 0) + return true; - pub fn init(desc: *const Descriptor, device: *usb.DeviceInterface) @This() { - 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, - }, - }; - } + if (!self.tx_ready.load(.seq_cst)) + return false; + self.tx_ready.store(false, .seq_cst); - 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 - .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 |_| {} + 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; + } - return usb.nak; - } + 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), - 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(); - } - - 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, &.{}); - } - } - } - } - }; -} + .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 new file mode 100644 index 000000000..2ddfc5284 --- /dev/null +++ b/core/src/core/usb/drivers/example.zig @@ -0,0 +1,117 @@ +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); + +/// 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 { + const desc = usb.descriptor; + + itf: desc.Interface, + 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. + pub fn create( + alloc: *usb.DescriptorAllocator, + max_supported_packet_size: usb.types.Len, + itf_string: []const u8, + ) usb.DescriptorCreateResult(@This()) { + return .{ + .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), + }, + // Buffers whose length is only known after creating the + // descriptor can be allocated at this stage. + .alloc_bytes = max_supported_packet_size, + }; + } + }; + + /// 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: usb.DriverHadlers(@This()) = .{ + .ep_in = on_tx_ready, + .ep_out = on_rx, + }; + + 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. + /// `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( + desc.ep_out.endpoint.num, + @intCast(desc.ep_out.max_packet_size.into()), + ); + } + + /// Used for interface configuration through endpoint 0. + /// 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; + return usb.ack; + } + + /// 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: EP_Num) void { + log.info("tx ready ({t})", .{ep_tx}); + // Mark transmission as available + 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 on {t}: {s}", .{ ep_rx, buf[0..len_rx] }); + // Check if we can transmit + + if (self.tx_ready.load(.seq_cst)) { + // Mark transmission as not available + self.tx_ready.store(false, .seq_cst); + // Send received packet + log.info("Sending {} bytes", .{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}); + } + // Listen for next packet + self.device.ep_listen(ep_rx, 64); + } +}; diff --git a/core/src/core/usb/drivers/hid.zig b/core/src/core/usb/drivers/hid.zig index a12f4f64e..b144db784 100644 --- a/core/src/core/usb/drivers/hid.zig +++ b/core/src/core/usb/drivers/hid.zig @@ -1,102 +1,154 @@ const std = @import("std"); const usb = @import("../../usb.zig"); -const descriptor = usb.descriptor; -const types = usb.types; +const assert = std.debug.assert; +const EP_Num = usb.types.Endpoint.Num; +const log = std.log.scoped(.usb_hid); -pub const Options = struct { - max_packet_size: u16, - boot_protocol: bool, - endpoint_interval: u8, +pub const KeyboardOptions = struct { + boot_protocol: bool = true, }; -pub fn HidClassDriver(options: Options, report_descriptor: anytype) type { +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 { - interface: descriptor.Interface, - hid: descriptor.hid.Hid, - ep_out: descriptor.Endpoint, - ep_in: descriptor.Endpoint, + const desc = usb.descriptor; + + interface: desc.Interface, + hid: desc.hid.HID, + ep_in: desc.Endpoint, + + pub const Options = struct { + itf_string: []const u8 = "", + poll_interval: u8, + }; pub fn create( - first_interface: u8, - first_string: u8, - first_endpoint_in: u4, - first_endpoint_out: u4, - ) @This() { - return .{ + alloc: *usb.DescriptorAllocator, + max_supported_packet_size: usb.types.Len, + desc_options: Options, + ) usb.DescriptorCreateResult(@This()) { + _ = max_supported_packet_size; + return .{ .descriptor = .{ .interface = .{ - .interface_number = first_interface, + .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_s = first_string, - }, - .hid = hid_descriptor, - .ep_out = .{ - .endpoint = .in(@enumFromInt(first_endpoint_out)), - .attributes = .{ .transfer_type = .Interrupt, .usage = .data }, - .max_packet_size = .from(options.max_packet_size), - .interval = options.endpoint_interval, + .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(desc_options.itf_string), }, - .ep_in = .{ - .endpoint = .in(@enumFromInt(first_endpoint_in)), - .attributes = .{ .transfer_type = .Interrupt, .usage = .data }, - .max_packet_size = .from(options.max_packet_size), - .interval = options.endpoint_interval, + .hid = .{ + .bcd_hid = .from(0x0111), + .country_code = .NotSupported, + .num_descriptors = 1, + .report_length = .from(@sizeOf(@TypeOf(report_descriptor))), }, - }; + .ep_in = .interrupt( + alloc.next_ep(.In), + if (options.boot_protocol) 8 else unreachable, + desc_options.poll_interval, + ), + } }; } }; - const hid_descriptor: 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_in = on_tx_ready, }; device: *usb.DeviceInterface, - ep_in: types.Endpoint.Num, - ep_out: types.Endpoint.Num, + descriptor: *const Descriptor, + tx_ready: std.atomic.Value(bool), - pub fn init(desc: *const Descriptor, device: *usb.DeviceInterface) @This() { - return .{ + 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, - .ep_in = desc.ep_in.endpoint.num, - .ep_out = desc.ep_out.endpoint.num, + .descriptor = desc, + .tx_ready = .init(true), }; } - pub fn class_control(self: *@This(), stage: types.ControlStage, setup: *const types.SetupPacket) ?[]const u8 { - _ = self; - if (stage == .Setup) switch (setup.request_type.type) { + pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 { + 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 request_code = std.meta.intToEnum(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)) + if (request_code == .GetDescriptor and hid_desc_type == .HID) + return std.mem.asBytes(&self.descriptor.hid) 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(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: 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, @@ -104,29 +156,42 @@ 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 => {}, } }, else => {}, - }; + } return usb.nak; } - pub fn transfer(self: *@This(), ep: types.Endpoint, data: []u8) void { - _ = self; - _ = ep; - _ = data; + pub fn on_tx_ready(self: *@This(), ep: EP_Num) void { + log.debug("tx ready ({t})", .{ep}); + self.tx_ready.store(true, .seq_cst); + } + + 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/core/src/core/usb/types.zig b/core/src/core/usb/types.zig index d0f010825..f030f013a 100644 --- a/core/src/core/usb/types.zig +++ b/core/src/core/usb/types.zig @@ -1,34 +1,286 @@ 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, + CDC_Data = 0x0A, + SmartCard = 0x0B, + ContentSecurity = 0x0D, + Video = 0x0E, + PersonalHealthcare = 0x0F, + AudioVideoDevice = 0x10, + BillboardDevice = 0x11, + Type_C_Bridge = 0x12, + BulkDisplayProtocol = 0x13, + MCTP_Over_USB_ProtocolEndpoint = 0x14, + I3C = 0x3C, + DiagnosticDevice = 0xDC, + WirelessController = 0xE0, + Miscellaneous = 0xEF, + ApplicationSpecific = 0xFE, + VendorSpecific = 0xFF, + _, + + pub fn Subclass(self: @This()) type { + return @field(ClassSubclassProtocol.Subclass, @tagName(self)); + } + + pub fn Protocol(self: @This()) type { + return @field(ClassSubclassProtocol.Protocol, @tagName(self)); + } + }; + + pub const Subclass = struct { + pub const Default = enum(u8) { + Unspecified = 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 HID = enum(u8) { + Unspecified = 0x00, + VendorSpecific = 0xFF, + /// + Boot = 0x01, + _, + }; + + 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 Hub = Default; + + pub const CDC_Data = enum(u8) { + Unused = 0, + VendorSpecific = 0xFF, + _, + }; + + 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 Type_C_Bridge = Default; + pub const BulkDisplayProtocol = Default; + pub const MCTP_Over_USB_ProtocolEndpoint = 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 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 CDC_Data = 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 Type_C_Bridge = Default; + pub const BulkDisplayProtocol = Default; + pub const MCTP_Over_USB_ProtocolEndpoint = 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. + 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`. @@ -39,19 +291,32 @@ 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) { + 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) { @@ -59,6 +324,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) @@ -133,26 +399,62 @@ pub const SetupPacket = extern struct { /// conflict. request: u8, /// A simple argument of up to 16 bits, specific to the request. - value: u16, - /// Not used in the requests we support. - index: u16, + value: U16_Le, + 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 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; + /// u16 value, little endian regardless of native endianness. -pub const U16Le = extern struct { - value: [2]u8, +pub const U16_Le = extern struct { + 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); } }; 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/examples/raspberrypi/rp2xxx/src/usb_cdc.zig b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig index 2d39844db..fbc96c319 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_cdc.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_cdc.zig @@ -4,50 +4,29 @@ 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 UsbSerial = usb.drivers.cdc.CdcClassDriver(.{ .max_packet_size = 64 }); - -var usb_dev: rp2xxx.usb.Polled( - usb.Config{ - .device_descriptor = .{ - .bcd_usb = .from(0x0200), - .device_triple = .{ - .class = .Miscellaneous, - .subclass = 2, - .protocol = 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 = true }, - .max_current_ma = 100, - .Drivers = struct { serial: UsbSerial }, - }}, - }, - .{}, -) = undefined; +const USB_Device = rp2xxx.usb.Polled(.{}); +const USB_Serial = usb.drivers.cdc.CdcClassDriver; + +var usb_device: USB_Device = undefined; + +var usb_controller: usb.DeviceController(.{ + .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 = &.{.{ + .attributes = .{ .self_powered = false }, + .max_current_ma = 50, + .Drivers = struct { serial: USB_Serial, reset: rp2xxx.usb.ResetDriver(null, 0) }, + }}, +}, .{.{ + .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}); @@ -57,22 +36,32 @@ 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, }; +const pin_config: rp2xxx.pins.GlobalConfiguration = .{ + .GPIO0 = .{ .function = .UART0_TX }, + .GPIO25 = .{ .name = "led", .direction = .out }, +}; + pub fn main() !void { - uart_tx_pin.set_function(.uart); + const pins = 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_dev = .init(); + usb_device = .init(); var old: u64 = time.get_time_since_boot().to_us(); var new: u64 = 0; @@ -80,15 +69,15 @@ 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; - led.toggle(); + pins.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}); } @@ -106,17 +95,16 @@ 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 { - const text = std.fmt.bufPrint(&usb_tx_buff, fmt, args) catch &.{}; +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 &.{}; - var write_buff = text; - while (write_buff.len > 0) { - write_buff = serial.write(write_buff); - usb_dev.poll(); + while (tx.len > 0) { + tx = tx[serial.write(tx)..]; + 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 - _ = serial.write_flush(); - usb_dev.poll(); + while (!serial.flush()) + usb_device.poll(&usb_controller); } var usb_rx_buff: [1024]u8 = undefined; @@ -124,17 +112,15 @@ 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..]; + 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..3dc7ec1e2 100644 --- a/examples/raspberrypi/rp2xxx/src/usb_hid.zig +++ b/examples/raspberrypi/rp2xxx/src/usb_hid.zig @@ -4,55 +4,29 @@ 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 Keyboard = usb.drivers.hid.Keyboard(.{}); -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( - .{ .max_packet_size = 64, .boot_protocol = true, .endpoint_interval = 0 }, - usb.descriptor.hid.ReportDescriptorKeyboard, -); +var usb_device: USB_Device = undefined; -const usb_config_descriptor = microzig.core.usb.descriptor.Configuration.create(); - -var usb_dev: rp2xxx.usb.Polled( - usb.Config{ - .device_descriptor = .{ - .bcd_usb = .from(0x0200), - .device_triple = .{ - .class = .Unspecified, - .subclass = 0, - .protocol = 0, - }, - .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 = true }, - .max_current_ma = 100, - .Drivers = struct { hid: HidDriver }, - }}, - }, - .{}, -) = undefined; +var usb_controller: usb.DeviceController(.{ + .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 = &.{.{ + .attributes = .{ .self_powered = false }, + .max_current_ma = 50, + .Drivers = struct { keyboard: Keyboard, reset: rp2xxx.usb.ResetDriver(null, 0) }, + }}, +}, .{.{ + .keyboard = .{ .itf_string = "Boot Keyboard", .poll_interval = 1 }, + .reset = "", +}}) = .init; pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { std.log.err("panic: {s}", .{message}); @@ -62,36 +36,56 @@ 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, }; +const pin_config: rp2xxx.pins.GlobalConfiguration = .{ + .GPIO0 = .{ .function = .UART0_TX }, + .GPIO25 = .{ .name = "led", .direction = .out }, +}; + pub fn main() !void { - uart_tx_pin.set_function(.uart); + const pins = 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_dev = .init(); + usb_device = .init(); 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_dev.poll(); - - if (usb_dev.controller.drivers()) |drivers| { - _ = drivers; // TODO + usb_device.poll(&usb_controller); + if (usb_controller.drivers()) |drivers| { 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; - led.toggle(); + idx = 0; + pins.led.toggle(); } } } 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..a6b6e2207 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; @@ -21,7 +22,6 @@ pub const Config = struct { }; const HardwareEndpointData = struct { - awaiting_rx: bool, data_buffer: []align(64) u8, }; @@ -75,115 +75,106 @@ 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"); } return struct { + pub const max_supported_packet_size = 64; + 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 = .{ - .start_tx = start_tx, - .start_rx = start_rx, + .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, data_buffer: []align(64) u8, - controller: usb.DeviceController(controller_config), interface: usb.DeviceInterface, - pub fn poll(self: *@This()) void { - // Check which interrupt flags are set. + 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(); // 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(); - self.controller.on_setup_req(&self.interface, &setup); + // 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); } // 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. - - 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. - // - // This ensures that we can return a shared reference to - // the databuffer contents without races. - // TODO: if ((bc & (1 << 10)) == 1) return EPBError.NotAvailable; - - // 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)].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; + 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) {} + + log.debug("buffer ep{} {t}", .{ ep_num, ep.dir }); + controller.on_buffer(&self.interface, ep); + } } - - peripherals.USB.BUFF_STATUS.write_raw(bufbits_init); - } // <-- END of buf status handling + peripherals.USB.BUFF_STATUS.raw = buff_status; + } // Has the host signaled a bus reset? if (ints.BUS_RESET != 0) { + log.info("bus reset", .{}); + + // 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 }); - peripherals.USB.ADDR_ENDP.modify(.{ .ADDRESS = 0 }); - - self.controller.on_bus_reset(); + set_address(&self.interface, 0); + controller.on_bus_reset(&self.interface); + while (peripherals.USB.EP_ABORT_DONE.raw != 0xFFFFFFFF) {} + peripherals.USB.EP_ABORT.raw = 0; } } 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. @@ -245,27 +236,19 @@ pub fn Polled( .endpoints = undefined, .data_buffer = rp2xxx_buffers.data_buffer, .interface = .{ .vtable = &vtable }, - .controller = .init, }; @memset(std.mem.asBytes(&self.endpoints), 0); - endpoint_open(&self.interface, &.{ - .endpoint = .in(.ep0), - .max_packet_size = .from(64), - .attributes = .{ .transfer_type = .Control, .usage = .data }, - .interval = 0, - }); - endpoint_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. peripherals.USB.SIE_CTRL.modify(.{ .PULLUP_EN = 1 }); + // Listen for ACKs + self.interface.ep_listen(.ep0, max_supported_packet_size); + return self; } @@ -274,38 +257,29 @@ 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 { - const self: *@This() = @fieldParentPtr("interface", itf); + data: []const []const u8, + ) usb.types.Len { + log.debug("writev {t} {}: {any}", .{ ep_num, data[0].len, data[0] }); - // It is technically possible to support longer buffers but this demo - // doesn't bother. - assert(buffer.len <= 64); + const self: *@This() = @fieldParentPtr("interface", itf); 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. - while (bufctrl_ptr.read().AVAILABLE_0 == 1) {} + var hw_buf: []align(1) u8 = ep.data_buffer; - const len = buffer.len; - switch (chip) { - .RP2040 => @memcpy(ep.data_buffer[0..len], buffer[0..len]), - .RP2350 => { - const dst: [*]align(4) u32 = @ptrCast(ep.data_buffer.ptr); - const src: [*]align(1) const u32 = @ptrCast(buffer.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]; - }, + 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..]; } - var bufctrl = bufctrl_ptr.read(); + 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 bufctrl.PID_0 ^= 1; // flip DATA0/1 bufctrl.FULL_0 = 1; // We have put data in @@ -322,30 +296,56 @@ pub fn Polled( // Set available bit bufctrl.AVAILABLE_0 = 1; bufctrl_ptr.write(bufctrl); + + return len; } - fn start_rx( + /// 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, - len: usize, - ) void { - const self: *@This() = @fieldParentPtr("interface", itf); + data: []const []u8, + ) usb.types.Len { + var total_len: usize = data[0].len; + for (data[1..]) |d| total_len += d.len; + log.debug("readv {t} {}", .{ ep_num, total_len }); - // It is technically possible to support longer buffers but this demo doesn't bother. - assert(len <= 64); + const self: *@This() = @fieldParentPtr("interface", itf); + assert(data.len > 0); - const bufctrl_ptr = &buffer_control[@intFromEnum(ep_num)].out; + 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); + 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); + } + log.warn("discarding rx data on ep {t}, {} bytes received", .{ ep_num, bufctrl.LENGTH_0 }); + return @intCast(total_len); + } - // 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; + fn ep_listen( + itf: *usb.DeviceInterface, + ep_num: usb.types.Endpoint.Num, + len: usb.types.Len, + ) void { + log.debug("listen {t} {}", .{ ep_num, len }); + + const self: *@This() = @fieldParentPtr("interface", itf); + const bufctrl_ptr = &buffer_control[@intFromEnum(ep_num)].out; - // Configure the 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(len); // 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); @@ -358,52 +358,49 @@ pub fn Polled( // Set available bit bufctrl.AVAILABLE_0 = 1; bufctrl_ptr.write(bufctrl); - - ep.awaiting_rx = true; } - /// 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}); - fn set_address(itf: *usb.DeviceInterface, addr: u7) void { - const self: *@This() = @fieldParentPtr("interface", itf); - _ = self; + peripherals.USB.ADDR_ENDP.write(.{ .ADDRESS = addr }); + } - peripherals.USB.ADDR_ENDP.modify(.{ .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)]; } - 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 ep = desc.endpoint; + const attr = desc.attributes; + 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() <= 64); - ep_hard.awaiting_rx = false; + assert(desc.max_packet_size.into() <= max_supported_packet_size); buffer_control[@intFromEnum(ep.num)].get(ep.dir).modify(.{ .PID_0 = 1 }); @@ -415,10 +412,11 @@ pub fn Polled( 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), }); } + @memset(ep_hard.data_buffer, 0); } fn endpoint_alloc(self: *@This(), desc: *const usb.descriptor.Endpoint) ![]align(64) u8 { @@ -435,3 +433,61 @@ pub fn Polled( } }; } + +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, + _: usb.types.Len, + interface_str: []const u8, + ) usb.DescriptorCreateResult(@This()) { + return .{ .descriptor = .{ .reset_interface = .{ + .interface_number = alloc.next_itf(), + .alternate_setting = 0, + .num_endpoints = 0, + .interface_triple = .from( + .VendorSpecific, + @enumFromInt(0x00), + @enumFromInt(0x01), + ), + .interface_s = alloc.string(interface_str), + } } }; + } + }; + + pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface, data: []u8) void { + assert(data.len == 0); + _ = 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; + } + }; +}