diff --git a/build.zig b/build.zig index e75a109..b9f2803 100644 --- a/build.zig +++ b/build.zig @@ -39,6 +39,10 @@ pub fn addPiPicoExecutable( // project requires multiple HALs, it accepts microzig as a param pub fn build(b: *Builder) !void { const optimize = b.standardOptimizeOption(.{}); + + const args_dep = b.dependency("args", .{}); + const args_mod = args_dep.module("args"); + var examples = Examples.init(b, optimize); examples.install(b); @@ -52,6 +56,16 @@ pub fn build(b: *Builder) !void { const test_step = b.step("test", "run unit tests"); test_step.dependOn(&b.addRunArtifact(pio_tests).step); + + const flash_tool = b.addExecutable(.{ + .name = "rp2040-flash", + .optimize = .Debug, + .target = .{}, + .root_source_file = .{ .path = "tools/rp2040-flash.zig" }, + }); + flash_tool.addModule("args", args_mod); + + b.installArtifact(flash_tool); } fn root() []const u8 { diff --git a/build.zig.zon b/build.zig.zon index 32cee66..d8db2db 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,6 +6,9 @@ .url = "https://github.com/ZigEmbeddedGroup/microzig/archive/9392fe0f7bddde26155c181ab80b70097b49c791.tar.gz", .hash = "1220326148075cd017425e04356329c33c3e794d31a54bdafa46521fcfcc55b422a3", }, + .args = .{ + .url = "https://github.com/MasterQ32/zig-args/archive/91d1e89fb89a4d01dec7c9aec95b0a324080ebcc.tar.gz", + .hash = "12203d04cafc97f952d74cdb077e74c0ab3414f9f6b5fbd159112c62bfa584a0dbed", + }, }, } - diff --git a/tools/rp2040-flash.zig b/tools/rp2040-flash.zig new file mode 100644 index 0000000..23b48ac --- /dev/null +++ b/tools/rp2040-flash.zig @@ -0,0 +1,331 @@ +const std = @import("std"); +const args_parser = @import("args"); +const builtin = @import("builtin"); + +const CliOptions = struct { + help: bool = false, + device: ?[]const u8 = null, + wait: bool = false, + + pub const shorthands = .{ + .h = "help", + .d = "device", + .w = "wait", + }; +}; + +const wait_device_ready_timeout = 60 * std.time.ns_per_s; // timeout until a device is found +const wait_device_avail_timeout = 60 * std.time.ns_per_s; // timeout until a device is found +const access_denied_limit = 20; // try that many times with AccessDenied before the user is informed + +fn print_usage(file: std.fs.File, exe: ?[]const u8) !void { + try file.writer().writeAll(exe orelse "rp2040-flash"); + try file.writer().writeAll( + \\ [-h] [-d ] + \\Flash your RP2040 devices easily via UF2 interface. + \\ + \\Options: + \\ -h, --help Shows this help text. + \\ -d, --device Uses as the UF2 device. Otherwise tries to auto-guess the correct device. + \\ -w, --wait Waits 60 seconds until a device appears. + \\ + ); +} + +pub fn main() !u8 { + const stderr = std.io.getStdErr(); + const stdout = std.io.getStdOut(); + const stdin = std.io.getStdIn(); + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer _ = arena.deinit(); + + const allocator = arena.allocator(); + + var cli = args_parser.parseForCurrentProcess(CliOptions, allocator, .print) catch return 1; + defer cli.deinit(); + + if (cli.options.help) { + try print_usage(stdout, cli.executable_name); + return 0; + } + + if (cli.positionals.len != 1) { + try print_usage(stderr, cli.executable_name); + return 1; + } + + const uf2_file_path = cli.positionals[0]; + + var uf2_file = try std.fs.cwd().openFile(uf2_file_path, .{}); + defer uf2_file.close(); + + const uf2_stat = try uf2_file.stat(); + if ((uf2_stat.size % 512) != 0) { + std.log.warn("{s} does not have a size multiple of 512. might be corrupt!", .{uf2_file_path}); + } + + const file_valid = blk: { + try uf2_file.seekTo(0); + + var file_valid = true; + + while (file_valid) { + var block: [512]u8 = undefined; + const len = try uf2_file.read(&block); + if (len == 0) + break; + + // 0 4 First magic number, 0x0A324655 ("UF2\n") + // 4 4 Second magic number, 0x9E5D5157 + // 8 4 Flags + // 12 4 Address in flash where the data should be written + // 16 4 Number of bytes used in data (often 256) + // 20 4 Sequential block number; starts at 0 + // 24 4 Total number of blocks in file + // 28 4 File size or board family ID or zero + // 32 476 Data, padded with zeros + // 508 4 Final magic number, 0x0AB16F30 + + const first_magic_number = std.mem.readIntLittle(u32, block[0..][0..4]); + const second_magic_number = std.mem.readIntLittle(u32, block[4..][0..4]); + const final_magic_number = std.mem.readIntLittle(u32, block[508..][0..4]); + + file_valid = file_valid and (first_magic_number == 0x0A324655); + file_valid = file_valid and (second_magic_number == 0x9E5D5157); + file_valid = file_valid and (final_magic_number == 0x0AB16F30); + } + break :blk file_valid; + }; + + if (file_valid == false) { + std.log.warn("{s} does not seem to be a valid UF2 file. Do you really want to flash it?", .{uf2_file_path}); + while (true) { + try stderr.writer().writeAll("Flash? [jN]: "); + + var buffer: [64]u8 = undefined; + const selection_or_null = try stdin.reader().readUntilDelimiterOrEof(&buffer, '\n'); + + const selection_str = std.mem.trim(u8, selection_or_null orelse "", "\r\n\t "); + if (selection_str.len == 0) + return 1; + + if (std.ascii.eqlIgnoreCase(selection_str, "j")) + break; + + if (std.ascii.eqlIgnoreCase(selection_str, "n")) + return 1; + } + } + + try uf2_file.seekTo(0); + + const detect_timeout = std.time.nanoTimestamp() + wait_device_avail_timeout; + var first_run = true; + const device_path = if (cli.options.device) |devname| + try allocator.dupe(u8, devname) + else while (true) { + if (std.time.nanoTimestamp() >= detect_timeout) { + try stderr.writeAll("failed to detect any RP2040 devices :(\n"); + + return 1; + } + + const maybe_device = try autoDetectPico(allocator); + + if (maybe_device) |device| + break device; + + if (!cli.options.wait) { + try stderr.writeAll("failed to detect any RP2040 devices :(\n"); + return 1; + } + + if (first_run) { + try stderr.writeAll("failed to detect any RP2040 devices, waiting...\n"); + first_run = false; + } + + std.time.sleep(250 * std.time.ns_per_ms); + }; + + const connect_timeout = std.time.nanoTimestamp() + wait_device_ready_timeout; + + var first_attempt = true; + var access_denied_counter: u32 = 0; + var last_err: anyerror = error.Unknown; + var device_file: std.fs.File = blk: while (std.time.nanoTimestamp() < connect_timeout) { + var device = std.fs.cwd().openFile(device_path, .{ .mode = .write_only }) catch |err| { + last_err = err; + + switch (err) { + error.FileNotFound => {}, // just waiting for the device + error.AccessDenied => { + access_denied_counter += 1; + if (access_denied_counter >= access_denied_limit) { + try stderr.writer().print("Could not open {s}: Access denied. Do you have write-access to the device?\n", .{device_path}); + return 1; + } + }, + else => |e| return e, + } + + if (first_attempt) { + try stderr.writer().print("Waiting for {s}.", .{device_path}); + first_attempt = false; + } else { + try stderr.writeAll("."); + } + std.time.sleep(250 * std.time.ns_per_ms); + continue; + }; + try stderr.writeAll("\n"); + break :blk device; + } else { + try stderr.writer().print("\nfailed to connect to {s}: {s}\n", .{ device_path, @errorName(last_err) }); + return 1; + }; + defer device_file.close(); + + try stderr.writeAll("Flashing"); + + { + try uf2_file.seekTo(0); + + var block_num: u64 = 0; + while (true) { + try stderr.writeAll("."); + + var block: [512]u8 = undefined; + const rd_len = try uf2_file.read(&block); + if (rd_len == 0) + break; + if (rd_len != block.len) { + try stderr.writer().print("\nFailed to read block {}: Only {} bytes read!\n", .{ block_num, rd_len }); + return 1; + } + + const wr_len = try device_file.write(&block); + if (wr_len != block.len) { + try stderr.writer().print("\nFailed to write block {}: Only {} bytes written!\n", .{ block_num, wr_len }); + return 1; + } + + block_num += 1; + } + } + try stderr.writeAll("\nDone.\n"); + + return 0; +} + +fn autoDetectPico(allocator: std.mem.Allocator) !?[]const u8 { + switch (builtin.os.tag) { + .linux => { + const stdin = std.io.getStdIn(); + const stderr = std.io.getStdErr(); + + const Device = struct { + name: []const u8, + path: []const u8, + }; + + var picos = std.ArrayList(Device).init(allocator); + defer picos.deinit(); + + var base_dir = try std.fs.openIterableDirAbsolute("/sys/block/", .{}); + defer base_dir.close(); + + var iter = base_dir.iterate(); + + while (try iter.next()) |entry| { + var device_dir = try base_dir.dir.openDir(entry.name, .{}); + defer device_dir.close(); + + const H = struct { + fn isPicoDevice(dir: std.fs.Dir, allo: std.mem.Allocator) !?[]const u8 { + // "/sys/block/*/removable" => "1" + // "/sys/block/*/device/model" => "RP2" + // "/sys/block/*/device/vendor" => "RPI" + + var buffer: [64]u8 = undefined; + + const removable = std.mem.trim(u8, try dir.readFile("removable", &buffer), "\r\n\t "); + if (!std.mem.eql(u8, removable, "1")) + return null; + + const device_model = std.mem.trim(u8, try dir.readFile("device/model", &buffer), "\r\n\t "); + if (!std.mem.eql(u8, device_model, "RP2")) + return null; + + const device_vendor = std.mem.trim(u8, try dir.readFile("device/vendor", &buffer), "\r\n\t "); + if (!std.mem.eql(u8, device_vendor, "RPI")) + return null; + + const device_id = std.mem.trim(u8, try dir.readFile("dev", &buffer), "\r\n\t "); + + return try std.fs.path.join(allo, &.{ + "/dev/block", device_id, + }); + } + }; + + const maybe_device = H.isPicoDevice(device_dir, allocator) catch |err| { + if (err != error.FileNotFound and err != error.AccessDenied) { + std.log.err("failed to scan /sys/block/{s}: {s}", .{ entry.name, @errorName(err) }); + } + continue; + }; + + if (maybe_device) |device_path| { + try picos.append(Device{ + .name = try allocator.dupe(u8, entry.name), + .path = device_path, + }); + } + } + + if (picos.items.len == 0) { + return null; + } + + var default_selection: usize = 0; + + try stderr.writer().writeAll("Select your device:\n"); + for (picos.items, 1..) |pico_dev, index| { + try stderr.writer().print("#{d: <2} {s}\n", .{ index, pico_dev.name }); + + if (default_selection == 0) { + default_selection = index; + } + } + + const selection = while (true) { + try stderr.writer().print("Select port [{}]: ", .{default_selection}); + + var buffer: [64]u8 = undefined; + const selection_or_null = try stdin.reader().readUntilDelimiterOrEof(&buffer, '\n'); + + const selection_str = std.mem.trim(u8, selection_or_null orelse break default_selection, "\r\n\t "); + + if (selection_str.len == 0) + break default_selection; + + const selection = std.fmt.parseInt(usize, selection_str, 10) catch continue; + + if (selection < 1 or selection > picos.items.len) { + continue; + } + + break selection; + }; + + return picos.items[selection - 1].path; + }, + + else => { + std.log.warn("Device auto-detection not implemented for {s}", .{@tagName(builtin.os.tag)}); + return null; + }, + } +}