diff --git a/build.zig b/build.zig index 39292a303..ace647939 100644 --- a/build.zig +++ b/build.zig @@ -16,6 +16,8 @@ pub const MemoryRegion = internals.MemoryRegion; const regz = @import("tools/regz"); +pub const tusb = @import("modules/tinyusb"); + // If more ports are available, the error "error: evaluation exceeded 1000 backwards branches" may occur. // In such cases, consider increasing the argument value for @setEvalBranchQuota(). const port_list: []const struct { @@ -860,6 +862,54 @@ pub fn MicroBuild(port_select: PortSelect) type { .{target.cpu.model.name}, ); } + /// Adds build step for tinyUSB and adds a c translate module to app "tinyusb_api" which can be imported + /// input includes and c sources need to include tusb_descriptors.c (or equivalent) + /// and tusb_config.h at a minimum (see) tinyusb documentation for details + /// The api can be imported into a project: `const tinyusb = @import("tinyusb_api");` + /// it's contents is dependant on the configuration in tusb_config.h + pub fn addTinyUsbLib( + root_build: *Build, + fw: *Firmware, + c_sources: []const LazyPath, + includes: []const LazyPath, + debug_printing: bool, + ) void { + const tusb_dep = fw.mb.dep.builder.dependency("modules/tinyusb", .{}); + const tusb_mod = tusb_dep.module("tinyusb"); + const target = root_build.resolveTargetQuery(fw.target.zig_target); + + if (debug_printing) + tusb.enableDebug(tusb_mod); + + tusb_mod.resolved_target = target; + for (c_sources) |source| { + tusb_mod.addCSourceFile(.{ .file = source }); + } + for (includes) |path| { + tusb_mod.addIncludePath(path); + } + tusb.addChip(tusb_mod.owner, tusb_mod, fw.target.chip.name); + + const tinyusb = root_build.addLibrary(.{ + .linkage = .static, + .name = "tinyusb", + .root_module = tusb_mod, + .use_llvm = true, + }); + const lib_step = root_build.addInstallArtifact(tinyusb, .{}); + root_build.getInstallStep().dependOn(&lib_step.step); + fw.add_object_file(lib_step.emitted_bin.?); + + const tusb_api_c = tusb.getTinyUsbCTranslate( + tusb_mod.owner, + target, + ); + + for (includes) |path| { + tusb_api_c.addIncludePath(path); + } + fw.app_mod.addImport("tinyusb", tusb_api_c.addModule("tinyusb_api")); + } }; } diff --git a/build.zig.zon b/build.zig.zon index 7a0e536a1..8fdea0d28 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -18,6 +18,7 @@ .@"modules/foundation-libc" = .{ .path = "modules/foundation-libc" }, .@"modules/riscv32-common" = .{ .path = "modules/riscv32-common" }, .@"modules/rtt" = .{ .path = "modules/rtt" }, + .@"modules/tinyusb" = .{ .path = "modules/tinyusb" }, // simulators .@"sim/aviron" = .{ .path = "sim/aviron", .lazy = true }, diff --git a/modules/tinyusb/build.zig b/modules/tinyusb/build.zig new file mode 100644 index 000000000..7923e671b --- /dev/null +++ b/modules/tinyusb/build.zig @@ -0,0 +1,177 @@ +const std = @import("std"); +const Build = std.Build; + +// TODO: Switch to lazy dependencies? + +pub fn build(b: *Build) !void { + addTinyUSBLib(b); +} +const tusb_h_contents = + \\#include_next "tusb.h" + \\#define TU_ATTR_FAST_FUNC __attribute__((section(".ram_text"))) +; + +pub fn addTinyUSBLib(b: *Build) void { + const module = b.addModule("tinyusb", .{ + .link_libc = false, + // seems important to release fast to avoid some extra dependencies + .optimize = .ReleaseFast, + }); + const wf_step = b.addWriteFile("tusb.h", tusb_h_contents); + module.addIncludePath(wf_step.getDirectory()); + + // TinyUSB src folder + const tusb_dep = b.dependency("tusb", .{}); + const tusb_src = tusb_dep.path("src"); + module.addIncludePath(tusb_src); + for (tinyusb_source_files) |file| { + module.addCSourceFile(.{ .file = tusb_src.path(b, file) }); + } + + // Add libc for embedded + const newlib_dep = b.dependency("newlib", .{}); + module.addIncludePath(newlib_dep.path("newlib/libc/include")); +} + +const SupportedChips = enum { + RP2040, +}; + +/// Add chip specific things to built library +// / TODO: Can this be a build option rather than a function call? +pub fn addChip(b: *Build, module: *Build.Module, chip_name: []const u8) void { + const option = std.meta.stringToEnum( + SupportedChips, + chip_name, + ) orelse { + std.debug.print("BUILD ERROR: Chip not supported: {s}\n\r", .{chip_name}); + @panic("Chip not supported for TinyUSB yet"); + }; + switch (option) { + .RP2040 => { + module.addCMacro("CFG_TUSB_MCU", "OPT_MCU_RP2040"); + module.addCMacro("CFG_TUSB_OS", "OPT_OS_NONE"); + module.addCMacro("PICO_RP2040", "1"); + module.addCMacro("BOARD_TUD_RHPORT", "0"); + module.addCMacro("PICO_RP2040_USB_FAST_IRQ", "1"); + module.addCMacro("__not_in_flash(group)", "__attribute__((section(\".ram_text\")))"); + + // add empty files for pico headers to be happy + // TODO: empty seems to work but maybe we'd prefer to add something. + // TODO: is the step order implicit when doing addIncludePath due to lazy directories + const wf_step = b.addWriteFiles(); + _ = wf_step.add("pico/config_autogen.h", ""); + _ = wf_step.add("pico/version.h", ""); + module.addIncludePath(wf_step.getDirectory()); + // pico-sdk headers + addPicoSdkInclude(b, module); + }, + } +} + +/// Enables debug logging in the generated tusb library +/// TODO: Can this be controlled through build options somehow rather than a function like this? +pub fn enableDebug(module: *Build.Module) void { + // Add printf implementation + const prntf_dep = module.owner.lazyDependency("printf", .{}); + if (prntf_dep) |dep| { + module.addIncludePath(dep.path("")); + module.addCSourceFile(.{ .file = dep.path("printf.c") }); + module.addCMacro("CFG_TUSB_DEBUG_PRINTF", "printf_"); + module.addCMacro("snprintf", "snprintf_"); + module.addCMacro("CFG_TUSB_DEBUG", "3"); + } +} + +pub fn getTinyUsbCTranslate( + b: *Build, + target: Build.ResolvedTarget, +) *Build.Step.TranslateC { + // const target = b.standardTargetOptions(.{}); + const tusb_dep = b.dependency("tusb", .{}); + const tusb_src = tusb_dep.path("src"); + const tinyusb_c = b.addTranslateC(.{ + .root_source_file = tusb_src.path(b, "tusb.h"), + .target = target, + .optimize = .ReleaseFast, + .link_libc = false, + }); + + tinyusb_c.defineCMacro("CFG_TUSB_MCU", "OPT_MCU_NONE"); + tinyusb_c.defineCMacro("CFG_TUSB_OS", "OPT_OS_NONE"); + + // TinyUSB src folder + tinyusb_c.addIncludePath(tusb_src); + + // Add libc for embedded + const newlib_dep = b.dependency("newlib", .{}); + tinyusb_c.addIncludePath(newlib_dep.path("newlib/libc/include")); + + return tinyusb_c; +} + +/// List of all .c files in the tinyUSB src directory less the portable directory files +/// tinyusb does a good job only compiling based on options selected. +const tinyusb_source_files = [_][]const u8{ + "tusb.c", + "device/usbd.c", + "device/usbd_control.c", + "class/msc/msc_host.c", + "class/msc/msc_device.c", + "class/cdc/cdc_host.c", + "class/cdc/cdc_rndis_host.c", + "class/cdc/cdc_device.c", + "class/dfu/dfu_rt_device.c", + "class/dfu/dfu_device.c", + "class/video/video_device.c", + "class/usbtmc/usbtmc_device.c", + "class/vendor/vendor_host.c", + "class/vendor/vendor_device.c", + "class/net/ecm_rndis_device.c", + "class/net/ncm_device.c", + // "class/mtp/mtp_device.c", not available in older tinyusb compatible with sdk? + "class/audio/audio_device.c", + "class/bth/bth_device.c", + "class/midi/midi_device.c", + // "class/midi/midi_host.c", not available in older tinyusb compatible with sdk? + "class/hid/hid_device.c", + "class/hid/hid_host.c", + "host/usbh.c", + "host/hub.c", + "common/tusb_fifo.c", + "typec/usbc.c", + "portable/raspberrypi/rp2040/rp2040_usb.c", + "portable/raspberrypi/rp2040/dcd_rp2040.c", +}; + +fn addPicoSdkInclude(b: *std.Build, m: *std.Build.Module) void { + const pico_sdk_dep = b.lazyDependency("pico_sdk", .{}); + if (pico_sdk_dep) |pico_dep| { + for (rp_2040_includes) |dir| { + m.addIncludePath(pico_dep.path(dir)); + } + } +} + +const rp_2040_includes = [_][]const u8{ + "src", + "src/common/pico_base_headers/include", + "src/common/pico_time/include", + "src/rp2_common/pico_platform_compiler/include", + "src/rp2_common/pico_platform_sections/include", + "src/rp2_common/pico_platform_panic/include", + "src/rp2_common/pico_platform_common/include", + "src/rp2_common/hardware_timer/include", + "src/rp2_common/hardware_base/include", + "src/common/pico_sync/include", + "src/rp2_common/hardware_sync/include", + "src/rp2_common/hardware_sync_spin_lock/include", + "src/rp2_common/hardware_irq/include", + "src/rp2_common/hardware_resets/include", + // I think these are the only things that would need to change for RP2350 + "src/rp2040/pico_platform/include", + "src/rp2040/hardware_structs/include", + "src/rp2040/hardware_regs/include", + "src/rp2040/hardware_regs/include", + "src/rp2040/hardware_structs/include", +}; diff --git a/modules/tinyusb/build.zig.zon b/modules/tinyusb/build.zig.zon new file mode 100644 index 000000000..d100dba93 --- /dev/null +++ b/modules/tinyusb/build.zig.zon @@ -0,0 +1,40 @@ +.{ + .name = .tinyusb, + .version = "0.1.0", + .fingerprint = 0x6f11d314f5263da7, // Changing this has security and trust implications? + .minimum_zig_version = "0.15.1", + .dependencies = .{ + .tusb = .{ + // matches pico-sdk 2.2.0 submodule hash + .url = "git+https://github.com/hathach/tinyusb.git#86ad6e56c1700e85f1c5678607a762cfe3aa2f47", + .hash = "N-V-__8AAGUZ_gDYqxEdexw7Rfp9YENlWbwlDPQu0bCfKxws", + // Latest release + // .url = "https://github.com/hathach/tinyusb/archive/refs/tags/0.19.0.tar.gz", + // .hash = "N-V-__8AAJebBAH6O7lcIkrzHzcWi6qE9wZoM3BhsLLCfuZX", + }, + .newlib = .{ + .url = "https://sourceware.org/pub/newlib/newlib-4.5.0.20241231.tar.gz", + .sha256 = "sha256:8f6903f8ceb0f991490a1e34074443c2a72b14dbd", + .hash = "N-V-__8AAJH7PAM8M1Wj7e7momRRAp7hssKtI2VvX0vEz-_B", + }, + .printf = .{ + .url = "https://github.com/mpaland/printf/archive/refs/tags/v4.0.0.zip", + .hash = "N-V-__8AALJuCQCrdHP5WGfuWR-16g1fK2VF3WA2C5p5e-jj", + .lazy = true, + }, + .pico_sdk = .{ + .url = "https://github.com/raspberrypi/pico-sdk/releases/download/2.2.0/pico-sdk-2.2.0.tar.gz", + .sha256 = "sha256:2678fe2b176cf64a7f71cd91749fdf9134c8cf7ff84b7199dfe5ea0d6dba6fa4", + .hash = "N-V-__8AAJZDcwFLNdIEF1wutjiH14f8fSET5jNORZIZKCaM", + .lazy = true, + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + "README.md", + }, +} diff --git a/modules/tinyusb/readme.md b/modules/tinyusb/readme.md new file mode 100644 index 000000000..ad54852d7 --- /dev/null +++ b/modules/tinyusb/readme.md @@ -0,0 +1,11 @@ +# Issues +- Only supports RP2040 +- Sometimes reset stalls when built. If effected comment out: + `unreset_block_wait(RESETS_RESET_USBCTRL_BITS);` + in /.../rp2040_usb.c:rp2040_usb_init as a workaround +# TODO +- How can we move the IRQ functions into ram +- USB Reset hang issue +- More chips supported +- Probably refactor to better align with project. +- Remove system includes dependency for arm-none-eabi \ No newline at end of file diff --git a/port/raspberrypi/rp2xxx/src/hal.zig b/port/raspberrypi/rp2xxx/src/hal.zig index 2750487d2..9f5c9bcca 100644 --- a/port/raspberrypi/rp2xxx/src/hal.zig +++ b/port/raspberrypi/rp2xxx/src/hal.zig @@ -39,6 +39,8 @@ pub const uart = @import("hal/uart.zig"); pub const usb = @import("hal/usb.zig"); pub const watchdog = @import("hal/watchdog.zig"); +pub const tinyusb_exports = @import("hal/tusb/tusb_exports.zig"); + comptime { // HACK: tests can't access microzig. maybe there's a better way to do this. if (!builtin.is_test and compatibility.chip == .RP2350) { diff --git a/port/raspberrypi/rp2xxx/src/hal/tusb/tusb_exports.zig b/port/raspberrypi/rp2xxx/src/hal/tusb/tusb_exports.zig new file mode 100644 index 000000000..10ef0b535 --- /dev/null +++ b/port/raspberrypi/rp2xxx/src/hal/tusb/tusb_exports.zig @@ -0,0 +1,82 @@ +const std = @import("std"); +const microzig = @import("microzig"); +const rp2xxx = microzig.hal; +const time = rp2xxx.time; +const peripherals = microzig.chip.peripherals; + +var charbuf: [128]u8 = undefined; +var char_index: usize = 0; +const tusb_logger = std.log.scoped(.tusb); + +export fn _putchar(char: u8) void { + if (char_index < charbuf.len and char != '\n') { + if (char == '\r') { + return; + } + charbuf[char_index] = char; + char_index += 1; + } else { + tusb_logger.debug("{s}", .{charbuf[0..char_index]}); + char_index = 0; + } +} + +// TODO: not sure why this is needed or if it's really right but seems to work. +export const _ctype_: u8 = undefined; + +export fn panic(format: [*c]const u8, ...) void { + const z_str: []const u8 = std.mem.span(format); + std.log.info("{s}", .{std.mem.trim(u8, z_str, "\n\n")}); +} + +export fn strlen(str: [*c]const u8) c_uint { + return std.mem.len(str); +} +export fn tusb_time_millis_api() c_uint { + return @truncate(time.get_time_since_boot().to_us() * 1000); +} + +export fn rp2040_chip_version() c_uint { + const chip_id = peripherals.SYSINFO.CHIP_ID.read(); + // TODO: should be able to switch to clear API? + return @intCast(chip_id.REVISION); +} + +pub fn irq_handler() linksection(".ram_text") callconv(.c) void { + if (handler_p) |h| + h(); +} + +//void irq_add_shared_handler(uint num, irq_handler_t handler, uint8_t order_priority); +const hdlr = *const fn () callconv(.c) void; + +var handler_p: ?hdlr = null; + +export fn irq_add_shared_handler(num: c_uint, handler: hdlr, order_priority: u8) callconv(.c) void { + _ = num; + handler_p = handler; + _ = order_priority; +} + +// void irq_remove_handler(uint num, irq_handler_t handler); +export fn irq_remove_handler(num: c_uint, handler: hdlr) void { + _ = num; + _ = handler; + handler_p = null; +} + +//void irq_set_enabled(uint num, bool enabled); +export fn irq_set_enabled(num: c_uint, enabled: bool) void { + _ = num; // TODO: Num should represent .usbctrl_irq + if (enabled) { + microzig.interrupt.enable(.USBCTRL_IRQ); + } else { + microzig.interrupt.disable(.USBCTRL_IRQ); + } +} + +// void __weak hard_assertion_failure(void) { +export fn hard_assertion_failure() void { + std.log.err("Probably hw_endpoint_alloc failed", .{}); + @panic("TUSB: Hard assertion Failed!"); +}