diff --git a/examples/nordic/nrf5x/build.zig b/examples/nordic/nrf5x/build.zig index 8bd330893..436685a26 100644 --- a/examples/nordic/nrf5x/build.zig +++ b/examples/nordic/nrf5x/build.zig @@ -31,6 +31,8 @@ pub fn build(b: *std.Build) void { .{ .target = nrf52840_mdk, .name = "nrf52840_mdk_rtt_log", .file = "src/rtt_log.zig" }, .{ .target = nrf52840_mdk, .name = "nrf52840_mdk_semihosting", .file = "src/semihosting.zig" }, .{ .target = nrf52840_mdk, .name = "nrf52840_mdk_spi_master", .file = "src/spi_master.zig" }, + .{ .target = nrf52840_mdk, .name = "nrf52840_mdk_usb_hid", .file = "src/usb_hid.zig" }, + .{ .target = nrf52840_mdk, .name = "nrf52840_mdk_usb_cdc", .file = "src/usb_cdc.zig" }, .{ .target = pca10040, .name = "pca10040_blinky", .file = "src/blinky.zig" }, .{ .target = pca10040, .name = "pca10040_uart", .file = "src/uart.zig" }, diff --git a/examples/nordic/nrf5x/src/usb_cdc.zig b/examples/nordic/nrf5x/src/usb_cdc.zig new file mode 100644 index 000000000..0a5cbc99e --- /dev/null +++ b/examples/nordic/nrf5x/src/usb_cdc.zig @@ -0,0 +1,127 @@ +//! Adapted from +//! examples/raspberrypi/rp2xxx/src/usb_cdc.zig + +const std = @import("std"); +const microzig = @import("microzig"); +const nrf = microzig.hal; +const board = microzig.board; +const usb = microzig.core.usb; +const time = nrf.time; +const USBD = nrf.usbd.USBD; +const clocks = nrf.clocks; +const uart = nrf.uart.num(0); + +const USB_Serial = usb.drivers.CDC; + +pub const panic = microzig.panic; + +pub const std_options = microzig.std_options(.{ + .log_level = .debug, + .log_scope_levels = &.{ + .{ .scope = .usb_dev, .level = .warn }, + .{ .scope = .usb_ctrl, .level = .warn }, + .{ .scope = .usb_cdc, .level = .warn }, + }, + .logFn = nrf.uart.log, +}); + +comptime { + _ = microzig.export_startup(); +} + +var usb_device: USBD = undefined; + +// Generate a device controller with descriptor and handlers setup for CDC (USB_Serial) +var usb_controller: usb.DeviceController(.{ + .bcd_usb = USBD.max_supported_bcd_usb, + .device_triple = .unspecified, + .vendor = USBD.default_vendor_id, + .product = USBD.default_product_id, + .bcd_device = .v1_00, + .serial = "someserial", + .max_supported_packet_size = USBD.max_supported_packet_size, + .configurations = &.{.{ + .attributes = .{ .self_powered = false }, + .max_current_ma = 50, + .Drivers = struct { serial: USB_Serial }, + }}, +}, .{.{ + .serial = .{ .itf_notifi = "Board CDC", .itf_data = "Board CDC Data" }, +}}) = .init; + +pub fn main() !void { + board.init(); + + uart.apply(.{ + .tx_pin = board.uart_tx, + .rx_pin = board.uart_rx, + }); + + nrf.uart.init_logger(uart); + + clocks.hfxo.start(); + usb_device = .init(); + + var old: u64 = 0; + var new: u64 = 0; + + var i: u32 = 0; + + while (true) { + // You can now poll for USB events + usb_device.poll(&usb_controller); + + // Ensure that the host as finished enumerating our USB device + if (usb_controller.drivers()) |drivers| { + new = time.get_time_since_boot().to_us(); + if (new - old > 500_000) { + old = new; + board.led1.toggle(); + i += 1; + std.log.info("cdc test: {}", .{i}); + + usb_cdc_write(&drivers.serial, "This is very very long text sent from nRF52 by USB CDC to your device: {}\r\n", .{i}); + } + + // read and print host command if present + const message = usb_cdc_read(&drivers.serial); + if (message.len > 0) { + usb_cdc_write(&drivers.serial, "Your message to me was: {s}\r\n", .{message}); + } + } + } +} + +var usb_tx_buff: [1024]u8 = undefined; + +/// Transfer data to host +/// NOTE: After each USB chunk transfer, we have to call the USB task so that bus TX events can be +/// handled +pub fn usb_cdc_write(serial: *USB_Serial, comptime fmt: []const u8, args: anytype) void { + var tx = std.fmt.bufPrint(&usb_tx_buff, fmt, args) catch &.{}; + + while (tx.len > 0) { + tx = tx[serial.write(tx)..]; + usb_device.poll(&usb_controller); + } + // Short messages are not sent right away; instead, they accumulate in a buffer, so we have to force a flush to send them + while (!serial.flush()) + usb_device.poll(&usb_controller); +} + +var usb_rx_buff: [1024]u8 = undefined; + +/// Receive data from host +/// NOTE: Read code was not tested extensively. In case of issues, try to call USB task before every +/// read operation +pub fn usb_cdc_read(serial: *USB_Serial) []const u8 { + var rx_len: usize = 0; + + while (true) { + const len = serial.read(usb_rx_buff[rx_len..]); + rx_len += len; + if (len == 0) break; + } + + return usb_rx_buff[0..rx_len]; +} diff --git a/examples/nordic/nrf5x/src/usb_hid.zig b/examples/nordic/nrf5x/src/usb_hid.zig new file mode 100644 index 000000000..e0f13ccec --- /dev/null +++ b/examples/nordic/nrf5x/src/usb_hid.zig @@ -0,0 +1,188 @@ +//! Adapted from +//! examples/raspberrypi/rp2xxx/src/usb_cdc.zig + +const std = @import("std"); +const microzig = @import("microzig"); +const nrf = microzig.hal; +const board = microzig.board; +const usb = microzig.core.usb; +const time = nrf.time; +const USBD = nrf.usbd.USBD; +const clocks = nrf.clocks; +const uart = nrf.uart.num(0); + +pub const panic = microzig.panic; + +pub const std_options = microzig.std_options(.{ + .log_level = .debug, + .log_scope_levels = &.{ + .{ .scope = .usb_dev, .level = .warn }, + .{ .scope = .usb_ctrl, .level = .warn }, + .{ .scope = .usb_hid_int_driver, .level = .warn }, + }, + .logFn = nrf.uart.log, +}); + +comptime { + _ = microzig.export_startup(); +} + +pub const Modifiers = packed struct(u8) { + lctrl: bool, + lshift: bool, + lalt: bool, + lgui: bool, + rctrl: bool, + rshift: bool, + ralt: bool, + rgui: bool, + + pub const none: @This() = @bitCast(@as(u8, 0)); +}; + +pub const Code = enum(u8) { + // Codes taken from https://gist.github.com/mildsunrise/4e231346e2078f440969cdefb6d4caa3 + // zig fmt: off + reserved = 0x00, error_roll_over, post_fail, error_undefined, + a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, + top_1, top_2, top_3, top_4, top_5, top_6, top_7, top_8, top_9, top_0, + enter, escape, delete, tab, space, + @"-", @"=", @"[", @"]", @"\\", @"non_us_#", @";", @"'", @"`", @",", @".", @"/", + caps_lock, + f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, + print_screen, scroll_lock, pause, insert, home, page_up, delete_forward, end, page_down, + right_arrow, left_arrow, down_arrow, up_arrow, num_lock, + kpad_div, kpad_mul, kpad_sub, kpad_add, kpad_enter, + kpad_1, kpad_2, kpad_3, kpad_4, kpad_5, kpad_6, kpad_7, kpad_8, kpad_9, kpad_0, + kpad_delete, @"non_us_\\", application, power, @"kpad_=", + f13, f14, f15, f16, f17, f18, f19, f20, f21, f22, f23, f24, + lctrl = 224, lshift, lalt, lgui, rctrl, rshift, ralt, rgui, + // zig fmt: on + _, +}; + +pub const KeyboardInReport = extern struct { + modifiers: Modifiers, + reserved: u8 = 0, + keys: [6]Code, + + comptime { + std.debug.assert(@sizeOf(@This()) == 8); + } + + pub const empty: @This() = .{ .modifiers = .none, .keys = @splat(.reserved) }; +}; + +pub const KeyboardOutReport = packed struct(u8) { + num_lock: bool, + caps_lock: bool, + scroll_lock: bool, + padding: u5 = 0, +}; + +const Keyboard = usb.drivers.hid.InterruptDriver(.{ + .subclass = .Boot, + .protocol = .Boot, + .report_descriptor = &.{ + .{ .global_usage_page = .generic_desktop }, + .local_usage_enum(.{ .generic_desktop = .keyboard }), + .{ .main_collection = .Application }, + // Input: modifier key bitmap + .{ .data = .{ + .usage = .{ .global_page = .keyboard }, + .usage_range = .{ 0xE0, 0xE7 }, + .count = 8, + .Child = bool, + .dir = .In, + .type = .dynamic, + } }, + // Reserved 8 bits + .{ .data_static = .{ .In, u8 } }, + // Output: indicator LEDs + .{ .data = .{ + .usage = .{ .global_page = .led }, + .usage_range = .{ 1, 5 }, + .count = 5, + .Child = bool, + .dir = .Out, + .type = .dynamic, + } }, + // Padding + .{ .data_static = .{ .Out, u3 } }, + // Input: up to 6 pressed key codes + .{ .data = .{ + .usage = .{ .global_page = .keyboard }, + .usage_range = .{ 0x00, 0xff }, + .count = 6, + .Child = u8, + .dir = .In, + .type = .selector, + } }, + // End + .main_collection_end, + }, + .InReport = KeyboardInReport, + .OutReport = KeyboardOutReport, +}); + +var usb_device: USBD = undefined; + +var usb_controller: usb.DeviceController(.{ + .bcd_usb = USBD.max_supported_bcd_usb, + .device_triple = .unspecified, + .vendor = USBD.default_vendor_id, + .product = USBD.default_product_id, + .bcd_device = .v1_00, + .serial = "someserial", + .max_supported_packet_size = USBD.max_supported_packet_size, + .configurations = &.{.{ + .attributes = .{ .self_powered = false }, + .max_current_ma = 50, + .Drivers = struct { keyboard: Keyboard }, + }}, +}, .{.{ + .keyboard = .{ .itf_string = "Boot Keyboard", .poll_interval = 1 }, +}}) = .init; + +pub fn main() !void { + board.init(); + + uart.apply(.{ + .tx_pin = board.uart_tx, + .rx_pin = board.uart_rx, + }); + + nrf.uart.init_logger(uart); + + clocks.hfxo.start(); + usb_device = .init(); + + var old: u64 = time.get_time_since_boot().to_us(); + var new: u64 = 0; + const message: []const Code = &.{ .h, .e, .l, .l, .o, .space, .w, .o, .r, .l, .d, .caps_lock, .enter }; + var idx: usize = 0; + + while (true) { + // You can now poll for USB events + usb_device.poll(&usb_controller); + + if (usb_controller.drivers()) |drivers| { + new = time.get_time_since_boot().to_us(); + if (new - old > 2_000_000) { + old = new; + idx = 0; + } else { + std.log.info("report {}", .{idx}); + idx += @intFromBool(if (idx & 1 == 0 and idx < 2 * message.len) + drivers.keyboard.send_report( + &.{ .modifiers = .none, .keys = .{message[@intCast(idx / 2)]} ++ .{.reserved} ** 5 }, + ) + else + drivers.keyboard.send_report(&.empty)); + } + + if (drivers.keyboard.receive_report()) |report| + board.led1.put(@intFromBool(report.caps_lock)); + } + } +} diff --git a/port/nordic/nrf5x/src/hal.zig b/port/nordic/nrf5x/src/hal.zig index 6034d9ec8..c2bb7be69 100644 --- a/port/nordic/nrf5x/src/hal.zig +++ b/port/nordic/nrf5x/src/hal.zig @@ -8,6 +8,7 @@ pub const i2cdma = @import("hal/i2cdma.zig"); pub const spim = @import("hal/spim.zig"); pub const time = @import("hal/time.zig"); pub const uart = @import("hal/uart.zig"); +pub const usbd = @import("hal/usbd.zig"); pub const drivers = @import("hal/drivers.zig"); // TODO: adc, timers, pwm, rng, rtc alarms, interrupts, wdt, wifi, nfc, bt, zigbee @@ -28,6 +29,7 @@ test "hal tests" { _ = spim; _ = time; _ = uart; + _ = usbd; _ = drivers; } diff --git a/port/nordic/nrf5x/src/hal/usbd.zig b/port/nordic/nrf5x/src/hal/usbd.zig new file mode 100644 index 000000000..9edc5459a --- /dev/null +++ b/port/nordic/nrf5x/src/hal/usbd.zig @@ -0,0 +1,384 @@ +//! USB device implementation +//! +//! References: +//! * RP2XXX HAL (port/raspberrypi/rp2xxx/src/hal/usb.zig) +//! * https://github.com/nrf-rs/nrf-usbd + +const std = @import("std"); +const log = std.log.scoped(.usb_dev); +const microzig = @import("microzig"); +const cpu = microzig.cpu; +const peripherals = microzig.chip.peripherals; +const usb = microzig.core.usb; + +const errata = @import("./usbd/errata.zig"); + +// +++++++++++++++++++++++++++++++++++++++++++++++++ +// Code +// +++++++++++++++++++++++++++++++++++++++++++++++++ + +const MAX_PACKET_SIZE = 64; +const NUM_EP = 8; + +const PowerState = union(enum) { + detached, + enabling, + waiting_pwrrdy, + connected, +}; + +const EP0_State = struct { + direction: usb.types.Dir = .Out, + remaining_size: u16 = 0, +}; + +const EP_Config = struct { + max_packet_size: u16 = MAX_PACKET_SIZE, +}; + +pub const USBD = struct { + power: PowerState = .detached, + + bufs_in: [NUM_EP][MAX_PACKET_SIZE]u8 align(4) = @splat(@splat(0)), + bufs_out: [NUM_EP][MAX_PACKET_SIZE]u8 align(4) = @splat(@splat(0)), + eps_in: [NUM_EP]EP_Config = @splat(.{}), + eps_out: [NUM_EP]EP_Config = @splat(.{}), + ep0_state: EP0_State = .{}, + + interface: usb.DeviceInterface, + + pub const max_supported_packet_size = MAX_PACKET_SIZE; + pub const max_supported_bcd_usb: usb.types.Version = .v1_10; + pub const default_vendor_id: usb.Config.IdStringPair = .{ .id = 0x1209, .str = "pid.codes" }; + pub const default_product_id: usb.Config.IdStringPair = .{ .id = 0x0001, .str = "nRF5x test device" }; + + const vtable: usb.DeviceInterface.VTable = .{ + .ep_writev = ep_writev, + .ep_readv = ep_readv, + .ep_listen = ep_listen, + .ep_open = ep_open, + .set_address = set_address, + }; + + pub fn init() @This() { + peripherals.USBD.USBPULLUP.write_raw(0); + peripherals.USBD.ENABLE.write_raw(0); + peripherals.USBD.EPINEN.write_raw(0x01); // only EP0 IN by default + peripherals.USBD.EPOUTEN.write_raw(0x01); // only EP0 OUT by default + peripherals.USBD.EVENTCAUSE.write_raw(0xFFFFFFFF); // W1C all + peripherals.USBD.EPSTATUS.write_raw(0xFFFFFFFF); // W1C all + peripherals.USBD.EPDATASTATUS.write_raw(0xFFFFFFFF); // W1C all + peripherals.USBD.EVENTS_USBRESET.write_raw(0); + peripherals.USBD.EVENTS_EP0SETUP.write_raw(0); + peripherals.USBD.EVENTS_EP0DATADONE.write_raw(0); + peripherals.USBD.EVENTS_EPDATA.write_raw(0); + peripherals.USBD.EVENTS_USBEVENT.write_raw(0); + for (0..8) |i| { + peripherals.USBD.EVENTS_ENDEPIN[i].write_raw(0); + peripherals.USBD.EVENTS_ENDEPOUT[i].write_raw(0); + peripherals.USBD.EPIN[i].PTR.write_raw(0); + peripherals.USBD.EPIN[i].MAXCNT.write_raw(0); + peripherals.USBD.EPOUT[i].PTR.write_raw(0); + peripherals.USBD.EPOUT[i].MAXCNT.write_raw(0); + } + return .{ + .interface = .{ .vtable = &vtable }, + }; + } + + pub fn poll(self: *@This(), controller: anytype) void { + comptime usb.validate_controller(@TypeOf(controller)); + + if (self.power != .detached and peripherals.POWER.USBREGSTATUS.read().VBUSDETECT == .NoVbus) { + self.teardown(); + self.power = .detached; + return; + } + + switch (self.power) { + .detached => { + if (peripherals.POWER.USBREGSTATUS.read().VBUSDETECT == .VbusPresent) { + errata.pre_enable(); + peripherals.USBD.ENABLE.write_raw(1); + self.power = .enabling; + } + }, + .enabling => { + if (peripherals.USBD.EVENTCAUSE.read().READY == .Ready) { + peripherals.USBD.EVENTCAUSE.write(.{ .READY = .Ready }); // W1C + peripherals.USBD.EVENTS_USBEVENT.write_raw(0); + // In case poll() is called inside an interrupt handler + // Dummy read to prevent spurious double IRQ + _ = peripherals.USBD.EVENTS_USBEVENT.read(); + errata.post_enable(); + self.power = .waiting_pwrrdy; + } + }, + .waiting_pwrrdy => { + if (peripherals.POWER.USBREGSTATUS.read().OUTPUTRDY == .Ready) { + peripherals.USBD.USBPULLUP.write_raw(1); + self.power = .connected; + } + }, + .connected => { + // Bus reset + if (peripherals.USBD.EVENTS_USBRESET.raw != 0) { + peripherals.USBD.EVENTS_USBRESET.write_raw(0); + _ = peripherals.USBD.EVENTS_USBRESET.read(); + + self.reset(); + controller.on_bus_reset(&self.interface); + return; + } + + // SETUP packet captured by hardware + if (peripherals.USBD.EVENTS_EP0SETUP.raw != 0) { + peripherals.USBD.EVENTS_EP0SETUP.write_raw(0); + _ = peripherals.USBD.EVENTS_EP0SETUP.read(); + + const setup: usb.types.SetupPacket = .{ + .request_type = @bitCast(@as(u8, @intCast(peripherals.USBD.BMREQUESTTYPE.raw))), + .request = @intCast(peripherals.USBD.BREQUEST.raw), + .value = .from(@as(u16, @intCast(peripherals.USBD.WVALUEH.raw)) << 8 | @as(u16, @intCast(peripherals.USBD.WVALUEL.raw))), + .index = .from(@as(u16, @intCast(peripherals.USBD.WINDEXH.raw)) << 8 | @as(u16, @intCast(peripherals.USBD.WINDEXL.raw))), + .length = .from(@as(u16, @intCast(peripherals.USBD.WLENGTHH.raw)) << 8 | @as(u16, @intCast(peripherals.USBD.WLENGTHL.raw))), + }; + self.ep0_state.direction = switch (peripherals.USBD.BMREQUESTTYPE.read().DIRECTION) { + .HostToDevice => .Out, + .DeviceToHost => .In, + }; + self.ep0_state.remaining_size = setup.length.into(); + controller.on_setup_req(&self.interface, &setup); + } + + // EP0 data-phase completions + if (peripherals.USBD.EVENTS_EP0DATADONE.raw != 0) { + // Here nrf-usbd doesn't clear OUT events + // (https://github.com/nrf-rs/nrf-usbd/blob/main/src/usbd.rs#L683) + // But this case is not handled in the controller yet + peripherals.USBD.EVENTS_EP0DATADONE.write_raw(0); + _ = peripherals.USBD.EVENTS_EP0DATADONE.read(); + switch (self.ep0_state.direction) { + .In => controller.on_buffer(&self.interface, .in(.ep0)), + // Control-OUT with data-phase is unhandled in the controller + .Out => peripherals.USBD.TASKS_EP0STATUS.write_raw(1), + } + } + + // Data-endpoint completions + if (peripherals.USBD.EVENTS_EPDATA.raw != 0) { + peripherals.USBD.EVENTS_EPDATA.write_raw(0); + _ = peripherals.USBD.EVENTS_EPDATA.read(); + const status = peripherals.USBD.EPDATASTATUS.raw; + peripherals.USBD.EPDATASTATUS.write_raw(status); // W1C handled bits + // Calling on_buffer() for each set bit + // IN endpoints (bits 1-7) + var status_in = status >> 1; + inline for (1..8) |i| { + if (status_in == 0) break; + if (status_in & 1 == 1) controller.on_buffer(&self.interface, .in(@enumFromInt(i))); + status_in >>= 1; + } + // OUT endpoints (bits 17-23) + var status_out = status >> 17; + inline for (1..8) |i| { + if (status_out == 0) break; + if (status_out & 1 == 1) controller.on_buffer(&self.interface, .out(@enumFromInt(i))); + status_out >>= 1; + } + } + + // Suspend (not implemented) + if (peripherals.USBD.EVENTCAUSE.read().SUSPEND == .Detected) { + peripherals.USBD.EVENTCAUSE.write(.{ .SUSPEND = .Detected }); + peripherals.USBD.EVENTS_USBEVENT.write_raw(0); + _ = peripherals.USBD.EVENTS_USBEVENT.read(); + } + + // Resume (not implemented) + if (peripherals.USBD.EVENTCAUSE.read().RESUME == .Detected) { + peripherals.USBD.EVENTCAUSE.write(.{ .RESUME = .Detected }); + peripherals.USBD.EVENTS_USBEVENT.write_raw(0); + _ = peripherals.USBD.EVENTS_USBEVENT.read(); + } + }, + } + } + + fn teardown(_: *@This()) void { + // Upon VBUS removal detection, it is recommended to + // let on-going EasyDMA transfers finish before disabling USBD + // + // But in this case it won't happen because + // we busywait for ENDEP* with interrupts disabled + peripherals.USBD.USBPULLUP.write_raw(0); + peripherals.USBD.ENABLE.write_raw(0); + } + + fn reset(self: *@This()) void { + peripherals.USBD.EPDATASTATUS.write_raw(0xFFFFFFFF); // W1C all + peripherals.USBD.EPSTATUS.write_raw(0xFFFFFFFF); // W1C all + peripherals.USBD.EVENTS_EP0SETUP.write_raw(0); + peripherals.USBD.EVENTS_EP0DATADONE.write_raw(0); + peripherals.USBD.EVENTS_EPDATA.write_raw(0); + peripherals.USBD.SHORTS.write(.{ .EP0DATADONE_EP0STATUS = .Disabled }); + peripherals.USBD.EPINEN.write_raw(0x01); // only EP0 IN + peripherals.USBD.EPOUTEN.write_raw(0x01); // only EP0 OUT + + for (1..NUM_EP) |i| { + // NOTE: Data endpoints are automatically disabled on reset + peripherals.USBD.EPIN[i].PTR.write_raw(0); + peripherals.USBD.EPIN[i].MAXCNT.write_raw(0); + peripherals.USBD.EPOUT[i].PTR.write_raw(0); + peripherals.USBD.EPOUT[i].MAXCNT.write_raw(0); + + // When first enabled, bulk/interrupt endpoints + // will return NAK until 0 is written to SIZE.EPOUT[n] + peripherals.USBD.SIZE.EPOUT[i].write_raw(0); + } + + self.eps_in = @splat(.{}); + self.eps_out = @splat(.{}); + self.ep0_state = .{}; + } + + fn ep_writev( + itf: *usb.DeviceInterface, + ep_num: usb.types.Endpoint.Num, + data: []const []const u8, + ) usb.types.Len { + const self: *@This() = @fieldParentPtr("interface", itf); + log.debug("ep_writev {t}: ({} bytes) {X} ({s})", .{ ep_num, data[0].len, data[0], data[0] }); + + if (data[0].len == 0) { + if (ep_num == .ep0) peripherals.USBD.TASKS_EP0STATUS.write_raw(1); + return 0; + } + + const i = @intFromEnum(ep_num); + const scratch = &self.bufs_in[i]; + const scratch_cap = @min(self.eps_in[i].max_packet_size, scratch.len); + var scratch_slice: []align(1) u8 = scratch[0..scratch_cap]; + + for (data) |src| { + const len = @min(src.len, scratch_slice.len); + @memcpy(scratch_slice[0..len], src[0..len]); + scratch_slice = scratch_slice[len..]; + } + + const len: usb.types.Len = @intCast(scratch_cap - scratch_slice.len); + + // Prepare DMA transfer + peripherals.USBD.EPIN[i].PTR.write_raw(@intFromPtr(scratch)); + peripherals.USBD.EPIN[i].MAXCNT.write_raw(len); + + if (ep_num == .ep0) { + // EPIN0: a short packet (len < max_packet_size) indicates the end of the data + // stage and must be followed by us responding with an ACK token to an OUT token + // sent from the host (AKA the status stage) + // Also handle the case when it's the last packet with size 64 + self.ep0_state.remaining_size -|= len; + const is_short_packet = len < self.eps_in[0].max_packet_size; + const is_last_packet = is_short_packet or self.ep0_state.remaining_size == 0; + if (is_last_packet) { + peripherals.USBD.SHORTS.write(.{ .EP0DATADONE_EP0STATUS = .Enabled }); + } else { + peripherals.USBD.SHORTS.write(.{ .EP0DATADONE_EP0STATUS = .Disabled }); + } + + // Here, nrf-usbd does this: + // > Hack: trigger status stage if the IN transfer is not acknowledged after a few frames, + // > so record the current frame here; the actual test and status stage activation happens + // > in the poll method. + } + + // Start DMA + const was_enabled = cpu.interrupt.is_enabled(.USBD); + defer if (was_enabled) cpu.interrupt.enable(.USBD); + peripherals.USBD.TASKS_STARTEPIN[i].write_raw(1); + while (peripherals.USBD.EVENTS_ENDEPIN[i].raw != 1) {} + peripherals.USBD.EVENTS_ENDEPIN[i].write_raw(0); + _ = peripherals.USBD.EVENTS_ENDEPIN[i].read(); + + return len; + } + + fn ep_readv( + itf: *usb.DeviceInterface, + ep_num: usb.types.Endpoint.Num, + data: []const []u8, + ) usb.types.Len { + var total_len: usize = data[0].len; + for (data[1..]) |d| total_len += d.len; + log.debug("ep_readv {t}: ({} bytes)", .{ ep_num, total_len }); + + const self: *@This() = @fieldParentPtr("interface", itf); + const i = @intFromEnum(ep_num); + const size = peripherals.USBD.SIZE.EPOUT[i].raw; + + const scratch_buf = &self.bufs_out[i]; + + // Prepare DMA transfer + peripherals.USBD.EPOUT[i].PTR.write_raw(@intFromPtr(scratch_buf)); + peripherals.USBD.EPOUT[i].MAXCNT.write_raw(size); + + // Start DMA + const was_enabled = cpu.interrupt.is_enabled(.USBD); + defer if (was_enabled) cpu.interrupt.enable(.USBD); + peripherals.USBD.TASKS_STARTEPOUT[i].write_raw(1); + while (peripherals.USBD.EVENTS_ENDEPOUT[i].raw != 1) {} + peripherals.USBD.EVENTS_ENDEPOUT[i].write_raw(0); + _ = peripherals.USBD.EVENTS_ENDEPOUT[i].read(); + + var scratch_slice: []align(1) u8 = scratch_buf[0..size]; + + for (data) |dst| { + const len = @min(dst.len, scratch_slice.len); + @memcpy(dst[0..len], scratch_slice[0..len]); + scratch_slice = scratch_slice[len..]; + if (scratch_slice.len == 0) + return @intCast(size); + } + + log.warn("discarding rx data on ep {t}, {} bytes received", .{ ep_num, size }); + return @intCast(size - scratch_slice.len); + } + + fn ep_listen( + _: *usb.DeviceInterface, + ep_num: usb.types.Endpoint.Num, + len: usb.types.Len, + ) void { + log.debug("ep_listen {t}: ({} bytes)", .{ ep_num, len }); + + if (ep_num == .ep0) { + peripherals.USBD.TASKS_EP0RCVOUT.write_raw(1); + } + } + + fn ep_open(itf: *usb.DeviceInterface, desc: *const usb.descriptor.Endpoint) void { + const self: *@This() = @fieldParentPtr("interface", itf); + const ep = desc.endpoint; + const i = @intFromEnum(ep.num); + const mask: u32 = @as(u32, 1) << i; + switch (ep.dir) { + .In => { + self.eps_in[i].max_packet_size = desc.max_packet_size.into(); + peripherals.USBD.EPINEN.write_raw(peripherals.USBD.EPINEN.raw | mask); + }, + .Out => { + self.eps_out[i].max_packet_size = desc.max_packet_size.into(); + peripherals.USBD.EPOUTEN.write_raw(peripherals.USBD.EPOUTEN.raw | mask); + }, + } + + const attr = desc.attributes; + log.debug( + "ep_open {t} {t}: {{ type: {t}, sync: {t}, usage: {t}, size: {} }}", + .{ ep.num, ep.dir, attr.transfer_type, attr.synchronisation, attr.usage, desc.max_packet_size.into() }, + ); + } + + /// No-op: the peripheral handles this + fn set_address(_: *usb.DeviceInterface, _: u7) void {} +}; diff --git a/port/nordic/nrf5x/src/hal/usbd/errata.zig b/port/nordic/nrf5x/src/hal/usbd/errata.zig new file mode 100644 index 000000000..204b7afcb --- /dev/null +++ b/port/nordic/nrf5x/src/hal/usbd/errata.zig @@ -0,0 +1,93 @@ +//! Errata workarounds +//! +//! References: +//! * https://github.com/nordicsemi/nrfx/blob/nrfx-1.x/master/drivers/src/nrfx_usbd.c +//! * https://github.com/nordicsemi/nrfx/blob/nrfx-1.x/master/mdk/nrf52_erratas.h +//! * https://github.com/nrf-rs/nrf-usbd/blob/main/src/errata.rs + +const microzig = @import("microzig"); +const compatibility = microzig.hal.compatibility; + +const version: enum { + nrf52833, + nrf52840, +} = switch (compatibility.chip) { + .nrf52833 => .nrf52833, + .nrf52840 => .nrf52840, + else => compatibility.unsupported_chip("usbd"), +}; + +inline fn reg(addr: usize) *volatile u32 { + return @ptrFromInt(addr); +} + +pub fn pre_enable() void { + if (errata_187_applies()) { + if (reg(0x4006EC00).* == 0x00000000) { + reg(0x4006EC00).* = 0x00009375; + reg(0x4006ED14).* = 0x00000003; + reg(0x4006EC00).* = 0x00009375; + } else { + reg(0x4006ED14).* = 0x00000003; + } + } + + pre_wakeup(); +} + +pub fn post_enable() void { + post_wakeup(); + + if (errata_187_applies()) { + if (reg(0x4006EC00).* == 0x00000000) { + reg(0x4006EC00).* = 0x00009375; + reg(0x4006ED14).* = 0x00000000; + reg(0x4006EC00).* = 0x00009375; + } else { + reg(0x4006ED14).* = 0x00000000; + } + } +} + +pub fn pre_wakeup() void { + if (errata_171_applies()) { + if (reg(0x4006EC00).* == 0x00000000) { + reg(0x4006EC00).* = 0x00009375; + reg(0x4006EC14).* = 0x000000C0; + reg(0x4006EC00).* = 0x00009375; + } else { + reg(0x4006EC14).* = 0x000000C0; + } + } +} + +pub fn post_wakeup() void { + if (errata_171_applies()) { + if (reg(0x4006EC00).* == 0x00000000) { + reg(0x4006EC00).* = 0x00009375; + reg(0x4006EC14).* = 0x00000000; + reg(0x4006EC00).* = 0x00009375; + } else { + reg(0x4006EC14).* = 0x00000000; + } + } +} + +/// Applies to nRF52840 +inline fn errata_171_applies() bool { + return switch (version) { + .nrf52840 => true, + else => false, + }; +} + +// Applies to: +// - nRF52833 +// - nRF52840 0x01..0x05 and unknown future variants +inline fn errata_187_applies() bool { + const variant = @as(*volatile u32, @ptrFromInt(0x10000134)).*; + return switch (version) { + .nrf52833 => true, + .nrf52840 => variant != 0x00, + }; +}