From 3af91ea8f9210cb5459b87ebcb63345991060e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arkadiusz=20W=C3=B3jcik?= Date: Sun, 14 Jul 2024 23:42:03 +0200 Subject: [PATCH] USB CDC ACM support + code refactor (#211) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * part1: code refactor * part2: config serialization * usb cdc acm support --------- Co-authored-by: Arkadiusz Wójcik @ikskuh --- bsp/raspberrypi/rp2040/src/hal/usb.zig | 183 ++-- core/src/core/usb.zig | 962 +++++++----------- core/src/core/usb/cdc.zig | 254 +++++ core/src/core/usb/descriptors.zig | 112 -- core/src/core/usb/hid.zig | 170 +++- core/src/core/usb/templates.zig | 56 + core/src/core/usb/types.zig | 476 +++++++++ core/src/core/usb/utils.zig | 53 + core/src/core/usb/vendor.zig | 26 + examples/raspberrypi/rp2040/build.zig | 2 +- examples/raspberrypi/rp2040/src/usb_cdc.zig | 105 ++ .../raspberrypi/rp2040/src/usb_device.zig | 169 --- examples/raspberrypi/rp2040/src/usb_hid.zig | 114 +-- 13 files changed, 1580 insertions(+), 1102 deletions(-) create mode 100644 core/src/core/usb/cdc.zig delete mode 100644 core/src/core/usb/descriptors.zig create mode 100644 core/src/core/usb/templates.zig create mode 100644 core/src/core/usb/types.zig create mode 100644 core/src/core/usb/utils.zig create mode 100644 core/src/core/usb/vendor.zig create mode 100644 examples/raspberrypi/rp2040/src/usb_cdc.zig delete mode 100644 examples/raspberrypi/rp2040/src/usb_device.zig diff --git a/bsp/raspberrypi/rp2040/src/hal/usb.zig b/bsp/raspberrypi/rp2040/src/hal/usb.zig index 575a4d3..260e5a9 100644 --- a/bsp/raspberrypi/rp2040/src/hal/usb.zig +++ b/bsp/raspberrypi/rp2040/src/hal/usb.zig @@ -9,7 +9,12 @@ const peripherals = microzig.chip.peripherals; /// Human Interface Device (HID) pub const usb = microzig.core.usb; +pub const types = usb.types; pub const hid = usb.hid; +pub const cdc = usb.cdc; +pub const vendor = usb.vendor; +pub const templates = usb.templates.DescriptorsConfigTemplates; +pub const utils = usb.UsbUtils; const rom = @import("rom.zig"); const resets = @import("resets.zig"); @@ -30,42 +35,26 @@ pub const Usb = usb.Usb(F); pub const DeviceConfiguration = usb.DeviceConfiguration; pub const DeviceDescriptor = usb.DeviceDescriptor; -pub const DescType = usb.DescType; -pub const InterfaceDescriptor = usb.InterfaceDescriptor; -pub const ConfigurationDescriptor = usb.ConfigurationDescriptor; -pub const EndpointDescriptor = usb.EndpointDescriptor; +pub const DescType = usb.types.DescType; +pub const InterfaceDescriptor = usb.types.InterfaceDescriptor; +pub const ConfigurationDescriptor = usb.types.ConfigurationDescriptor; +pub const EndpointDescriptor = usb.types.EndpointDescriptor; pub const EndpointConfiguration = usb.EndpointConfiguration; -pub const Dir = usb.Dir; -pub const TransferType = usb.TransferType; +pub const Dir = usb.types.Dir; +pub const TransferType = usb.types.TransferType; +pub const Endpoint = usb.types.Endpoint; pub const utf8ToUtf16Le = usb.utf8Toutf16Le; -pub var EP0_OUT_CFG: usb.EndpointConfiguration = .{ - .descriptor = &usb.EndpointDescriptor{ - .descriptor_type = usb.DescType.Endpoint, - .endpoint_address = usb.EP0_OUT_ADDR, - .attributes = @intFromEnum(usb.TransferType.Control), - .max_packet_size = 64, - .interval = 0, - }, - .endpoint_control_index = null, - .buffer_control_index = 1, - .data_buffer_index = 0, - .next_pid_1 = false, -}; - -pub var EP0_IN_CFG: usb.EndpointConfiguration = .{ - .descriptor = &usb.EndpointDescriptor{ - .descriptor_type = usb.DescType.Endpoint, - .endpoint_address = usb.EP0_IN_ADDR, - .attributes = @intFromEnum(usb.TransferType.Control), - .max_packet_size = 64, - .interval = 0, - }, - .endpoint_control_index = null, - .buffer_control_index = 0, - .data_buffer_index = 0, - .next_pid_1 = false, +const HardwareEndpoint = struct { + configured: bool, + ep_addr: u8, + next_pid_1: bool, + max_packet_size: u16, + transfer_type: types.TransferType, + endpoint_control_index: usize, + buffer_control_index: usize, + data_buffer_index: usize, }; // +++++++++++++++++++++++++++++++++++++++++++++++++ @@ -118,6 +107,9 @@ pub const buffers = struct { /// A set of functions required by the abstract USB impl to /// create a concrete one. pub const F = struct { + // Fixed number 4, probably should be comptime number up to 16 + var endpoints: [4][2]HardwareEndpoint = undefined; + /// Initialize the USB clock to 48 MHz /// /// This requres that the system clock has been set up before hand @@ -150,7 +142,7 @@ pub const F = struct { // We now have the stable 48MHz reference clock required for USB: } - pub fn usb_init_device(device_config: *usb.DeviceConfiguration) void { + pub fn usb_init_device(_: *usb.DeviceConfiguration) void { // Bring USB out of reset resets.reset(.{ .usbctrl = true }); @@ -264,33 +256,9 @@ pub const F = struct { .SETUP_REQ = 1, }); - // setup endpoints - for (device_config.endpoints) |ep| { - // EP0 doesn't have an endpoint control index; only process the other - // endpoints here. - if (ep.endpoint_control_index) |epci| { - // We need to compute the offset from the base of USB SRAM to the - // buffer we're choosing, because that's how the peripheral do. - const buf_base = @intFromPtr(buffers.B.get(ep.data_buffer_index)); - const dpram_base = @intFromPtr(peripherals.USBCTRL_DPRAM); - // The offset _should_ fit in a u16, but if we've gotten something - // wrong in the past few lines, a common symptom will be integer - // overflow producing a Very Large Number, - const dpram_offset = @as(u16, @intCast(buf_base - dpram_base)); - - // Configure the endpoint! - modify_endpoint_control(epci, .{ - .ENABLE = 1, - // Please set the corresponding bit in buff_status when a - // buffer is done, thx. - .INTERRUPT_PER_BUFF = 1, - // Select bulk vs control (or interrupt as soon as implemented). - .ENDPOINT_TYPE = .{ .raw = @as(u2, @intCast(ep.descriptor.attributes)) }, - // And, designate our buffer by its offset. - .BUFFER_ADDRESS = dpram_offset, - }); - } - } + @memset(std.mem.asBytes(&endpoints), 0); + endpoint_init(Endpoint.EP0_IN_ADDR, 64, types.TransferType.Control); + endpoint_init(Endpoint.EP0_OUT_ADDR, 64, types.TransferType.Control); // Present full-speed device by enabling pullup on DP. This is the point // where the host will notice our presence. @@ -304,7 +272,7 @@ pub const F = struct { /// reuse `buffer` immediately after this returns. No need to wait for the /// packet to be sent. pub fn usb_start_tx( - ep: *usb.EndpointConfiguration, + ep_addr: u8, buffer: []const u8, ) void { // It is technically possible to support longer buffers but this demo @@ -313,6 +281,8 @@ pub const F = struct { // You should only be calling this on IN endpoints. // TODO: assert!(UsbDir::of_endpoint_addr(ep.descriptor.endpoint_address) == UsbDir::In); + const ep = hardware_endpoint_get_by_address(ep_addr); + // Copy the given data into the corresponding ep buffer const epbuffer = buffers.B.get(ep.data_buffer_index); _ = rom.memcpy(epbuffer[0..buffer.len], buffer); @@ -349,7 +319,7 @@ pub const F = struct { } pub fn usb_start_rx( - ep: *usb.EndpointConfiguration, + ep_addr: u8, len: usize, ) void { // It is technically possible to support longer buffers but this demo @@ -358,6 +328,8 @@ pub const F = struct { // You should only be calling this on OUT endpoints. // TODO: assert!(UsbDir::of_endpoint_addr(ep.descriptor.endpoint_address) == UsbDir::Out); + const ep = hardware_endpoint_get_by_address(ep_addr); + // Check which DATA0/1 PID this endpoint is expecting next. const np: u1 = if (ep.next_pid_1) 1 else 0; // Configure the OUT: @@ -392,7 +364,7 @@ pub const F = struct { /// /// One can assume that this function is only called if the /// setup request falg is set. - pub fn get_setup_packet() usb.SetupPacket { + pub fn get_setup_packet() usb.types.SetupPacket { // Clear the status flag (write-one-to-clear) peripherals.USBCTRL_REGS.SIE_STATUS.modify(.{ .SETUP_REC = 1 }); @@ -411,7 +383,7 @@ pub const F = struct { _ = rom.memcpy(setup_packet[0..4], std.mem.asBytes(&spl)); _ = rom.memcpy(setup_packet[4..8], std.mem.asBytes(&sph)); // Reinterpret as setup packet - return std.mem.bytesToValue(usb.SetupPacket, &setup_packet); + return std.mem.bytesToValue(usb.types.SetupPacket, &setup_packet); } /// Called on a bus reset interrupt @@ -425,6 +397,11 @@ pub const F = struct { peripherals.USBCTRL_REGS.ADDR_ENDP.modify(.{ .ADDRESS = addr }); } + pub fn reset_ep0() void { + var ep = hardware_endpoint_get_by_address(Endpoint.EP0_IN_IDX); + ep.next_pid_1 = true; + } + pub fn get_EPBIter(dc: *const usb.DeviceConfiguration) usb.EPBIter { return .{ .bufbits = peripherals.USBCTRL_REGS.BUFF_STATUS.raw, @@ -432,21 +409,61 @@ pub const F = struct { .next = next, }; } + + fn hardware_endpoint_get_by_address(ep_addr: u8) *HardwareEndpoint { + const num = Endpoint.num_from_address(ep_addr); + const dir = Endpoint.dir_from_address(ep_addr); + return &endpoints[num][dir.as_number()]; + } + + pub fn endpoint_init(ep_addr: u8, max_packet_size: u16, transfer_type: types.TransferType) void { + const ep_num = Endpoint.num_from_address(ep_addr); + const ep_dir = Endpoint.dir_from_address(ep_addr); + + var ep = hardware_endpoint_get_by_address(ep_addr); + ep.ep_addr = ep_addr; + ep.max_packet_size = max_packet_size; + ep.transfer_type = transfer_type; + ep.next_pid_1 = false; + + ep.buffer_control_index = 2 * ep_num + ep_dir.as_number_reversed(); + // TODO - some other way to deal with it + ep.data_buffer_index = 2 * ep_num + ep_dir.as_number(); + + if (ep_num == 0) { + ep.endpoint_control_index = 0; + } else { + ep.endpoint_control_index = 2*ep_num - ep_dir.as_number(); + endpoint_alloc(ep, transfer_type); + } + } + + fn endpoint_alloc(ep: *HardwareEndpoint, transfer_type: TransferType) void { + const buf_base = @intFromPtr(buffers.B.get(ep.data_buffer_index)); + const dpram_base = @intFromPtr(peripherals.USBCTRL_DPRAM); + // The offset _should_ fit in a u16, but if we've gotten something + // wrong in the past few lines, a common symptom will be integer + // overflow producing a Very Large Number, + const dpram_offset = @as(u16, @intCast(buf_base - dpram_base)); + + // Configure the endpoint! + modify_endpoint_control(ep.endpoint_control_index, .{ + .ENABLE = 1, + // Please set the corresponding bit in buff_status when a + // buffer is done, thx. + .INTERRUPT_PER_BUFF = 1, + // Select bulk vs control (or interrupt as soon as implemented). + .ENDPOINT_TYPE = .{ .raw = transfer_type.as_number() }, + // And, designate our buffer by its offset. + .BUFFER_ADDRESS = dpram_offset, + }); + } }; // +++++++++++++++++++++++++++++++++++++++++++++++++ // Utility functions // +++++++++++++++++++++++++++++++++++++++++++++++++ -/// Check if the corresponding buffer is available -pub fn buffer_available( - ep: *usb.EndpointConfiguration, -) bool { - const rbc = read_raw_buffer_control(ep.buffer_control_index); - // Bit 11 of the EPn_X_BUFFER_CONTROL register represents the AVAILABLE_0 flag - return ((rbc & 0x400) == 0); -} - pub fn modify_buffer_control( i: usize, fields: anytype, @@ -596,9 +613,9 @@ pub fn next(self: *usb.EPBIter) ?usb.EPB { const epnum = @as(u8, @intCast(lowbit_index >> 1)); // Of the pair, the IN endpoint comes first, followed by OUT, so // we can get the direction by: - const dir = if (lowbit_index & 1 == 0) usb.Dir.In else usb.Dir.Out; + const dir = if (lowbit_index & 1 == 0) usb.types.Dir.In else usb.types.Dir.Out; - const ep_addr = dir.endpoint(epnum); + const ep_addr = Endpoint.to_address(epnum, dir); // Process the buffer-done event. // Process the buffer-done event. @@ -607,17 +624,13 @@ pub fn next(self: *usb.EPBIter) ?usb.EPB { // 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. - var endpoint: ?*usb.EndpointConfiguration = null; - for (self.device_config.endpoints) |ep| { - if (ep.descriptor.endpoint_address == ep_addr) { - endpoint = ep; - break; - } - } + + const endpoint = F.hardware_endpoint_get_by_address(ep_addr); + // Buffer event for unknown EP?! // TODO: if (endpoint == null) return EPBError.UnknownEndpoint; // Read the buffer control register to check status. - const bc = read_raw_buffer_control(endpoint.?.buffer_control_index); + const bc = read_raw_buffer_control(endpoint.buffer_control_index); // We should only get here if we've been notified that // the buffer is ours again. This is indicated by the hw @@ -631,7 +644,7 @@ pub fn next(self: *usb.EPBIter) ?usb.EPB { // Get a pointer to the buffer in USB SRAM. This is the // buffer _contents_. See the safety comments below. - const epbuffer = buffers.B.get(endpoint.?.data_buffer_index); + const epbuffer = buffers.B.get(endpoint.data_buffer_index); // Get the actual length of the data, which may be less // than the buffer size. @@ -639,7 +652,7 @@ pub fn next(self: *usb.EPBIter) ?usb.EPB { // Copy the data from SRAM return usb.EPB{ - .endpoint = endpoint.?, + .endpoint_address = ep_addr, .buffer = epbuffer[0..len], }; } diff --git a/core/src/core/usb.zig b/core/src/core/usb.zig index 8f1f611..3e789e5 100644 --- a/core/src/core/usb.zig +++ b/core/src/core/usb.zig @@ -13,8 +13,21 @@ const std = @import("std"); const builtin = @import("builtin"); +/// USB primitive types +pub const types = @import("usb/types.zig"); /// USB Human Interface Device (HID) pub const hid = @import("usb/hid.zig"); +pub const cdc = @import("usb/cdc.zig"); +pub const vendor = @import("usb/vendor.zig"); +pub const utils = @import("usb/utils.zig"); +pub const templates = @import("usb/templates.zig"); + + +const DescType = types.DescType; +const Dir = types.Dir; +const Endpoint = types.Endpoint; +const SetupRequest = types.SetupRequest; +const BosConfig = utils.BosConfig; /// Create a USB device /// @@ -40,14 +53,56 @@ pub fn Usb(comptime f: anytype) type { var usb_config: ?*DeviceConfiguration = null; /// The clock has been initialized [Y/n] var clk_init: bool = false; + var itf_to_drv: [16]u8 = .{0} ** 16; + var ep_to_drv: [4][2]u8 = .{.{0} ** 2} ** 4; - /// Index of enpoint buffer 0 out - pub const EP0_OUT_IDX = 0; - /// Index of enpoint buffer 0 in - pub const EP0_IN_IDX = 1; /// The callbacks passed provided by the caller pub const callbacks = f; + // We'll keep some state in Plain Old Static Local Variables: + const S = struct { + var debug_mode = false; + // When the host gives us a new address, we can't just slap it into + // registers right away, because we have to do an acknowledgement step using + // our _old_ address. + var new_address: ?u8 = null; + // 0 - no config set + var cfg_num: u16 = 0; + // Flag recording whether the host has configured us with a + // `SetConfiguration` message. + var configured = false; + // Flag recording whether we've set up buffer transfers after being + // configured. + var started = false; + // Some scratch space that we'll use for things like preparing string + // descriptors for transmission. + var tmp: [128]u8 = .{0} ** 128; + // Keeps track of sent data from tmp buffer + var buffer_reader = BufferReader { .buffer = &.{} }; + // Last setup packet request + var setup_packet: types.SetupPacket = undefined; + // Class driver associated with last setup request if any + var driver: ?*types.UsbClassDriver = null; + }; + + // Command endpoint utilities + const CmdEndpoint = struct { + + /// Command response utility function that can split long data in multiple packets + fn send_cmd_response(data: []const u8, expected_max_length: u16) void { + S.buffer_reader = BufferReader { .buffer = data[0..@min(data.len, expected_max_length)] }; + const data_chunk = S.buffer_reader.try_peek(64); + + if (data_chunk.len > 0) { + f.usb_start_tx(Endpoint.EP0_IN_IDX, data_chunk); + } + } + + fn send_cmd_ack() void { + f.usb_start_tx(Endpoint.EP0_IN_IDX, &.{}); + } + }; + /// Initialize the USB clock pub fn init_clk() void { f.usb_init_clk(); @@ -62,6 +117,56 @@ pub fn Usb(comptime f: anytype) type { f.usb_init_device(device_config); usb_config = device_config; + + const device_interface = device(); + for (usb_config.?.drivers) |*driver| { + driver.init(device_interface); + } + } + + fn device() types.UsbDevice { + return .{ + .fn_ready = device_ready, + .fn_control_transfer = device_control_transfer, + .fn_control_ack = device_control_ack, + .fn_endpoint_open = device_endpoint_open, + .fn_endpoint_transfer = device_endpoint_transfer + }; + } + + fn device_ready() bool { + return S.started; + } + + fn device_control_transfer(setup: *const types.SetupPacket, data: []const u8) void { + CmdEndpoint.send_cmd_response(data, setup.length); + } + + fn device_control_ack(_: *const types.SetupPacket) void { + CmdEndpoint.send_cmd_ack(); + } + + fn device_endpoint_open(ep_desc: []const u8) void { + const ep_addr = BosConfig.get_data_u8(ep_desc, 2); + const ep_transfer_type = BosConfig.get_data_u8(ep_desc, 3); + const ep_max_packet_size = BosConfig.get_data_u16(ep_desc, 4); + + f.endpoint_init(ep_addr, ep_max_packet_size, types.TransferType.from_u8(ep_transfer_type) orelse types.TransferType.Bulk); + } + + fn device_endpoint_transfer(ep_addr: u8, data: []const u8) void { + f.usb_start_tx(ep_addr, data); + } + + fn get_driver(drv_idx: u8) *types.UsbClassDriver { + return &usb_config.?.drivers[drv_idx]; + } + + fn get_setup_packet() types.SetupPacket { + const setup = f.get_setup_packet(); + S.setup_packet = setup; + S.driver = null; + return setup; } /// Usb task function meant to be executed in regular intervals after @@ -71,44 +176,203 @@ pub fn Usb(comptime f: anytype) type { pub fn task(debug: bool) !void { if (usb_config == null) return error.UninitializedDevice; - // We'll keep some state in Plain Old Static Local Variables: - const S = struct { - // When the host gives us a new address, we can't just slap it into - // registers right away, because we have to do an acknowledgement step using - // our _old_ address. - var new_address: ?u8 = null; - // Flag recording whether the host has configured us with a - // `SetConfiguration` message. - var configured = false; - // Flag recording whether we've set up buffer transfers after being - // configured. - var started = false; - // Some scratch space that we'll use for things like preparing string - // descriptors for transmission. - var tmp: [128]u8 = .{0} ** 128; - // Keeps track of sent data from tmp buffer - var buffer_reader = BufferReader { .buffer = &.{} }; + S.debug_mode = debug; + + // Device Specific Request + const DeviceRequestProcessor = struct { + + fn process_setup_request(setup: *const types.SetupPacket) !void { + switch (setup.request_type.type) { + .Class => { + //const itfIndex = setup.index & 0x00ff; + std.log.info("Device.Class", .{}); + }, + .Standard => { + const req = SetupRequest.from_u8(setup.request); + if (req == null) return; + switch (req.?) { + SetupRequest.SetAddress => { + S.new_address = @as(u8, @intCast(setup.value & 0xff)); + CmdEndpoint.send_cmd_ack(); + if (S.debug_mode) std.log.info(" SetAddress: {}", .{S.new_address.?}); + }, + SetupRequest.SetConfiguration => { + if (S.debug_mode) std.log.info(" SetConfiguration", .{}); + const cfg_num = setup.value; + if (S.cfg_num != cfg_num) { + if (S.cfg_num > 0) { + // TODO - cleanup current config drivers + } + + if (cfg_num > 0) { + try process_set_config(cfg_num - 1); + // TODO: call mount callback if any + } else { + // TODO: call umount callback if any + } + } + S.cfg_num = cfg_num; + S.configured = true; + CmdEndpoint.send_cmd_ack(); + }, + SetupRequest.GetDescriptor => { + const descriptor_type = DescType.from_u8(@intCast(setup.value >> 8)); + if (descriptor_type) |dt| { + try process_get_descriptor(setup, dt); + } + } + } + }, + else => {} + } + } + + fn process_get_descriptor(setup: *const types.SetupPacket, descriptor_type: DescType) !void { + switch (descriptor_type) { + .Device => { + if (S.debug_mode) std.log.info(" Device", .{}); + + var bw = BufferWriter { .buffer = &S.tmp }; + try bw.write(&usb_config.?.device_descriptor.serialize()); + + CmdEndpoint.send_cmd_response(bw.get_written_slice(), setup.length); + }, + .Config => { + if (S.debug_mode) std.log.info(" Config", .{}); + + var bw = BufferWriter { .buffer = &S.tmp }; + try bw.write(usb_config.?.config_descriptor); + + CmdEndpoint.send_cmd_response(bw.get_written_slice(), setup.length); + }, + .String => { + if (S.debug_mode) std.log.info(" String", .{}); + // String descriptor index is in bottom 8 bits of + // `value`. + const i: usize = @intCast(setup.value & 0xff); + const bytes = StringBlk: { + if (i == 0) { + // Special index 0 requests the language + // descriptor. + break :StringBlk usb_config.?.lang_descriptor; + } else { + // Otherwise, set up one of our strings. + const s = usb_config.?.descriptor_strings[i - 1]; + const len = 2 + s.len; + + var wb = BufferWriter { .buffer = &S.tmp }; + try wb.write_int(u8, @intCast(len)); + try wb.write_int(u8, 0x03); + try wb.write(s); + + break :StringBlk wb.get_written_slice(); + } + }; + + CmdEndpoint.send_cmd_response(bytes, setup.length); + }, + .Interface => { + if (S.debug_mode) std.log.info(" Interface", .{}); + }, + .Endpoint => { + if (S.debug_mode) std.log.info(" Endpoint", .{}); + }, + .DeviceQualifier => { + if (S.debug_mode) std.log.info(" DeviceQualifier", .{}); + // We will just copy parts of the DeviceDescriptor because + // the DeviceQualifierDescriptor can be seen as a subset. + const dqd = types.DeviceQualifierDescriptor{ + .bcd_usb = usb_config.?.device_descriptor.bcd_usb, + .device_class = usb_config.?.device_descriptor.device_class, + .device_subclass = usb_config.?.device_descriptor.device_subclass, + .device_protocol = usb_config.?.device_descriptor.device_protocol, + .max_packet_size0 = usb_config.?.device_descriptor.max_packet_size0, + .num_configurations = usb_config.?.device_descriptor.num_configurations, + }; + + var bw = BufferWriter { .buffer = &S.tmp }; + try bw.write(&dqd.serialize()); + + CmdEndpoint.send_cmd_response(bw.get_written_slice(), setup.length); + }, + else => {} + } + } + + fn process_set_config(_: u16) !void { + // TODO: we support just one config for now so ignore config index + const bos_cfg = usb_config.?.config_descriptor; + + var curr_bos_cfg = bos_cfg; + var curr_drv_idx: u8 = 0; + + if (BosConfig.try_get_desc_as(types.ConfigurationDescriptor, curr_bos_cfg)) |_| { + curr_bos_cfg = BosConfig.get_desc_next(curr_bos_cfg); + } else { + // TODO - error + return; + } + + while (curr_bos_cfg.len > 0) : (curr_drv_idx += 1) { + var assoc_itf_count: u8 = 1; + // New class starts optionally from InterfaceAssociation followed by mandatory Interface + if (BosConfig.try_get_desc_as(types.InterfaceAssociationDescriptor, curr_bos_cfg)) |desc_assoc_itf| { + assoc_itf_count = desc_assoc_itf.interface_count; + curr_bos_cfg = BosConfig.get_desc_next(curr_bos_cfg); + } + + if (BosConfig.get_desc_type(curr_bos_cfg) != DescType.Interface) { + // TODO - error + return; + } + const desc_itf = BosConfig.get_desc_as(types.InterfaceDescriptor, curr_bos_cfg); + + var driver = usb_config.?.drivers[curr_drv_idx]; + const drv_cfg_len = try driver.open(curr_bos_cfg); + + for (0..assoc_itf_count) |itf_offset| { + const itf_num = desc_itf.interface_number + itf_offset; + itf_to_drv[itf_num] = curr_drv_idx; + } + + bind_endpoints_to_driver(curr_bos_cfg[0..drv_cfg_len], curr_drv_idx); + curr_bos_cfg = curr_bos_cfg[drv_cfg_len..]; + + // TODO: TMP solution - just 1 driver so quit while loop + return; + } + } + + fn bind_endpoints_to_driver(drv_bos_cfg: []const u8, drv_idx: u8) void { + var curr_bos_cfg = drv_bos_cfg; + while (curr_bos_cfg.len > 0) : ({curr_bos_cfg = BosConfig.get_desc_next(curr_bos_cfg);}) { + if (BosConfig.try_get_desc_as(types.EndpointDescriptor, curr_bos_cfg)) |desc_ep| { + const ep_addr = desc_ep.endpoint_address; + ep_to_drv[Endpoint.num_from_address(ep_addr)][Endpoint.dir_from_address(ep_addr).as_number()] = drv_idx; + } + } + } }; - - // Command endpoint utilities - const CmdEndpoint = struct { - - /// Command response utility function that can split long data in multiple packets - fn send_cmd_response(data: []const u8, expected_max_length: u16) void { - const cmd_in_endpoint = usb_config.?.endpoints[EP0_IN_IDX]; - - S.buffer_reader = BufferReader { .buffer = data[0..@min(data.len, expected_max_length)] }; - const data_chunk = S.buffer_reader.try_peek(cmd_in_endpoint.descriptor.max_packet_size); - - if (data_chunk.len > 0) { - f.usb_start_tx( - cmd_in_endpoint, - data_chunk - ); + + // Class/Interface Specific Request + const InterfaceRequestProcessor = struct { + fn process_setup_request(setup: *const types.SetupPacket) !void { + const itf: u8 = @intCast(setup.index & 0xFF); + var driver = get_driver(itf_to_drv[itf]); + S.driver = driver; + + if (driver.class_control(.Setup, setup) == false) { + // TODO } } }; + // Endpoint Specific Request + const EndpointRequestProcessor = struct { + fn process_setup_request(_: *const types.SetupPacket) !void { + } + }; + // Check which interrupt flags are set. const ints = f.get_interrupts(); @@ -116,204 +380,22 @@ pub fn Usb(comptime f: anytype) type { if (ints.SetupReq) { if (debug) std.log.info("setup req", .{}); - // Get the setup request setup packet - const setup = f.get_setup_packet(); + const setup = get_setup_packet(); // 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. - usb_config.?.endpoints[EP0_IN_IDX].next_pid_1 = true; - - // Attempt to parse the request type and request into one of our - // known enum values, and then inspect them. (These will return None - // if we get an unexpected numeric value.) - const reqty = Dir.of_endpoint_addr(setup.request_type); - const req = SetupRequest.from_u8(setup.request); - - if (reqty == Dir.Out and req != null and req.? == SetupRequest.SetAddress) { - // The new address is in the bottom 8 bits of the setup - // packet value field. Store it for use later. - S.new_address = @as(u8, @intCast(setup.value & 0xff)); - // The address will actually get set later, we have - // to use address 0 to send a status response. - f.usb_start_tx( - usb_config.?.endpoints[EP0_IN_IDX], //EP0_IN_CFG, - &.{}, // <- see, empty buffer - ); - if (debug) std.log.info(" SetAddress: {}", .{S.new_address.?}); - } else if (reqty == Dir.Out and req != null and req.? == SetupRequest.SetConfiguration) { - // We only have one configuration, and it doesn't really - // mean anything to us -- more of a formality. All we do in - // response to this is: - S.configured = true; - f.usb_start_tx( - usb_config.?.endpoints[EP0_IN_IDX], //EP0_IN_CFG, - &.{}, // <- see, empty buffer - ); - if (debug) std.log.info(" SetConfiguration", .{}); - } else if (reqty == Dir.Out) { - // This is sort of a hack, but: if we get any other kind of - // OUT, just acknowledge it with the same zero-length status - // phase that we use for control transfers that we _do_ - // understand. This keeps the host from spinning forever - // while we NAK. - // - // This behavior copied shamelessly from the C example. - f.usb_start_tx( - usb_config.?.endpoints[EP0_IN_IDX], // EP0_IN_CFG, - &.{}, // <- see, empty buffer - ); - if (debug) std.log.info(" Just OUT", .{}); - } else if (reqty == Dir.In and req != null and req.? == SetupRequest.GetDescriptor) { - // Identify the requested descriptor type, which is in the - // _top_ 8 bits of value. - const descriptor_type = DescType.from_u16(setup.value >> 8); - if (debug) std.log.info(" GetDescriptor: {}", .{setup.value >> 8}); - if (descriptor_type) |dt| { - switch (dt) { - .Device => { - if (debug) std.log.info(" Device", .{}); - // TODO: this sure looks like a duplicate, but it's - // a duplicate that was present in the C - // implementation. - usb_config.?.endpoints[EP0_IN_IDX].next_pid_1 = true; - - var bw = BufferWriter { .buffer = &S.tmp }; - try bw.write(&usb_config.?.device_descriptor.serialize()); - - CmdEndpoint.send_cmd_response(bw.get_written_slice(), setup.length); - }, - .Config => { - if (debug) std.log.info(" Config", .{}); - - // Config descriptor requests are slightly unusual. - // We can respond with just our config descriptor, - // but we can _also_ append our interface and - // endpoint descriptors to the end, saving some - // round trips. - var bw = BufferWriter { .buffer = &S.tmp }; - try bw.write(&usb_config.?.config_descriptor.serialize()); - try bw.write(&usb_config.?.interface_descriptor.serialize()); - - - // Seems like the host does not bother asking for the - // hid descriptor so we'll just send it with the - // other descriptors. - if (usb_config.?.hid) |hid_conf| { - try bw.write(&hid_conf.hid_descriptor.serialize()); - } - - for (usb_config.?.endpoints[2..]) |ep| { - try bw.write(&ep.descriptor.serialize()); - } - - CmdEndpoint.send_cmd_response(bw.get_written_slice(), setup.length); - }, - .String => { - if (debug) std.log.info(" String", .{}); - // String descriptor index is in bottom 8 bits of - // `value`. - const i: usize = @intCast(setup.value & 0xff); - const bytes = StringBlk: { - if (i == 0) { - // Special index 0 requests the language - // descriptor. - break :StringBlk usb_config.?.lang_descriptor; - } else { - // Otherwise, set up one of our strings. - const s = usb_config.?.descriptor_strings[i - 1]; - const len = 2 + s.len; - - var wb = BufferWriter { .buffer = &S.tmp }; - try wb.write_int(u8, @intCast(len)); - try wb.write_int(u8, 0x03); - try wb.write(s); - - break :StringBlk wb.get_written_slice(); - } - }; - - CmdEndpoint.send_cmd_response(bytes, setup.length); - }, - .Interface => { - if (debug) std.log.info(" Interface", .{}); - // We don't expect the host to send this because we - // delivered our interface descriptor with the - // config descriptor. - // - // Should probably implement it, though, because - // otherwise the host will be unhappy. TODO. - // - // Note that the C example gets away with ignoring - // this. - }, - .Endpoint => { - if (debug) std.log.info(" Endpoint", .{}); - // Same deal as interface descriptors above. - }, - .DeviceQualifier => { - if (debug) std.log.info(" DeviceQualifier", .{}); - // We will just copy parts of the DeviceDescriptor because - // the DeviceQualifierDescriptor can be seen as a subset. - const dqd = DeviceQualifierDescriptor{ - .bcd_usb = usb_config.?.device_descriptor.bcd_usb, - .device_class = usb_config.?.device_descriptor.device_class, - .device_subclass = usb_config.?.device_descriptor.device_subclass, - .device_protocol = usb_config.?.device_descriptor.device_protocol, - .max_packet_size0 = usb_config.?.device_descriptor.max_packet_size0, - .num_configurations = usb_config.?.device_descriptor.num_configurations, - }; - - var bw = BufferWriter { .buffer = &S.tmp }; - try bw.write(&dqd.serialize()); - - CmdEndpoint.send_cmd_response(bw.get_written_slice(), setup.length); - }, - } - } else { - // Maybe the unknown request type is a hid request - - if (usb_config.?.hid) |hid_conf| { - const _hid_desc_type = hid.DescType.from_u16(setup.value >> 8); - - if (_hid_desc_type) |hid_desc_type| { - switch (hid_desc_type) { - .Hid => { - if (debug) std.log.info(" HID", .{}); - - var bw = BufferWriter { .buffer = &S.tmp }; - try bw.write(&hid_conf.hid_descriptor.serialize()); - - CmdEndpoint.send_cmd_response(bw.get_written_slice(), setup.length); - }, - .Report => { - if (debug) std.log.info(" Report", .{}); - - // The report descriptor is already a (static) - // u8 array, i.e., we can pass it directly - CmdEndpoint.send_cmd_response(hid_conf.report_descriptor, setup.length); - }, - .Physical => { - if (debug) std.log.info(" Physical", .{}); - // Ignore for now - }, - } - } else { - // It's not a valid HID request. This can totally happen - // we'll just ignore it for now... - } - } - } - } else if (reqty == Dir.In) { - if (debug) std.log.info(" Just IN", .{}); - // Other IN request. Ignore. - } else { - if (debug) std.log.info(" This is unexpected", .{}); - // Unexpected request type or request bits. This can totally - // happen (yay, hardware!) but is rare in practice. Ignore - // it. + // TODO - maybe it can be moved to f.get_setup_packet? + f.reset_ep0(); + + switch (setup.request_type.recipient) { + .Device => try DeviceRequestProcessor.process_setup_request(&setup), + .Interface => try InterfaceRequestProcessor.process_setup_request(&setup), + .Endpoint => try EndpointRequestProcessor.process_setup_request(&setup), + else => {} } - } // <-- END of setup request handling + + } // Events on one or more buffers? (In practice, always one.) if (ints.BuffStatus) { @@ -326,11 +408,10 @@ pub fn Usb(comptime f: anytype) type { // 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 (epb.endpoint.descriptor.endpoint_address) { - EP0_IN_ADDR => { + switch (epb.endpoint_address) { + Endpoint.EP0_IN_ADDR => { if (debug) std.log.info(" EP0_IN_ADDR", .{}); - const cmd_in_endpoint = usb_config.?.endpoints[EP0_IN_IDX]; const buffer_reader = &S.buffer_reader; // We use this opportunity to finish the delayed @@ -342,17 +423,21 @@ pub fn Usb(comptime f: anytype) type { if (epb.buffer.len > 0 and buffer_reader.get_remaining_bytes_count() > 0) { _ = buffer_reader.try_advance(epb.buffer.len); - const next_data_chunk = buffer_reader.try_read(cmd_in_endpoint.descriptor.max_packet_size); + const next_data_chunk = buffer_reader.try_peek(64); if (next_data_chunk.len > 0) { f.usb_start_tx( - cmd_in_endpoint, + Endpoint.EP0_IN_IDX, next_data_chunk, ); } else { f.usb_start_rx( - usb_config.?.endpoints[EP0_OUT_IDX], // EP0_OUT_CFG, + Endpoint.EP0_OUT_IDX, 0, ); + + if (S.driver) |driver| { + _ = driver.class_control(.Ack, &S.setup_packet); + } } } else { // Otherwise, we've just finished sending @@ -361,19 +446,19 @@ pub fn Usb(comptime f: anytype) type { // OUT) a zero-byte DATA packet, so, set that // up: f.usb_start_rx( - usb_config.?.endpoints[EP0_OUT_IDX], // EP0_OUT_CFG, + Endpoint.EP0_OUT_IDX, 0, ); + + if (S.driver) |driver| { + _ = driver.class_control(.Ack, &S.setup_packet); + } } }, else => { if (debug) std.log.info(" ELSE, ep_addr: {}", .{ - epb.endpoint.descriptor.endpoint_address & 0x7f, + epb.endpoint_address & 0x7f, }); - // Handle user provided endpoints. - - // Invoke the callback (if the user provides one). - if (epb.endpoint.callback) |callback| callback(usb_config.?, epb.buffer); }, } } @@ -396,376 +481,22 @@ pub fn Usb(comptime f: anytype) type { // If we have been configured but haven't reached this point yet, set up // our custom EP OUT's to receive whatever data the host wants to send. if (S.configured and !S.started) { - // We can skip the first two endpoints because those are EP0_OUT and EP0_IN - for (usb_config.?.endpoints[2..]) |ep| { - if (Dir.of_endpoint_addr(ep.descriptor.endpoint_address) == .Out) { - // Hey host! we expect data! - f.usb_start_rx( - ep, - 64, - ); - } - } S.started = true; } } }; } -// +++++++++++++++++++++++++++++++++++++++++++++++++ -// Data Types -// +++++++++++++++++++++++++++++++++++++++++++++++++ - -// ------------------------- -// | DeviceDescriptor | -// ------------------------- -// | -// v -// ------------------------- -// | ConfigurationDesc | -// ------------------------- -// | -// v -// ------------------------- -// | InterfaceDescriptor | -// ------------------------- -// | | -// v ------------------------------ -// ------------------------- | -// | EndpointDescriptor | v -// ------------------------- --------------------- -// | HID Descriptor | -// --------------------- - -/// Types of USB descriptor -pub const DescType = enum(u8) { - Device = 0x01, - Config = 0x02, - String = 0x03, - Interface = 0x04, - Endpoint = 0x05, - DeviceQualifier = 0x06, - //-------- Class Specific Descriptors ---------- - // 0x21 ... - - pub fn from_u16(v: u16) ?@This() { - return switch (v) { - 1 => @This().Device, - 2 => @This().Config, - 3 => @This().String, - 4 => @This().Interface, - 5 => @This().Endpoint, - 6 => @This().DeviceQualifier, - else => null, - }; - } -}; - -/// Types of transfer that can be indicated by the `attributes` field on -/// `EndpointDescriptor`. -pub const TransferType = enum(u2) { - Control = 0, - Isochronous = 1, - Bulk = 2, - Interrupt = 3, -}; - -/// The types of USB SETUP requests that we understand. -pub const SetupRequest = enum(u8) { - /// Asks the device to send a certain descriptor back to the host. Always - /// used on an IN request. - GetDescriptor = 0x06, - /// Notifies the device that it's being moved to a different address on the - /// bus. Always an OUT. - SetAddress = 0x05, - /// Configures a device by choosing one of the options listed in its - /// descriptors. Always an OUT. - SetConfiguration = 0x09, - - pub fn from_u8(request: u8) ?@This() { - return switch (request) { - 0x06 => SetupRequest.GetDescriptor, - 0x05 => SetupRequest.SetAddress, - 0x09 => SetupRequest.SetConfiguration, - else => null, - }; - } -}; - -/// USB deals in two different transfer directions, called OUT (host-to-device) -/// and IN (device-to-host). In the vast majority of cases, OUT is represented -/// by a 0 byte, and IN by an `0x80` byte. -pub const Dir = enum(u8) { - Out = 0, - In = 0x80, - - pub inline fn endpoint(self: @This(), num: u8) u8 { - return num | @intFromEnum(self); - } - - pub inline fn of_endpoint_addr(addr: u8) @This() { - return if (addr & @intFromEnum(@This().In) != 0) @This().In else @This().Out; - } -}; - -/// Describes an endpoint within an interface -pub const EndpointDescriptor = struct { - /// Type of this descriptor, must be `Endpoint`. - descriptor_type: DescType, - /// Address of this endpoint, where the bottom 4 bits give the endpoint - /// number (0..15) and the top bit distinguishes IN (1) from OUT (0). - endpoint_address: u8, - /// Endpoint attributes; the most relevant part is the bottom 2 bits, which - /// control the transfer type using the values from `TransferType`. - attributes: u8, - /// Maximum packet size this endpoint can accept/produce. - max_packet_size: u16, - /// Interval for polling interrupt/isochronous endpoints (which we don't - /// currently support) in milliseconds. - interval: u8, - - pub fn serialize(self: *const @This()) [7]u8 { - var out: [7]u8 = undefined; - out[0] = out.len; - out[1] = @intFromEnum(self.descriptor_type); - out[2] = self.endpoint_address; - out[3] = self.attributes; - out[4] = @intCast(self.max_packet_size & 0xff); - out[5] = @intCast((self.max_packet_size >> 8) & 0xff); - out[6] = self.interval; - return out; - } -}; - -/// Description of an interface within a configuration. -pub const InterfaceDescriptor = struct { - /// Type of this descriptor, must be `Interface`. - descriptor_type: DescType, - /// ID of this interface. - interface_number: u8, - /// Allows a single `interface_number` to have several alternate interface - /// settings, where each alternate increments this field. Normally there's - /// only one, and `alternate_setting` is zero. - 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, - /// Index of interface name within string descriptor table. - interface_s: u8, - - pub fn serialize(self: *const @This()) [9]u8 { - var out: [9]u8 = undefined; - out[0] = out.len; - out[1] = @intFromEnum(self.descriptor_type); - out[2] = self.interface_number; - out[3] = self.alternate_setting; - out[4] = self.num_endpoints; - out[5] = self.interface_class; - out[6] = self.interface_subclass; - out[7] = self.interface_protocol; - out[8] = self.interface_s; - return out; - } -}; - -/// Description of a single available device configuration. -pub const ConfigurationDescriptor = struct { - /// Type of this descriptor, must be `Config`. - descriptor_type: DescType, - /// 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: u16, - /// Number of interface descriptors in this configuration. - num_interfaces: u8, - /// Number to use when requesting this configuration via a - /// `SetConfiguration` request. - configuration_value: u8, - /// Index of this configuration's name in the string descriptor table. - configuration_s: u8, - /// Bit set of device attributes: - /// - /// - Bit 7 should be set (indicates that device can be bus powered in USB - /// 1.0). - /// - Bit 6 indicates that the device can be self-powered. - /// - Bit 5 indicates that the device can signal remote wakeup of the host - /// (like a keyboard). - /// - The rest are reserved and should be zero. - attributes: u8, - /// Maximum device power consumption in units of 2mA. - max_power: u8, - - pub fn serialize(self: *const @This()) [9]u8 { - var out: [9]u8 = undefined; - out[0] = out.len; - out[1] = @intFromEnum(self.descriptor_type); - out[2] = @intCast(self.total_length & 0xff); - out[3] = @intCast((self.total_length >> 8) & 0xff); - out[4] = self.num_interfaces; - out[5] = self.configuration_value; - out[6] = self.configuration_s; - out[7] = self.attributes; - out[8] = self.max_power; - return out; - } -}; - -/// Describes a device. This is the most broad description in USB and is -/// typically the first thing the host asks for. -pub const DeviceDescriptor = struct { - /// Type of this descriptor, must be `Device`. - descriptor_type: DescType, - /// Version of the device descriptor / USB protocol, in binary-coded - /// decimal. This is typically `0x01_10` for USB 1.1. - bcd_usb: u16, - /// Class of device, giving a broad functional area. - device_class: u8, - /// Subclass of device, refining the class. - device_subclass: u8, - /// Protocol within the subclass. - device_protocol: u8, - /// Maximum unit of data this device can move. - max_packet_size0: u8, - /// ID of product vendor. - vendor: u16, - /// ID of product. - product: u16, - /// Device version number, as BCD again. - bcd_device: u16, - /// Index of manufacturer name in string descriptor table. - manufacturer_s: u8, - /// Index of product name in string descriptor table. - product_s: u8, - /// Index of serial number in string descriptor table. - serial_s: u8, - /// Number of configurations supported by this device. - num_configurations: u8, - - pub fn serialize(self: *const @This()) [18]u8 { - var out: [18]u8 = undefined; - out[0] = out.len; - out[1] = @intFromEnum(self.descriptor_type); - out[2] = @intCast(self.bcd_usb & 0xff); - out[3] = @intCast((self.bcd_usb >> 8) & 0xff); - out[4] = self.device_class; - out[5] = self.device_subclass; - out[6] = self.device_protocol; - out[7] = self.max_packet_size0; - out[8] = @intCast(self.vendor & 0xff); - out[9] = @intCast((self.vendor >> 8) & 0xff); - out[10] = @intCast(self.product & 0xff); - out[11] = @intCast((self.product >> 8) & 0xff); - out[12] = @intCast(self.bcd_device & 0xff); - out[13] = @intCast((self.bcd_device >> 8) & 0xff); - out[14] = self.manufacturer_s; - out[15] = self.product_s; - out[16] = self.serial_s; - out[17] = self.num_configurations; - return out; - } -}; - -/// USB Device Qualifier Descriptor -/// This descriptor is mostly the same as the DeviceDescriptor -pub const DeviceQualifierDescriptor = struct { - /// Type of this descriptor, must be `Device`. - descriptor_type: DescType = DescType.DeviceQualifier, - /// Version of the device descriptor / USB protocol, in binary-coded - /// decimal. This is typically `0x01_10` for USB 1.1. - bcd_usb: u16, - /// Class of device, giving a broad functional area. - device_class: u8, - /// Subclass of device, refining the class. - device_subclass: u8, - /// Protocol within the subclass. - device_protocol: u8, - /// Maximum unit of data this device can move. - max_packet_size0: u8, - /// Number of configurations supported by this device. - num_configurations: u8, - /// Reserved for future use; must be 0 - reserved: u8 = 0, - - pub fn serialize(self: *const @This()) [10]u8 { - var out: [10]u8 = undefined; - out[0] = out.len; - out[1] = @intFromEnum(self.descriptor_type); - out[2] = @intCast(self.bcd_usb & 0xff); - out[3] = @intCast((self.bcd_usb >> 8) & 0xff); - out[4] = self.device_class; - out[5] = self.device_subclass; - out[6] = self.device_protocol; - out[7] = self.max_packet_size0; - out[8] = self.num_configurations; - out[9] = self.reserved; - return out; - } -}; - -/// Layout of an 8-byte USB SETUP packet. -pub const SetupPacket = extern struct { - request_type: u8, - /// Request. Standard setup requests are in the `SetupRequest` enum. - /// Devices can extend this with additional types as long as they don't - /// 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, - /// 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, -}; - // +++++++++++++++++++++++++++++++++++++++++++++++++ // Driver support stuctures // +++++++++++++++++++++++++++++++++++++++++++++++++ -pub const EndpointConfiguration = struct { - descriptor: *const EndpointDescriptor, - /// Index of this endpoint's control register in the `ep_control` array. - /// - /// TODO: this can be derived from the endpoint address, perhaps it should - /// be. - endpoint_control_index: ?usize, - /// Index of this endpoint's buffer control register in the - /// `ep_buffer_control` array. - /// - /// TODO this, too, can be derived. - buffer_control_index: usize, - - /// Index of this endpoint's data buffer in the array of data buffers - /// allocated from DPRAM. This can be arbitrary, and endpoints can even - /// share buffers if you're careful. - data_buffer_index: usize, - - /// Keeps track of which DATA PID (DATA0/DATA1) is expected on this endpoint - /// next. If `true`, we're expecting `DATA1`, otherwise `DATA0`. - next_pid_1: bool, - - /// Optional callback for custom OUT endpoints. This function will be called - /// if the device receives data on the corresponding endpoint. - callback: ?*const fn (dc: *DeviceConfiguration, data: []const u8) void = null, -}; - pub const DeviceConfiguration = struct { - device_descriptor: *const DeviceDescriptor, - interface_descriptor: *const InterfaceDescriptor, - config_descriptor: *const ConfigurationDescriptor, + device_descriptor: *const types.DeviceDescriptor, + config_descriptor: []const u8, lang_descriptor: []const u8, descriptor_strings: []const []const u8, - hid: ?struct { - hid_descriptor: *const hid.HidDescriptor, - report_descriptor: []const u8, - } = null, - endpoints: [4]*EndpointConfiguration, + drivers: []types.UsbClassDriver }; /// Buffer pointers, once they're prepared and initialized. @@ -788,13 +519,6 @@ pub const Buffers = struct { } }; -// Handy constants for the endpoints we use here -pub const EP0_IN_ADDR: u8 = Dir.In.endpoint(0); -pub const EP0_OUT_ADDR: u8 = Dir.Out.endpoint(0); -const EP1_OUT_ADDR: u8 = Dir.Out.endpoint(1); -const EP1_IN_ADDR: u8 = Dir.In.endpoint(1); -const EP2_IN_ADDR: u8 = Dir.In.endpoint(2); - /// USB interrupt status /// /// __Note__: Available interrupts may change from device to device. @@ -822,7 +546,7 @@ pub const EPBError = error{ /// Element returned by the endpoint buffer iterator (EPBIter) pub const EPB = struct { /// The endpoint the data belongs to - endpoint: *EndpointConfiguration, + endpoint_address: u8, /// Data buffer buffer: []u8, }; @@ -927,22 +651,24 @@ const BufferReader = struct { } }; -/// Convert an utf8 into an utf16 (little endian) string -pub fn utf8Toutf16Le(comptime s: []const u8) [s.len << 1]u8 { - const l = s.len << 1; - var ret: [l]u8 = .{0} ** l; - var i: usize = 0; - while (i < s.len) : (i += 1) { - ret[i << 1] = s[i]; +pub const UsbUtils = struct { + /// Convert an utf8 into an utf16 (little endian) string + pub fn utf8ToUtf16Le(comptime s: []const u8) [s.len << 1]u8 { + const l = s.len << 1; + var ret: [l]u8 = .{0} ** l; + var i: usize = 0; + while (i < s.len) : (i += 1) { + ret[i << 1] = s[i]; + } + return ret; } - return ret; -} +}; test "tests" { _ = hid; } test "utf8 to utf16" { - try std.testing.expectEqualSlices(u8, "M\x00y\x00 \x00V\x00e\x00n\x00d\x00o\x00r\x00", &utf8Toutf16Le("My Vendor")); - try std.testing.expectEqualSlices(u8, "R\x00a\x00s\x00p\x00b\x00e\x00r\x00r\x00y\x00 \x00P\x00i\x00", &utf8Toutf16Le("Raspberry Pi")); + try std.testing.expectEqualSlices(u8, "M\x00y\x00 \x00V\x00e\x00n\x00d\x00o\x00r\x00", &UsbUtils.utf8Toutf16Le("My Vendor")); + try std.testing.expectEqualSlices(u8, "R\x00a\x00s\x00p\x00b\x00e\x00r\x00r\x00y\x00 \x00P\x00i\x00", &UsbUtils.utf8Toutf16Le("Raspberry Pi")); } diff --git a/core/src/core/usb/cdc.zig b/core/src/core/usb/cdc.zig new file mode 100644 index 0000000..0a4159b --- /dev/null +++ b/core/src/core/usb/cdc.zig @@ -0,0 +1,254 @@ +const std = @import("std"); + +const types = @import("types.zig"); +const utils = @import("utils.zig"); + +const DescType = types.DescType; +const bos = utils.BosConfig; + +pub const DescSubType = enum(u8) { + Header = 0x00, + CallManagement = 0x01, + ACM = 0x02, + Union = 0x06, + + pub fn from_u16(v: u16) ?@This() { + return std.meta.intToEnum(@This(), v) catch null; + } +}; + +pub const CdcManagementRequestType = enum(u8) { + SetLineCoding = 0x20, + GetLineCoding = 0x21, + SetControlLineState = 0x22, + SendBreak = 0x23, + + pub fn from_u8(v: u8) ?@This() { + return std.meta.intToEnum(@This(), v) catch null; + } +}; + +pub const CdcCommSubClassType = enum(u8) { + AbstractControlModel = 2 +}; + +pub const CdcHeaderDescriptor = extern struct { + length: u8 = 5, + // Type of this descriptor, must be `ClassSpecific`. + descriptor_type: DescType = DescType.CsInterface, + // Subtype of this descriptor, must be `Header`. + descriptor_subtype: DescSubType = DescSubType.Header, + // USB Class Definitions for Communication Devices Specification release + // number in binary-coded decimal. Typically 0x01_10. + bcd_cdc: u16 align(1), + + pub fn serialize(self: *const @This()) [5]u8 { + var out: [5]u8 = undefined; + out[0] = out.len; + out[1] = @intFromEnum(self.descriptor_type); + out[2] = @intFromEnum(self.descriptor_subtype); + out[3] = @intCast(self.bcd_cdc & 0xff); + out[4] = @intCast((self.bcd_cdc >> 8) & 0xff); + return out; + } +}; + +pub const CdcCallManagementDescriptor = extern struct { + length: u8 = 5, + // Type of this descriptor, must be `ClassSpecific`. + descriptor_type: DescType = DescType.CsInterface, + // Subtype of this descriptor, must be `CallManagement`. + descriptor_subtype: DescSubType = DescSubType.CallManagement, + // Capabilities. Should be 0x00 for use as a serial device. + capabilities: u8, + // Data interface number. + data_interface: u8, + + pub fn serialize(self: *const @This()) [5]u8 { + var out: [5]u8 = undefined; + out[0] = out.len; + out[1] = @intFromEnum(self.descriptor_type); + out[2] = @intFromEnum(self.descriptor_subtype); + out[3] = self.capabilities; + out[4] = self.data_interface; + return out; + } +}; + +pub const CdcAcmDescriptor = extern struct { + length: u8 = 4, + // Type of this descriptor, must be `ClassSpecific`. + descriptor_type: DescType = DescType.CsInterface, + // Subtype of this descriptor, must be `ACM`. + descriptor_subtype: DescSubType = DescSubType.ACM, + // Capabilities. Should be 0x02 for use as a serial device. + capabilities: u8, + + pub fn serialize(self: *const @This()) [4]u8 { + var out: [4]u8 = undefined; + out[0] = out.len; + out[1] = @intFromEnum(self.descriptor_type); + out[2] = @intFromEnum(self.descriptor_subtype); + out[3] = self.capabilities; + return out; + } +}; + +pub const CdcUnionDescriptor = extern struct { + length: u8 = 5, + // Type of this descriptor, must be `ClassSpecific`. + descriptor_type: DescType = DescType.CsInterface, + // Subtype of this descriptor, must be `Union`. + descriptor_subtype: DescSubType = DescSubType.Union, + // The interface number of the communication or data class interface + // designated as the master or controlling interface for the union. + master_interface: u8, + // The interface number of the first slave or associated interface in the + // union. + slave_interface_0: u8, + + pub fn serialize(self: *const @This()) [5]u8 { + var out: [5]u8 = undefined; + out[0] = out.len; + out[1] = @intFromEnum(self.descriptor_type); + out[2] = @intFromEnum(self.descriptor_subtype); + out[3] = self.master_interface; + out[4] = self.slave_interface_0; + return out; + } +}; + +pub const CdcLineCoding = extern struct { + bit_rate: u32 align(1), + stop_bits: u8, + parity: u8, + data_bits: u8, +}; + +pub const CdcClassDriver = struct { + + device: ?types.UsbDevice = null, + ep_notif: u8 = 0, + ep_in: u8 = 0, + ep_out: u8 = 0, + + line_coding: CdcLineCoding = undefined, + + pub fn write(self: *@This(), data: []const u8) void { + // TODO - ugly hack, current limitation 63 chars (in future endpoints should implement ring buffers) + const max_size = 63; + var buf: [64]u8 = undefined; + const size = @min(data.len, max_size); + @memcpy(buf[0..size], data[0..size]); + buf[max_size-2] = '\r'; + buf[max_size-1] = '\n'; + const data_packet = buf[0..size]; + + if (self.device.?.ready() == false) { + return; + } + self.device.?.endpoint_transfer(self.ep_in, data_packet); + } + + fn init(ptr: *anyopaque, device: types.UsbDevice) void { + var self: *CdcClassDriver = @ptrCast(@alignCast(ptr)); + self.device = device; + self.line_coding = .{ + .bit_rate = 115200, + .stop_bits = 0, + .parity = 0, + .data_bits = 8 + }; + } + + fn open(ptr: *anyopaque, cfg: []const u8) !usize { + var self: *CdcClassDriver = @ptrCast(@alignCast(ptr)); + var curr_cfg = cfg; + + if (bos.try_get_desc_as(types.InterfaceDescriptor, curr_cfg)) |desc_itf| { + if (desc_itf.interface_class != @intFromEnum(types.ClassCode.Cdc)) return types.DriverErrors.UnsupportedInterfaceClassType; + if (desc_itf.interface_subclass != @intFromEnum(CdcCommSubClassType.AbstractControlModel)) return types.DriverErrors.UnsupportedInterfaceSubClassType; + } else { + return types.DriverErrors.ExpectedInterfaceDescriptor; + } + + curr_cfg = bos.get_desc_next(curr_cfg); + + while (curr_cfg.len > 0 and bos.get_desc_type(curr_cfg) == DescType.CsInterface) { + curr_cfg = bos.get_desc_next(curr_cfg); + } + + if (bos.try_get_desc_as(types.EndpointDescriptor, curr_cfg)) |desc_ep| { + self.ep_notif = desc_ep.endpoint_address; + curr_cfg = bos.get_desc_next(curr_cfg); + } + + if (bos.try_get_desc_as(types.InterfaceDescriptor, curr_cfg)) |desc_itf| { + if (desc_itf.interface_class == @intFromEnum(types.ClassCode.CdcData)) { + curr_cfg = bos.get_desc_next(curr_cfg); + for (0..2) |_| { + if (bos.try_get_desc_as(types.EndpointDescriptor, curr_cfg)) |desc_ep| { + switch (types.Endpoint.dir_from_address(desc_ep.endpoint_address)) { + .In => { self.ep_in = desc_ep.endpoint_address; }, + .Out => { self.ep_out = desc_ep.endpoint_address; }, + } + self.device.?.endpoint_open(curr_cfg[0..desc_ep.length]); + curr_cfg = bos.get_desc_next(curr_cfg); + } + } + } + } + + return cfg.len - curr_cfg.len; + } + + fn class_control(ptr: *anyopaque, stage: types.ControlStage, setup: *const types.SetupPacket) bool { + var self: *CdcClassDriver = @ptrCast(@alignCast(ptr)); + + if (CdcManagementRequestType.from_u8(setup.request)) |request| { + switch (request) { + .SetLineCoding => { + switch (stage) { + .Setup => { + // HACK, we should handle data phase somehow to read sent line_coding + self.device.?.control_ack(setup); + }, + else => {} + } + }, + .GetLineCoding => { + if (stage == .Setup) { + self.device.?.control_transfer(setup, std.mem.asBytes(&self.line_coding)); + } + }, + .SetControlLineState => { + switch (stage) { + .Setup => { + self.device.?.control_ack(setup); + }, + else => {} + } + }, + .SendBreak => { + switch (stage) { + .Setup => { + self.device.?.control_ack(setup); + }, + else => {} + } + } + } + } + + return true; + } + + pub fn driver(self: *@This()) types.UsbClassDriver { + return .{ + .ptr = self, + .fn_init = init, + .fn_open = open, + .fn_class_control = class_control + }; + } +}; \ No newline at end of file diff --git a/core/src/core/usb/descriptors.zig b/core/src/core/usb/descriptors.zig deleted file mode 100644 index 81416bc..0000000 --- a/core/src/core/usb/descriptors.zig +++ /dev/null @@ -1,112 +0,0 @@ -/// 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 { - /// Version of the device descriptor / USB protocol, in binary-coded - /// decimal. This is typically `0x01_10` for USB 1.1. - bcdUSB: u16, - /// Class of device, giving a broad functional area. - bDeviceClass: u8, - /// SubClass of device, refining the class. - bDeviceSubClass: u8, - /// Protocol within the subclass. - bDeviceProtocol: u8, - /// Maximum unit of data this device can move. - bMaxPacketSize0: u8, - /// ID of product vendor. - idVendor: u16, - /// ID of product. - idProduct: u16, - /// Device version number, as BCD again. - bcdDevice: u16, - /// Index of manufacturer name in string descriptor table. - iManufacturer: u8, - /// Index of product name in string descriptor table. - iPRoduct: u8, - /// Index of serial number in string descriptor table. - iSerialNumber: u8, - /// Number of configurations supported by this device. - bNumConfigurations: u8, -}; - -pub const Interface = extern struct { - bInterfaceNumber: u8, - bAlternateSetting: u8, - bNumEndpoints: u8, - bInterfaceClass: u8, - bInterfaceSubClass: u8, - bInterfaceProtocol: u8, - iInterface: u8, -}; - -/// Description of a single available device configuration. -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. - wTotalLength: u16, - /// Number of interface descriptors in this configuration. - bNumInterfaces: u8, - /// Number to use when requesting this configuration via a - /// `SetConfiguration` request. - bConfigurationValue: u8, - /// Index of this configuration's name in the string descriptor table. - iConfiguration: u8, - /// Bit set of device attributes: - /// - /// - Bit 7 should be set (indicates that device can be bus powered in USB - /// 1.0). - /// - Bit 6 indicates that the device can be self-powered. - /// - Bit 5 indicates that the device can signal remote wakeup of the host - /// (like a keyboard). - /// - The rest are reserved and should be zero. - bmAttributes: u8, - /// Maximum device power consumption in units of 2mA. - bMaxPower: u8, -}; - -pub const InterfaceAssociation = extern struct { - bFirstInterface: u8, - bInterfaceCount: u8, - bFunctionClass: u8, - bFunctionSubClass: u8, - bFunctionProtocol: u8, - iFunction: u8, -}; - -/// Describes an endpoint within an interface -pub const Endpoint = extern struct { - /// Address of this endpoint, where the bottom 4 bits give the endpoint - /// number (0..15) and the top bit distinguishes IN (1) from OUT (0). - bEndpointAddress: u8, - /// Endpoint attributes; the most relevant part is the bottom 2 bits, which - /// control the transfer type using the values from `TransferType`. - bmAttributes: u8, - /// Maximum packet size this endpoint can accept/produce. - wMaxPacketSize: u16, - /// Interval for polling interrupt/isochronous endpoints (which we don't - /// currently support) in milliseconds. - bInterval: u8, -}; - -pub const BOS = extern struct { - /// The number of bytes in this descriptor and all of its subordinate - /// descriptors - wTotalLength: u16, - /// The numbre of device capability descriptors subordinate to this BOS - /// descriptor - bNumDeviceCaps: u8, - -}; - -pub const Utf16Le = struct { - bytes: []const u8, -}; - -/// For string descriptor zero: an array of one or more language identifier -/// codes, for other string descriptors, a unicode UTF-16LE string -pub const String = extern struct { - string_or_lang: union { - bSTRING: Utf16Le, - wLANGID: []const u8, - }; -}; diff --git a/core/src/core/usb/hid.zig b/core/src/core/usb/hid.zig index 79f87d7..b10a91a 100644 --- a/core/src/core/usb/hid.zig +++ b/core/src/core/usb/hid.zig @@ -43,6 +43,12 @@ const std = @import("std"); +const types = @import("types.zig"); +const utils = @import("utils.zig"); + +const DescType = types.DescType; +const bos = utils.BosConfig; + // +++++++++++++++++++++++++++++++++++++++++++++++++ // Common Data Types // +++++++++++++++++++++++++++++++++++++++++++++++++ @@ -68,37 +74,46 @@ const std = @import("std"); // | ReportDescriptor | | PhysicalDesc | // ----------------------- --------------------- -pub const DescType = enum(u8) { - /// HID descriptor - Hid = 0x21, - /// Report descriptor - Report = 0x22, - /// Physical descriptor +pub const HidDescType = enum(u8) { + Hid = 0x21, + Report = 0x22, Physical = 0x23, - pub fn from_u16(v: u16) ?@This() { - return switch (v) { - 0x21 => @This().Hid, - 0x22 => @This().Report, - 0x23 => @This().Physical, - else => null, - }; + pub fn from_u8(v: u8) ?@This() { + return std.meta.intToEnum(@This(), v) catch null; + } +}; + +pub const HidRequestType = enum(u8) { + GetReport = 0x01, + GetIdle = 0x02, + GetProtocol = 0x03, + SetReport = 0x09, + SetIdle = 0x0a, + SetProtocol = 0x0b, + + pub fn from_u8(v: u8) ?@This() { + return std.meta.intToEnum(@This(), v) catch null; } }; /// USB HID descriptor pub const HidDescriptor = struct { - descriptor_type: DescType = DescType.Hid, + pub const const_descriptor_type = HidDescType.Hid; + + length: u8 = 9, + /// Type of this descriptor + descriptor_type: HidDescType = const_descriptor_type, /// Numeric expression identifying the HID Class Specification release - bcd_hid: u16, + bcd_hid: u16 align(1), /// Numeric expression identifying country code of the localized hardware country_code: u8, /// Numeric expression specifying the number of class descriptors num_descriptors: u8, /// Type of HID class report - report_type: DescType = DescType.Report, + report_type: HidDescType = HidDescType.Report, /// The total size of the Report descriptor - report_length: u16, + report_length: u16 align(1), pub fn serialize(self: *const @This()) [9]u8 { var out: [9]u8 = undefined; @@ -249,6 +264,7 @@ pub const LocalItem = enum(u4) { pub const UsageTable = struct { const fido: [2]u8 = "\xD0\xF1".*; + const vendor: [2]u8 = "\x00\xFF".*; }; pub const FidoAllianceUsage = struct { @@ -429,6 +445,126 @@ pub const ReportDescriptorFidoU2f = hid_usage_page(2, UsageTable.fido) // // End ++ hid_collection_end(); +pub const ReportDescriptorGenericInOut = hid_usage_page(2, UsageTable.vendor) // +++ hid_usage(1, "\x01".*) // +++ hid_collection(CollectionItem.Application) // +// Usage Data In +++ hid_usage(1, "\x02".*) // +++ hid_logical_min(1, "\x00".*) // +++ hid_logical_max(2, "\xff\x00".*) // +++ hid_report_size(1, "\x08".*) // +++ hid_report_count(1, "\x40".*) // +++ hid_input(HID_DATA | HID_VARIABLE | HID_ABSOLUTE) // +// Usage Data Out +++ hid_usage(1, "\x03".*) // +++ hid_logical_min(1, "\x00".*) // +++ hid_logical_max(2, "\xff\x00".*) // +++ hid_report_size(1, "\x08".*) // +++ hid_report_count(1, "\x40".*) // +++ hid_output(HID_DATA | HID_VARIABLE | HID_ABSOLUTE) // +// End +++ hid_collection_end(); + +pub const HidClassDriver = struct { + + device: ?types.UsbDevice = null, + ep_in: u8 = 0, + ep_out: u8 = 0, + hid_descriptor: []const u8 = &.{}, + report_descriptor: []const u8, + + fn init(ptr: *anyopaque, device: types.UsbDevice) void { + var self: *HidClassDriver = @ptrCast(@alignCast(ptr)); + self.device = device; + } + + fn open(ptr: *anyopaque, cfg: []const u8) !usize { + var self: *HidClassDriver = @ptrCast(@alignCast(ptr)); + var curr_cfg = cfg; + + if (bos.try_get_desc_as(types.InterfaceDescriptor, curr_cfg)) |desc_itf| { + if (desc_itf.interface_class != @intFromEnum(types.ClassCode.Hid)) return types.DriverErrors.UnsupportedInterfaceClassType; + } else { + return types.DriverErrors.ExpectedInterfaceDescriptor; + } + + curr_cfg = bos.get_desc_next(curr_cfg); + if (bos.try_get_desc_as(HidDescriptor, curr_cfg)) |_| { + self.hid_descriptor = curr_cfg[0..bos.get_desc_len(curr_cfg)]; + curr_cfg = bos.get_desc_next(curr_cfg); + } else { + return types.DriverErrors.UnexpectedDescriptor; + } + + for (0..2) |_| { + if (bos.try_get_desc_as(types.EndpointDescriptor, curr_cfg)) |desc_ep| { + switch (types.Endpoint.dir_from_address(desc_ep.endpoint_address)) { + .In => { self.ep_in = desc_ep.endpoint_address; }, + .Out => { self.ep_out = desc_ep.endpoint_address; }, + } + self.device.?.endpoint_open(curr_cfg[0..desc_ep.length]); + curr_cfg = bos.get_desc_next(curr_cfg); + } + } + + return cfg.len - curr_cfg.len; + } + + fn class_control(ptr: *anyopaque, stage: types.ControlStage, setup: *const types.SetupPacket) bool { + var self: *HidClassDriver = @ptrCast(@alignCast(ptr)); + + switch (setup.request_type.type) { + .Standard => { + if (stage == .Setup) { + const hid_desc_type = HidDescType.from_u8(@intCast((setup.value >> 8) & 0xff)); + const request_code = types.SetupRequest.from_u8(setup.request); + + if (hid_desc_type == null or request_code == null) { + return false; + } + + if (request_code.? == .GetDescriptor and hid_desc_type == .Hid) { + self.device.?.control_transfer(setup, self.hid_descriptor); + } else if (request_code.? == .GetDescriptor and hid_desc_type == .Report) { + self.device.?.control_transfer(setup, self.report_descriptor); + } else { + return false; + } + } + }, + .Class => { + const hid_request_type = HidRequestType.from_u8(setup.request); + if (hid_request_type == null) return false; + + switch (hid_request_type.?) { + .SetIdle => { + if (stage == .Setup) { + self.device.?.control_ack(setup); + } + }, + else => { + return false; + } + } + }, + else => { + return false; + } + } + + return true; + } + + pub fn driver(self: *@This()) types.UsbClassDriver { + return .{ + .ptr = self, + .fn_init = init, + .fn_open = open, + .fn_class_control = class_control + }; + } +}; + test "create hid report item" { const r = hid_report_item( 2, diff --git a/core/src/core/usb/templates.zig b/core/src/core/usb/templates.zig new file mode 100644 index 0000000..4d8d3f7 --- /dev/null +++ b/core/src/core/usb/templates.zig @@ -0,0 +1,56 @@ +const types = @import("types.zig"); +const hid = @import("hid.zig"); +const cdc = @import("cdc.zig"); + +pub const DescriptorsConfigTemplates = struct { + pub const config_descriptor_len = 9; + + pub fn config_descriptor(config_num: u8, interfaces_num: u8, string_index: u8, total_len: u16, attributes: u8, max_power_ma: u9) [9]u8 { + const desc1 = types.ConfigurationDescriptor { .total_length = total_len, .num_interfaces = interfaces_num, .configuration_value = config_num, .configuration_s = string_index, .attributes = 0b01000000 | attributes, .max_power = max_power_ma/2 }; + return desc1.serialize(); + } + + pub const cdc_descriptor_len = 8 + 9 + 5 + 5 + 4 + 5 + 7 + 9 + 7 + 7; + + pub fn cdc_descriptor(interface_number: u8, string_index: u8, endpoint_notifi_address: u8, endpoint_notifi_size: u16, endpoint_out_address: u8, endpoint_in_address: u8, endpoint_size: u16) [cdc_descriptor_len]u8 { + const desc1 = types.InterfaceAssociationDescriptor { .first_interface = interface_number, .interface_count = 2, .function_class = 2, .function_subclass = 2, .function_protocol = 0, .function = 0 }; + const desc2 = types.InterfaceDescriptor { .interface_number = interface_number, .alternate_setting = 0, .num_endpoints = 1, .interface_class = 2, .interface_subclass = 2, .interface_protocol = 0, .interface_s = string_index }; + const desc3 = cdc.CdcHeaderDescriptor { .descriptor_type = .CsInterface, .descriptor_subtype = .Header, .bcd_cdc = 0x0120 }; + const desc4 = cdc.CdcCallManagementDescriptor { .descriptor_type = .CsInterface, .descriptor_subtype = .CallManagement, .capabilities = 0, .data_interface = interface_number + 1 }; + const desc5 = cdc.CdcAcmDescriptor { .descriptor_type = .CsInterface, .descriptor_subtype = .ACM, .capabilities = 6 }; + const desc6 = cdc.CdcUnionDescriptor { .descriptor_type = .CsInterface, .descriptor_subtype = .Union, .master_interface = interface_number, .slave_interface_0 = interface_number + 1 }; + const desc7 = types.EndpointDescriptor { .endpoint_address = endpoint_notifi_address, .attributes = @intFromEnum(types.TransferType.Interrupt), .max_packet_size = endpoint_notifi_size, .interval = 16 }; + const desc8 = types.InterfaceDescriptor { .interface_number = interface_number + 1, .alternate_setting = 0, .num_endpoints = 2, .interface_class = 10, .interface_subclass = 0, .interface_protocol = 0, .interface_s = 0 }; + const desc9 = types.EndpointDescriptor { .endpoint_address = endpoint_out_address, .attributes = @intFromEnum(types.TransferType.Bulk), .max_packet_size = endpoint_size, .interval = 0 }; + const desc10 = types.EndpointDescriptor { .endpoint_address = endpoint_in_address, .attributes = @intFromEnum(types.TransferType.Bulk), .max_packet_size = endpoint_size, .interval = 0 }; + return desc1.serialize() ++ desc2.serialize() ++ desc3.serialize() ++ desc4.serialize() ++ desc5.serialize() ++ desc6.serialize() ++ desc7.serialize() ++ desc8.serialize() ++ desc9.serialize() ++ desc10.serialize(); + } + + pub const hid_in_descriptor_len = 9 + 9 + 7; + + pub fn hid_in_descriptor(interface_number: u8, string_index: u8, boot_protocol: u8, report_desc_len: u16, endpoint_in_address: u8, endpoint_size: u16, endpoint_interval: u16) [hid_in_descriptor_len]u8 { + const desc1 = types.InterfaceDescriptor { .interface_number = interface_number, .alternate_setting = 0, .num_endpoints = 1, .interface_class = 3, .interface_subclass = if (boot_protocol > 0) 1 else 0, .interface_protocol = boot_protocol, .interface_s = string_index }; + const desc2 = hid.HidDescriptor { .bcd_hid = 0x0111, .country_code = 0, .num_descriptors = 1, .report_length = report_desc_len }; + const desc3 = types.EndpointDescriptor { .endpoint_address = endpoint_in_address, .attributes = @intFromEnum(types.TransferType.Interrupt), .max_packet_size = endpoint_size, .interval = endpoint_interval }; + return desc1.serialize() ++ desc2.serialize() ++ desc3.serialize(); + } + + pub const hid_in_out_descriptor_len = 9 + 9 + 7 + 7; + + pub fn hid_in_out_descriptor(interface_number: u8, string_index: u8, boot_protocol: u8, report_desc_len: u16, endpoint_out_address: u8, endpoint_in_address: u8, endpoint_size: u16, endpoint_interval: u16) [hid_in_out_descriptor_len]u8 { + const desc1 = types.InterfaceDescriptor { .interface_number = interface_number, .alternate_setting = 0, .num_endpoints = 2, .interface_class = 3, .interface_subclass = if (boot_protocol > 0) 1 else 0, .interface_protocol = boot_protocol, .interface_s = string_index }; + const desc2 = hid.HidDescriptor { .bcd_hid = 0x0111, .country_code = 0, .num_descriptors = 1, .report_length = report_desc_len }; + const desc3 = types.EndpointDescriptor { .endpoint_address = endpoint_out_address, .attributes = @intFromEnum(types.TransferType.Interrupt), .max_packet_size = endpoint_size, .interval = endpoint_interval }; + const desc4 = types.EndpointDescriptor { .endpoint_address = endpoint_in_address, .attributes = @intFromEnum(types.TransferType.Interrupt), .max_packet_size = endpoint_size, .interval = endpoint_interval }; + return desc1.serialize() ++ desc2.serialize() ++ desc3.serialize() ++ desc4.serialize(); + } + + pub const vendor_descriptor_len = 9 + 7 + 7; + + pub fn vendor_descriptor(interface_number: u8, string_index: u8, endpoint_out_address: u8, endpoint_in_address: u8, endpoint_size: u16) [vendor_descriptor_len]u8 { + const desc1 = types.InterfaceDescriptor { .interface_number = interface_number, .alternate_setting = 0, .num_endpoints = 2, .interface_class = 0xff, .interface_subclass = 0, .interface_protocol = 0, .interface_s = string_index }; + const desc2 = types.EndpointDescriptor { .endpoint_address = endpoint_out_address, .attributes = @intFromEnum(types.TransferType.Bulk), .max_packet_size = endpoint_size, .interval = 0 }; + const desc3 = types.EndpointDescriptor { .endpoint_address = endpoint_in_address, .attributes = @intFromEnum(types.TransferType.Bulk), .max_packet_size = endpoint_size, .interval = 0 }; + return desc1.serialize() ++ desc2.serialize() ++ desc3.serialize(); + } +}; \ No newline at end of file diff --git a/core/src/core/usb/types.zig b/core/src/core/usb/types.zig new file mode 100644 index 0000000..a669930 --- /dev/null +++ b/core/src/core/usb/types.zig @@ -0,0 +1,476 @@ +const std = @import("std"); + +pub const DescType = enum(u8) { + Device = 0x01, + Config = 0x02, + String = 0x03, + Interface = 0x04, + Endpoint = 0x05, + DeviceQualifier = 0x06, + InterfaceAssociation = 0x0b, + CsDevice = 0x21, + CsConfig = 0x22, + CsString = 0x23, + CsInterface = 0x24, + CsEndpoint = 0x25, + + pub fn from_u8(v: u8) ?@This() { + return std.meta.intToEnum(@This(), v) catch null; + } +}; + +pub const ClassCode = enum(u8) { + Unspecified = 0, + Audio = 1, + Cdc = 2, + Hid = 3, + CdcData = 10, +}; + +/// Types of transfer that can be indicated by the `attributes` field on `EndpointDescriptor`. +pub const TransferType = enum(u2) { + Control = 0, + Isochronous = 1, + Bulk = 2, + Interrupt = 3, + + pub fn from_u8(v: u8) ?@This() { + return std.meta.intToEnum(@This(), v) catch null; + } + + pub inline fn as_number(self: @This()) u2 { + return @intFromEnum(self); + } +}; + +pub const ControlStage = enum { + Idle, + Setup, + Data, + Ack +}; + +/// The types of USB SETUP requests that we understand. +pub const SetupRequest = enum(u8) { + GetDescriptor = 0x06, + SetAddress = 0x05, + SetConfiguration = 0x09, + + pub fn from_u8(v: u8) ?@This() { + return std.meta.intToEnum(@This(), v) catch null; + } +}; + +/// USB deals in two different transfer directions, called OUT (host-to-device) +/// and IN (device-to-host). In the vast majority of cases, OUT is represented +/// by a 0 byte, and IN by an `0x80` byte. +pub const Dir = enum(u1) { + Out = 0, + In = 1, + + pub const DIR_IN_MASK = 0x80; + + pub inline fn as_number(self: @This()) u1 { + return @intFromEnum(self); + } + + pub inline fn as_number_reversed(self: @This()) u1 { + return ~@intFromEnum(self); + } +}; + +pub const Endpoint = struct { + pub inline fn to_address(num: u8, dir: Dir) u8 { + return switch (dir) { + .Out => num, + .In => num | Dir.DIR_IN_MASK + }; + } + + pub inline fn num_from_address(addr: u8) u8 { + return addr & ~@as(u8, @intCast(Dir.DIR_IN_MASK)); + } + + pub inline fn dir_from_address(addr: u8) Dir { + return if (addr & Dir.DIR_IN_MASK != 0) Dir.In else Dir.Out; + } + + pub const EP0_OUT_IDX = 0; + pub const EP0_IN_IDX = 1; + + pub const EP0_IN_ADDR: u8 = to_address(0, .In); + pub const EP0_OUT_ADDR: u8 = to_address(0, .Out); +}; + +pub const RequestType = packed struct(u8) { + recipient: Recipient, + type: Type, + direction: Dir, + + const Type = enum(u2) { + Standard, + Class, + Vendor, + Other + }; + + /// RequestType is created from raw bytes using std.mem.bytesToValue, we need to support all potential values if we don't want to crash such conversion + const Recipient = enum(u5) { + Device, + Interface, + Endpoint, + Other, + Reserved1, + Reserved2, + Reserved3, + Reserved4, + Reserved5, + Reserved6, + Reserved7, + Reserved8, + Reserved9, + Reserved10, + Reserved11, + Reserved12, + Reserved13, + Reserved14, + Reserved15, + Reserved16, + Reserved17, + Reserved18, + Reserved19, + Reserved20, + Reserved21, + Reserved22, + Reserved23, + Reserved24, + Reserved25, + Reserved26, + Reserved27, + Reserved28 + }; +}; + +/// Layout of an 8-byte USB SETUP packet. +pub const SetupPacket = extern struct { + /// Request metadata: recipient, type, direction. + request_type: RequestType, + /// Request. Standard setup requests are in the `SetupRequest` enum. + /// Devices can extend this with additional types as long as they don't + /// 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, + /// 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, +}; + +/// Describes an endpoint within an interface +pub const EndpointDescriptor = extern struct { + pub const const_descriptor_type = DescType.Endpoint; + + length: u8 = 7, + /// Type of this descriptor, must be `Endpoint`. + descriptor_type: DescType = const_descriptor_type, + /// Address of this endpoint, where the bottom 4 bits give the endpoint + /// number (0..15) and the top bit distinguishes IN (1) from OUT (0). + endpoint_address: u8, + /// Endpoint attributes; the most relevant part is the bottom 2 bits, which + /// control the transfer type using the values from `TransferType`. + attributes: u8, + /// Maximum packet size this endpoint can accept/produce. + max_packet_size: u16 align(1), + /// Interval for polling interrupt/isochronous endpoints (which we don't + /// currently support) in milliseconds. + interval: u8, + + pub fn serialize(self: *const @This()) [7]u8 { + var out: [7]u8 = undefined; + out[0] = out.len; + out[1] = @intFromEnum(self.descriptor_type); + out[2] = self.endpoint_address; + out[3] = self.attributes; + out[4] = @intCast(self.max_packet_size & 0xff); + out[5] = @intCast((self.max_packet_size >> 8) & 0xff); + out[6] = self.interval; + return out; + } +}; + +/// Description of an interface within a configuration. +pub const InterfaceDescriptor = extern struct { + pub const const_descriptor_type = DescType.Interface; + + length: u8 = 9, + /// Type of this descriptor, must be `Interface`. + descriptor_type: DescType = const_descriptor_type, + /// ID of this interface. + interface_number: u8, + /// Allows a single `interface_number` to have several alternate interface + /// settings, where each alternate increments this field. Normally there's + /// only one, and `alternate_setting` is zero. + 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, + /// Index of interface name within string descriptor table. + interface_s: u8, + + pub fn serialize(self: *const @This()) [9]u8 { + var out: [9]u8 = undefined; + out[0] = out.len; + out[1] = @intFromEnum(self.descriptor_type); + out[2] = self.interface_number; + out[3] = self.alternate_setting; + out[4] = self.num_endpoints; + out[5] = self.interface_class; + out[6] = self.interface_subclass; + out[7] = self.interface_protocol; + out[8] = self.interface_s; + return out; + } +}; + +/// USB interface association descriptor (IAD) allows the device to group interfaces that belong to a function. +pub const InterfaceAssociationDescriptor = extern struct { + pub const const_descriptor_type = DescType.InterfaceAssociation; + + length: u8 = 8, + // Type of this descriptor, must be `InterfaceAssociation`. + descriptor_type: DescType = const_descriptor_type, + // First interface number of the set of interfaces that follow this + // descriptor. + first_interface: u8, + // The number of interfaces that follow this descriptor that are considered + // associated. + interface_count: u8, + // The interface class used for associated interfaces. + function_class: u8, + // The interface subclass used for associated interfaces. + function_subclass: u8, + // The interface protocol used for associated interfaces. + function_protocol: u8, + // Index of the string descriptor describing the associated interfaces. + function: u8, + + pub fn serialize(self: *const @This()) [8]u8 { + var out: [8]u8 = undefined; + out[0] = out.len; + out[1] = @intFromEnum(self.descriptor_type); + out[2] = self.first_interface; + out[3] = self.interface_count; + out[4] = self.function_class; + out[5] = self.function_subclass; + out[6] = self.function_protocol; + out[7] = self.function; + return out; + } +}; + +/// Description of a single available device configuration. +pub const ConfigurationDescriptor = extern struct { + pub const const_descriptor_type = DescType.Config; + + length: u8 = 9, + /// Type of this descriptor, must be `Config`. + descriptor_type: DescType = const_descriptor_type, + /// 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: u16 align(1), + /// Number of interface descriptors in this configuration. + num_interfaces: u8, + /// Number to use when requesting this configuration via a + /// `SetConfiguration` request. + configuration_value: u8, + /// Index of this configuration's name in the string descriptor table. + configuration_s: u8, + /// Bit set of device attributes: + /// + /// - Bit 7 should be set (indicates that device can be bus powered in USB + /// 1.0). + /// - Bit 6 indicates that the device can be self-powered. + /// - Bit 5 indicates that the device can signal remote wakeup of the host + /// (like a keyboard). + /// - The rest are reserved and should be zero. + attributes: u8, + /// Maximum device power consumption in units of 2mA. + max_power: u8, + + pub fn serialize(self: *const @This()) [9]u8 { + var out: [9]u8 = undefined; + out[0] = out.len; + out[1] = @intFromEnum(self.descriptor_type); + out[2] = @intCast(self.total_length & 0xff); + out[3] = @intCast((self.total_length >> 8) & 0xff); + out[4] = self.num_interfaces; + out[5] = self.configuration_value; + out[6] = self.configuration_s; + out[7] = self.attributes; + out[8] = self.max_power; + return out; + } +}; + +/// Describes a device. This is the most broad description in USB and is +/// typically the first thing the host asks for. +pub const DeviceDescriptor = extern struct { + pub const const_descriptor_type = DescType.Device; + + length: u8 = 18, + /// Type of this descriptor, must be `Device`. + descriptor_type: DescType = const_descriptor_type, + /// Version of the device descriptor / USB protocol, in binary-coded + /// decimal. This is typically `0x01_10` for USB 1.1. + bcd_usb: u16 align(1), + /// Class of device, giving a broad functional area. + device_class: u8, + /// Subclass of device, refining the class. + device_subclass: u8, + /// Protocol within the subclass. + device_protocol: u8, + /// Maximum unit of data this device can move. + max_packet_size0: u8, + /// ID of product vendor. + vendor: u16 align(1), + /// ID of product. + product: u16 align(1), + /// Device version number, as BCD again. + bcd_device: u16 align(1), + /// Index of manufacturer name in string descriptor table. + manufacturer_s: u8, + /// Index of product name in string descriptor table. + product_s: u8, + /// Index of serial number in string descriptor table. + serial_s: u8, + /// Number of configurations supported by this device. + num_configurations: u8, + + pub fn serialize(self: *const @This()) [18]u8 { + var out: [18]u8 = undefined; + out[0] = out.len; + out[1] = @intFromEnum(self.descriptor_type); + out[2] = @intCast(self.bcd_usb & 0xff); + out[3] = @intCast((self.bcd_usb >> 8) & 0xff); + out[4] = self.device_class; + out[5] = self.device_subclass; + out[6] = self.device_protocol; + out[7] = self.max_packet_size0; + out[8] = @intCast(self.vendor & 0xff); + out[9] = @intCast((self.vendor >> 8) & 0xff); + out[10] = @intCast(self.product & 0xff); + out[11] = @intCast((self.product >> 8) & 0xff); + out[12] = @intCast(self.bcd_device & 0xff); + out[13] = @intCast((self.bcd_device >> 8) & 0xff); + out[14] = self.manufacturer_s; + out[15] = self.product_s; + out[16] = self.serial_s; + out[17] = self.num_configurations; + return out; + } +}; + +/// USB Device Qualifier Descriptor +/// This descriptor is mostly the same as the DeviceDescriptor +pub const DeviceQualifierDescriptor = extern struct { + pub const const_descriptor_type = DescType.DeviceQualifier; + + length: u8 = 10, + /// Type of this descriptor, must be `Device`. + descriptor_type: DescType = const_descriptor_type, + /// Version of the device descriptor / USB protocol, in binary-coded + /// decimal. This is typically `0x01_10` for USB 1.1. + bcd_usb: u16 align(1), + /// Class of device, giving a broad functional area. + device_class: u8, + /// Subclass of device, refining the class. + device_subclass: u8, + /// Protocol within the subclass. + device_protocol: u8, + /// Maximum unit of data this device can move. + max_packet_size0: u8, + /// Number of configurations supported by this device. + num_configurations: u8, + /// Reserved for future use; must be 0 + reserved: u8 = 0, + + pub fn serialize(self: *const @This()) [10]u8 { + var out: [10]u8 = undefined; + out[0] = out.len; + out[1] = @intFromEnum(self.descriptor_type); + out[2] = @intCast(self.bcd_usb & 0xff); + out[3] = @intCast((self.bcd_usb >> 8) & 0xff); + out[4] = self.device_class; + out[5] = self.device_subclass; + out[6] = self.device_protocol; + out[7] = self.max_packet_size0; + out[8] = self.num_configurations; + out[9] = self.reserved; + return out; + } +}; + +pub const DriverErrors = error { + ExpectedInterfaceDescriptor, + UnsupportedInterfaceClassType, + UnsupportedInterfaceSubClassType, + UnexpectedDescriptor +}; + +pub const UsbDevice = struct { + fn_ready: *const fn () bool, + fn_control_transfer: *const fn (setup: *const SetupPacket, data: []const u8) void, + fn_control_ack: *const fn (setup: *const SetupPacket) void, + fn_endpoint_open: *const fn (ep_desc: []const u8) void, + fn_endpoint_transfer: *const fn (ep_addr: u8, data: []const u8) void, + + pub fn ready(self: *@This()) bool { + return self.fn_ready(); + } + + pub fn control_transfer(self: *@This(), setup: *const SetupPacket, data: []const u8) void { + return self.fn_control_transfer(setup, data); + } + + pub fn control_ack(self: *@This(), setup: *const SetupPacket) void { + return self.fn_control_ack(setup); + } + + pub fn endpoint_open(self: *@This(), ep_desc: []const u8) void { + return self.fn_endpoint_open(ep_desc); + } + + pub fn endpoint_transfer(self: *@This(), ep_addr: u8, data: []const u8) void { + return self.fn_endpoint_transfer(ep_addr, data); + } +}; + +/// USB Class driver interface +pub const UsbClassDriver = struct { + ptr: *anyopaque, + fn_init: *const fn (ptr: *anyopaque, device: UsbDevice) void, + fn_open: *const fn (ptr: *anyopaque, cfg: []const u8) anyerror!usize, + fn_class_control: *const fn (ptr: *anyopaque, stage: ControlStage, setup: *const SetupPacket) bool, + + pub fn init(self: *@This(), device: UsbDevice) void { + return self.fn_init(self.ptr, device); + } + + /// Driver open (set config) operation. Must return length of consumed driver config data. + pub fn open(self: *@This(), cfg: []const u8) !usize { + return self.fn_open(self.ptr, cfg); + } + + pub fn class_control(self: *@This(), stage: ControlStage, setup: *const SetupPacket) bool { + return self.fn_class_control(self.ptr, stage, setup); + } +}; \ No newline at end of file diff --git a/core/src/core/usb/utils.zig b/core/src/core/usb/utils.zig new file mode 100644 index 0000000..ec34446 --- /dev/null +++ b/core/src/core/usb/utils.zig @@ -0,0 +1,53 @@ +const std = @import("std"); +const types = @import("types.zig"); + +pub const BosConfig = struct { + const DESC_OFFSET_LEN = 0; + const DESC_OFFSET_TYPE = 1; + + pub fn get_desc_len(bos_cfg: []const u8) u8 { + return bos_cfg[DESC_OFFSET_LEN]; + } + + pub fn get_desc_type(bos_cfg: []const u8) ?types.DescType { + return types.DescType.from_u8(bos_cfg[DESC_OFFSET_TYPE]); + } + + pub fn get_data_u8(bos_cfg: []const u8, offset: u16) u8 { + return bos_cfg[offset]; + } + + pub fn get_data_u16(bos_cfg: []const u8, offset: u16) u16 { + const low_byte: u16 = bos_cfg[offset]; + const high_byte: u16 = bos_cfg[offset+1]; + return (high_byte << 8) | low_byte; + } + + /// Only for temporal u8 fields use as u16 fields will have wrong values because BOS endianness + pub fn get_desc_as(comptime T: type, bos_cfg: []const u8) *const T { + return @ptrCast(@constCast(bos_cfg.ptr)); + } + + /// Only for temporal u8 fields use as u16 fields will have wrong values because BOS endianness + pub fn try_get_desc_as(comptime T: type, bos_cfg: []const u8) ?*const T { + if (bos_cfg.len == 0) return null; + const exp_desc_type = @field(T, "const_descriptor_type"); + const cfg_desc_type = bos_cfg[DESC_OFFSET_TYPE]; + if (cfg_desc_type != @intFromEnum(exp_desc_type)) { + return null; + } + else { + return @constCast(@ptrCast(bos_cfg.ptr)); + } + } + + pub fn get_desc_next(bos_cfg: []const u8) []const u8 { + const len = bos_cfg[DESC_OFFSET_LEN]; + return bos_cfg[len..]; + } +}; + +test "Test try_get_desc_as" { + const cfg = [_]u8{ 7, 5, 129, 3, 8, 0, 16 }; + try std.testing.expect(BosConfig.try_get_desc_as(types.EndpointDescriptor, cfg[0..]) != null); +} \ No newline at end of file diff --git a/core/src/core/usb/vendor.zig b/core/src/core/usb/vendor.zig new file mode 100644 index 0000000..0026049 --- /dev/null +++ b/core/src/core/usb/vendor.zig @@ -0,0 +1,26 @@ +const std = @import("std"); + +const types = @import("types.zig"); + +pub const VendorClassDriver = struct { + + fn init(_: *anyopaque, _: types.UsbDevice) void { + } + + fn open(_: *anyopaque, _: []const u8) !usize { + return 0; + } + + pub fn class_control(_: *anyopaque, _: types.ControlStage, _: *const types.SetupPacket) bool { + return true; + } + + pub fn driver(self: *@This()) types.UsbClassDriver { + return .{ + .ptr = self, + .fn_init = init, + .fn_open = open, + .fn_class_control = class_control + }; + } +}; \ No newline at end of file diff --git a/examples/raspberrypi/rp2040/build.zig b/examples/raspberrypi/rp2040/build.zig index 5be2fad..29f17a6 100644 --- a/examples/raspberrypi/rp2040/build.zig +++ b/examples/raspberrypi/rp2040/build.zig @@ -15,8 +15,8 @@ const available_examples = [_]Example{ .{ .target = rp2040.boards.raspberrypi.pico, .name = "pico_spi-master", .file = "src/spi_master.zig" }, .{ .target = rp2040.boards.raspberrypi.pico, .name = "pico_squarewave", .file = "src/squarewave.zig" }, .{ .target = rp2040.boards.raspberrypi.pico, .name = "pico_uart", .file = "src/uart.zig" }, - .{ .target = rp2040.boards.raspberrypi.pico, .name = "pico_usb-device", .file = "src/usb_device.zig" }, .{ .target = rp2040.boards.raspberrypi.pico, .name = "pico_usb-hid", .file = "src/usb_hid.zig" }, + .{ .target = rp2040.boards.raspberrypi.pico, .name = "pico_usb-cdc", .file = "src/usb_cdc.zig" }, .{ .target = rp2040.boards.raspberrypi.pico, .name = "pico_ws2812", .file = "src/ws2812.zig" }, .{ .target = rp2040.boards.raspberrypi.pico, .name = "pico_multicore" , .file = "src/blinky_core1.zig" }, diff --git a/examples/raspberrypi/rp2040/src/usb_cdc.zig b/examples/raspberrypi/rp2040/src/usb_cdc.zig new file mode 100644 index 0000000..0d042aa --- /dev/null +++ b/examples/raspberrypi/rp2040/src/usb_cdc.zig @@ -0,0 +1,105 @@ +const std = @import("std"); +const microzig = @import("microzig"); + +const rp2040 = microzig.hal; +const flash = rp2040.flash; +const time = rp2040.time; +const gpio = rp2040.gpio; +const clocks = rp2040.clocks; +const usb = rp2040.usb; + +const led = gpio.num(25); +const uart = rp2040.uart.num(0); +const baud_rate = 115200; +const uart_tx_pin = gpio.num(0); +const uart_rx_pin = gpio.num(1); + +const usb_config_len = usb.templates.config_descriptor_len + usb.templates.cdc_descriptor_len; +const usb_config_descriptor = + usb.templates.config_descriptor(1, 2, 0, usb_config_len, 0xc0, 100) ++ + usb.templates.cdc_descriptor(0, 4, usb.Endpoint.to_address(1, .In), 8, usb.Endpoint.to_address(2, .Out), usb.Endpoint.to_address(2, .In), 64); + +var driver_cdc = usb.cdc.CdcClassDriver{}; +var drivers = [_]usb.types.UsbClassDriver{ driver_cdc.driver() }; + +// This is our device configuration +pub var DEVICE_CONFIGURATION: usb.DeviceConfiguration = .{ + .device_descriptor = &.{ + .descriptor_type = usb.DescType.Device, + .bcd_usb = 0x0200, + .device_class = 0xEF, + .device_subclass = 2, + .device_protocol = 1, + .max_packet_size0 = 64, + .vendor = 0x2E8A, + .product = 0x000a, + .bcd_device = 0x0100, + .manufacturer_s = 1, + .product_s = 2, + .serial_s = 0, + .num_configurations = 1, + }, + .config_descriptor = &usb_config_descriptor, + .lang_descriptor = "\x04\x03\x09\x04", // length || string descriptor (0x03) || Engl (0x0409) + .descriptor_strings = &.{ + &usb.utils.utf8ToUtf16Le("Raspberry Pi"), + &usb.utils.utf8ToUtf16Le("Pico Test Device"), + &usb.utils.utf8ToUtf16Le("someserial"), + &usb.utils.utf8ToUtf16Le("Board CDC"), + }, + .drivers = &drivers +}; + +pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { + std.log.err("panic: {s}", .{message}); + @breakpoint(); + while (true) {} +} + +pub const microzig_options = .{ + .log_level = .debug, + .logFn = rp2040.uart.log, +}; + +pub fn main() !void { + led.set_function(.sio); + led.set_direction(.out); + led.put(1); + + uart.apply(.{ + .baud_rate = baud_rate, + .tx_pin = uart_tx_pin, + .rx_pin = uart_rx_pin, + .clock_config = rp2040.clock_config, + }); + + rp2040.uart.init_logger(uart); + + // First we initialize the USB clock + rp2040.usb.Usb.init_clk(); + // Then initialize the USB device using the configuration defined above + rp2040.usb.Usb.init_device(&DEVICE_CONFIGURATION) catch unreachable; + var old: u64 = time.get_time_since_boot().to_us(); + var new: u64 = 0; + + var i: u32 = 0; + var buf: [1024]u8 = undefined; + while (true) { + // You can now poll for USB events + rp2040.usb.Usb.task( + false, // debug output over UART [Y/n] + ) catch unreachable; + + new = time.get_time_since_boot().to_us(); + if (new - old > 500000) { + old = new; + led.toggle(); + i += 1; + // uart log + std.log.info("cdc test: {}\r\n", .{i}); + // usb log (at this moment 63 bytes is max limit per single call) + const text = std.fmt.bufPrint(&buf, "cdc test: {}\r\n", .{i}) catch &.{}; + driver_cdc.write(text); + } + } +} \ No newline at end of file diff --git a/examples/raspberrypi/rp2040/src/usb_device.zig b/examples/raspberrypi/rp2040/src/usb_device.zig deleted file mode 100644 index 8aa5145..0000000 --- a/examples/raspberrypi/rp2040/src/usb_device.zig +++ /dev/null @@ -1,169 +0,0 @@ -const std = @import("std"); -const microzig = @import("microzig"); - -const rp2040 = microzig.hal; -const flash = rp2040.flash; -const time = rp2040.time; -const gpio = rp2040.gpio; -const clocks = rp2040.clocks; -const usb = rp2040.usb; - -const led = gpio.num(25); -const uart = rp2040.uart.num(0); -const baud_rate = 115200; -const uart_tx_pin = gpio.num(0); -const uart_rx_pin = gpio.num(1); - -// First we define two callbacks that will be used by the endpoints we define next... -fn ep1_in_callback(dc: *usb.DeviceConfiguration, data: []const u8) void { - _ = data; - // The host has collected the data we repeated onto - // EP1! Set up to receive more data on EP1. - usb.Usb.callbacks.usb_start_rx( - dc.endpoints[2], // EP1_OUT_CFG, - 64, - ); -} - -fn ep1_out_callback(dc: *usb.DeviceConfiguration, data: []const u8) void { - // We've gotten data from the host on our custom - // EP1! Set up EP1 to repeat it. - usb.Usb.callbacks.usb_start_tx( - dc.endpoints[3], // EP1_IN_CFG, - data, - ); -} - -// The endpoints EP0_IN and EP0_OUT are already defined but you can -// add your own endpoints to... -pub var EP1_OUT_CFG: usb.EndpointConfiguration = .{ - .descriptor = &usb.EndpointDescriptor{ - .descriptor_type = usb.DescType.Endpoint, - .endpoint_address = usb.Dir.Out.endpoint(1), - .attributes = @intFromEnum(usb.TransferType.Bulk), - .max_packet_size = 64, - .interval = 0, - }, - .endpoint_control_index = 2, - .buffer_control_index = 3, - .data_buffer_index = 2, - .next_pid_1 = false, - // The callback will be executed if we got an interrupt on EP1_OUT - .callback = ep1_out_callback, -}; - -pub var EP1_IN_CFG: usb.EndpointConfiguration = .{ - .descriptor = &usb.EndpointDescriptor{ - .descriptor_type = usb.DescType.Endpoint, - .endpoint_address = usb.Dir.In.endpoint(1), - .attributes = @intFromEnum(usb.TransferType.Bulk), - .max_packet_size = 64, - .interval = 0, - }, - .endpoint_control_index = 1, - .buffer_control_index = 2, - .data_buffer_index = 3, - .next_pid_1 = false, - // The callback will be executed if we got an interrupt on EP1_IN - .callback = ep1_in_callback, -}; - -// This is our device configuration -pub var DEVICE_CONFIGURATION: usb.DeviceConfiguration = .{ - .device_descriptor = &.{ - .descriptor_type = usb.DescType.Device, - .bcd_usb = 0x0110, - .device_class = 0, - .device_subclass = 0, - .device_protocol = 0, - .max_packet_size0 = 64, - .vendor = 0, - .product = 1, - .bcd_device = 0, - .manufacturer_s = 1, - .product_s = 2, - .serial_s = 0, - .num_configurations = 1, - }, - .interface_descriptor = &.{ - .descriptor_type = usb.DescType.Interface, - .interface_number = 0, - .alternate_setting = 0, - // We have two endpoints (EP0 IN/OUT don't count) - .num_endpoints = 2, - .interface_class = 0xff, - .interface_subclass = 0, - .interface_protocol = 0, - .interface_s = 0, - }, - .config_descriptor = &.{ - .descriptor_type = usb.DescType.Config, - // This is calculated via the sizes of underlying descriptors contained in this configuration. - // ConfigurationDescriptor(9) + InterfaceDescriptor(9) * 1 + EndpointDescriptor(8) * 2 - .total_length = 34, - .num_interfaces = 1, - .configuration_value = 1, - .configuration_s = 0, - .attributes = 0xc0, - .max_power = 0x32, - }, - .lang_descriptor = "\x04\x03\x09\x04", // length || string descriptor (0x03) || Engl (0x0409) - .descriptor_strings = &.{ - // ugly unicode :| - "R\x00a\x00s\x00p\x00b\x00e\x00r\x00r\x00y\x00 \x00P\x00i\x00", - "P\x00i\x00c\x00o\x00 \x00T\x00e\x00s\x00t\x00 \x00D\x00e\x00v\x00i\x00c\x00e\x00", - }, - // Here we pass all endpoints to the config - // Dont forget to pass EP0_[IN|OUT] in the order seen below! - .endpoints = .{ - &usb.EP0_OUT_CFG, - &usb.EP0_IN_CFG, - &EP1_OUT_CFG, - &EP1_IN_CFG, - }, -}; - -pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { - std.log.err("panic: {s}", .{message}); - @breakpoint(); - while (true) {} -} - -pub const microzig_options = .{ - .log_level = .debug, - .logFn = rp2040.uart.log, -}; - -pub fn main() !void { - led.set_function(.sio); - led.set_direction(.out); - led.put(1); - - uart.apply(.{ - .baud_rate = baud_rate, - .tx_pin = uart_tx_pin, - .rx_pin = uart_rx_pin, - .clock_config = rp2040.clock_config, - }); - - rp2040.uart.init_logger(uart); - - // First we initialize the USB clock - rp2040.usb.Usb.init_clk(); - // Then initialize the USB device using the configuration defined above - rp2040.usb.Usb.init_device(&DEVICE_CONFIGURATION) catch unreachable; - var old: u64 = time.get_time_since_boot().to_us(); - var new: u64 = 0; - while (true) { - // You can now poll for USB events - rp2040.usb.Usb.task( - false, // debug output over UART [Y/n] - ) catch unreachable; - - new = time.get_time_since_boot().to_us(); - if (new - old > 500000) { - old = new; - led.toggle(); - } - } -} diff --git a/examples/raspberrypi/rp2040/src/usb_hid.zig b/examples/raspberrypi/rp2040/src/usb_hid.zig index d73e4ef..360a175 100644 --- a/examples/raspberrypi/rp2040/src/usb_hid.zig +++ b/examples/raspberrypi/rp2040/src/usb_hid.zig @@ -14,59 +14,14 @@ const baud_rate = 115200; const uart_tx_pin = gpio.num(0); const uart_rx_pin = gpio.num(1); -// First we define two callbacks that will be used by the endpoints we define next... -fn ep1_in_callback(dc: *usb.DeviceConfiguration, data: []const u8) void { - _ = data; - // The host has collected the data we repeated onto - // EP1! Set up to receive more data on EP1. - usb.Usb.callbacks.usb_start_rx( - dc.endpoints[2], // EP1_OUT_CFG, - 64, - ); -} - -fn ep1_out_callback(dc: *usb.DeviceConfiguration, data: []const u8) void { - // We've gotten data from the host on our custom - // EP1! Set up EP1 to repeat it. - usb.Usb.callbacks.usb_start_tx( - dc.endpoints[3], // EP1_IN_CFG, - data, - ); -} - -// The endpoints EP0_IN and EP0_OUT are already defined but you can -// add your own endpoints to... -pub var EP1_OUT_CFG: usb.EndpointConfiguration = .{ - .descriptor = &usb.EndpointDescriptor{ - .descriptor_type = usb.DescType.Endpoint, - .endpoint_address = usb.Dir.Out.endpoint(1), - .attributes = @intFromEnum(usb.TransferType.Interrupt), - .max_packet_size = 64, - .interval = 0, - }, - .endpoint_control_index = 2, - .buffer_control_index = 3, - .data_buffer_index = 2, - .next_pid_1 = false, - // The callback will be executed if we got an interrupt on EP1_OUT - .callback = ep1_out_callback, -}; +const usb_packet_size = 64; +const usb_config_len = usb.templates.config_descriptor_len + usb.templates.hid_in_out_descriptor_len; +const usb_config_descriptor = + usb.templates.config_descriptor(1, 1, 0, usb_config_len, 0xc0, 100) ++ + usb.templates.hid_in_out_descriptor(0, 0, 0, usb.hid.ReportDescriptorGenericInOut.len, usb.Endpoint.to_address(1, .Out), usb.Endpoint.to_address(1, .In), usb_packet_size, 0); -pub var EP1_IN_CFG: usb.EndpointConfiguration = .{ - .descriptor = &usb.EndpointDescriptor{ - .descriptor_type = usb.DescType.Endpoint, - .endpoint_address = usb.Dir.In.endpoint(1), - .attributes = @intFromEnum(usb.TransferType.Interrupt), - .max_packet_size = 64, - .interval = 0, - }, - .endpoint_control_index = 1, - .buffer_control_index = 2, - .data_buffer_index = 3, - .next_pid_1 = false, - // The callback will be executed if we got an interrupt on EP1_IN - .callback = ep1_in_callback, -}; +var driver_hid = usb.hid.HidClassDriver{ .report_descriptor = &usb.hid.ReportDescriptorGenericInOut }; +var drivers = [_]usb.types.UsbClassDriver{ driver_hid.driver() }; // This is our device configuration pub var DEVICE_CONFIGURATION: usb.DeviceConfiguration = .{ @@ -78,7 +33,7 @@ pub var DEVICE_CONFIGURATION: usb.DeviceConfiguration = .{ .device_protocol = 0, .max_packet_size0 = 64, .vendor = 0xCafe, - .product = 1, + .product = 2, .bcd_device = 0x0100, // Those are indices to the descriptor strings // Make sure to provide enough string descriptors! @@ -87,55 +42,14 @@ pub var DEVICE_CONFIGURATION: usb.DeviceConfiguration = .{ .serial_s = 3, .num_configurations = 1, }, - .interface_descriptor = &.{ - .descriptor_type = usb.DescType.Interface, - .interface_number = 0, - .alternate_setting = 0, - // We have two endpoints (EP0 IN/OUT don't count) - .num_endpoints = 2, - .interface_class = 3, - .interface_subclass = 0, - .interface_protocol = 0, - .interface_s = 0, - }, - .config_descriptor = &.{ - .descriptor_type = usb.DescType.Config, - // This is calculated via the sizes of underlying descriptors contained in this configuration. - // ConfigurationDescriptor(9) + InterfaceDescriptor(9) * 1 + EndpointDescriptor(8) * 2 - .total_length = 34, - .num_interfaces = 1, - .configuration_value = 1, - .configuration_s = 0, - .attributes = 0xc0, - .max_power = 0x32, - }, + .config_descriptor = &usb_config_descriptor, .lang_descriptor = "\x04\x03\x09\x04", // length || string descriptor (0x03) || Engl (0x0409) .descriptor_strings = &.{ - // ugly unicode :| - //"R\x00a\x00s\x00p\x00b\x00e\x00r\x00r\x00y\x00 \x00P\x00i\x00", - &usb.utf8ToUtf16Le("Raspberry Pi"), - //"P\x00i\x00c\x00o\x00 \x00T\x00e\x00s\x00t\x00 \x00D\x00e\x00v\x00i\x00c\x00e\x00", - &usb.utf8ToUtf16Le("Pico Test Device"), - //"c\x00a\x00f\x00e\x00b\x00a\x00b\x00e\x00", - &usb.utf8ToUtf16Le("cafebabe"), - }, - .hid = .{ - .hid_descriptor = &.{ - .bcd_hid = 0x0111, - .country_code = 0, - .num_descriptors = 1, - .report_length = 34, - }, - .report_descriptor = &usb.hid.ReportDescriptorFidoU2f, - }, - // Here we pass all endpoints to the config - // Dont forget to pass EP0_[IN|OUT] in the order seen below! - .endpoints = .{ - &usb.EP0_OUT_CFG, - &usb.EP0_IN_CFG, - &EP1_OUT_CFG, - &EP1_IN_CFG, + &usb.utils.utf8ToUtf16Le("Raspberry Pi"), + &usb.utils.utf8ToUtf16Le("Pico Test Device"), + &usb.utils.utf8ToUtf16Le("cafebabe"), }, + .drivers = &drivers }; pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { @@ -172,7 +86,7 @@ pub fn main() !void { while (true) { // You can now poll for USB events rp2040.usb.Usb.task( - true, // debug output over UART [Y/n] + false, // debug output over UART [Y/n] ) catch unreachable; new = time.get_time_since_boot().to_us();