commit 84ee98bde8cbb1ae93be63a2559ce4cc0f4c056b
parent da6ca5a829f07c4bfcf9ffafb5eece2828ee4c09
Author: Sylvia Ivory <git@sivory.net>
Date: Fri, 30 Jan 2026 20:41:16 -0800
Bootloader local side
Diffstat:
7 files changed, 270 insertions(+), 38 deletions(-)
diff --git a/build.zig b/build.zig
@@ -195,6 +195,9 @@ pub fn build(b: *std.Build) !void {
installer.addCSourceFile(.{ .file = b.path("include/setup-tty.c") });
installer.addIncludePath(b.path("include/"));
+ const clap = b.dependency("clap", .{});
+ installer.addImport("clap", clap.module("clap"));
+
const exe = b.addExecutable(.{
.name = "pi-install",
.linkage = .dynamic,
diff --git a/build.zig.zon b/build.zig.zon
@@ -32,35 +32,10 @@
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
- // See `zig fetch --save <url>` for a command-line interface for adding dependencies.
- //.example = .{
- // // When updating this field to a new URL, be sure to delete the corresponding
- // // `hash`, otherwise you are communicating that you expect to find the old hash at
- // // the new URL. If the contents of a URL change this will result in a hash mismatch
- // // which will prevent zig from using it.
- // .url = "https://example.com/foo.tar.gz",
- //
- // // This is computed from the file contents of the directory of files that is
- // // obtained after fetching `url` and applying the inclusion rules given by
- // // `paths`.
- // //
- // // This field is the source of truth; packages do not come from a `url`; they
- // // come from a `hash`. `url` is just one of many possible mirrors for how to
- // // obtain a package matching this `hash`.
- // //
- // // Uses the [multihash](https://multiformats.io/multihash/) format.
- // .hash = "...",
- //
- // // When this is provided, the package is found in a directory relative to the
- // // build root. In this case the package's hash is irrelevant and therefore not
- // // computed. This field and `url` are mutually exclusive.
- // .path = "foo",
- //
- // // When this is set to `true`, a package is declared to be lazily
- // // fetched. This makes the dependency only get fetched if it is
- // // actually used.
- // .lazy = false,
- //},
+ .clap = .{
+ .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.11.0.tar.gz",
+ .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e",
+ },
},
// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
diff --git a/include/setup-tty.c b/include/setup-tty.c
@@ -0,0 +1,83 @@
+#include <fcntl.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <termios.h>
+
+#include "setup-tty.h"
+
+unsigned parse_baud(unsigned rate) {
+ switch(rate) {
+ case 9600: return B9600;
+ case 115200: return B115200;
+ case 230400: return B230400;
+ case 460800: return B460800;
+ #ifdef B576000
+ case 576000: return B576000;
+ #endif
+ #ifdef B921600
+ case 921600: return B921600;
+ #endif
+ #ifdef B1000000
+ case 1000000: return B1000000;
+ #endif
+ default:
+ return B115200;
+ }
+}
+
+// XXX: if anyone figures out a cleaner way to do this, lmk. I don't
+// have a mac, so stopped as soon as we had something that worked on
+// linux & macos.
+// params:
+// - <timeout> is in seconds (< 1 ok)
+// - <speed> is baud rate.
+int set_tty_to_8n1(int fd, unsigned speed, double timeout) {
+ struct termios tty;
+ memset(&tty, 0, sizeof tty);
+ if (tcgetattr (fd, &tty) != 0)
+ return -1;
+ memset (&tty, 0, sizeof tty);
+
+ // https://github.com/rlcheng/raspberry_pi_workshop
+ cfsetspeed(&tty, speed);
+
+ // disable IGNBRK for mismatched speed tests; otherwise receive break
+ // as \000 chars
+
+ // XXX: wait, does this disable break or ignore-ignore break??
+ tty.c_iflag &= ~IGNBRK; // disable break processing
+ tty.c_lflag = 0; // no signaling chars, no echo,
+ // no canonical processing
+ tty.c_oflag = 0; // no remapping, no delays
+ tty.c_cc[VMIN] = 0; // read doesn't block
+ // VTIME is in .1 seconds, so have to multiply by 10.
+ tty.c_cc[VTIME] = (int)(timeout *10); // this seems to cause issues?
+
+ /*
+ * Setup TTY for 8n1 mode, used by the pi UART.
+ */
+
+ // Disables the Parity Enable bit(PARENB),So No Parity
+ tty.c_cflag &= ~PARENB;
+ // CSTOPB = 2 Stop bits,here it is cleared so 1 Stop bit
+ tty.c_cflag &= ~CSTOPB;
+ // Clears the mask for setting the data size
+ tty.c_cflag &= ~CSIZE;
+ // Set the data bits = 8
+ tty.c_cflag |= CS8;
+ // No Hardware flow Control
+ tty.c_cflag &= ~CRTSCTS;
+ // Enable receiver,Ignore Modem Control lines
+ tty.c_cflag |= CREAD | CLOCAL;
+
+ // Disable XON/XOFF flow control both i/p and o/p
+ tty.c_iflag &= ~(IXON | IXOFF | IXANY);
+ // Non Cannonical mode
+ tty.c_iflag &= ~(ICANON | ECHO | ECHOE | ISIG);
+ // No Output Processing
+ tty.c_oflag &= ~OPOST;
+
+ if(tcsetattr (fd, TCSANOW, &tty) != 0)
+ return -1;
+ return fd;
+}
diff --git a/include/setup-tty.h b/include/setup-tty.h
@@ -0,0 +1,2 @@
+unsigned parse_baud(unsigned rate);
+int set_tty_to_8n1(int fd, unsigned speed, double timeout);
diff --git a/installer/bootloader.zig b/installer/bootloader.zig
@@ -0,0 +1,82 @@
+const std = @import("std");
+
+const net = @import("./net.zig");
+
+const Message = net.Message;
+const Error = net.Error;
+
+// w - UART (unsafe)
+// r - UART (unsafe)
+pub fn boot(w: *std.Io.Writer, r: *std.Io.Reader, code: []u8, base_addr: u32) !void {
+ const checksum = std.hash.Crc32.hash(code);
+
+ std.debug.print("crc32={x}", .{checksum});
+
+ const parent_progress = std.Progress.start(.{});
+
+ const header: net.Header = .{
+ .arm_base = base_addr,
+ .checksum = checksum,
+ .n_bytes = @truncate(code.len),
+ };
+
+ parent_progress.setName("waiting for a start");
+
+ while (try net.get_u32(r) != @intFromEnum(Message.GET_PROG_INFO)) {
+ const byte = try net.get_u8(r);
+ std.debug.print("got invalid GET_PROG_INFO: 0x{X}\n", .{byte});
+ }
+
+ parent_progress.setName("sending header");
+
+ try net.transmit_header(w, header);
+
+ parent_progress.setName("waiting for response");
+
+ while (true) {
+ switch (try net.get_u32(r)) {
+ @intFromEnum(Message.GET_CODE) => break,
+ @intFromEnum(Message.BAD_CODE_ADDR) => return Error.BadAddress,
+ else => continue,
+ }
+ }
+
+ const checksum_return = try net.get_u32(r);
+
+ if (checksum != checksum_return) {
+ return Error.BadChecksum;
+ }
+
+ parent_progress.setName("got response, sending code");
+
+ const transmit_progress = parent_progress.start("uploading", code.len);
+ try net.transmit_code(w, transmit_progress, code);
+
+ parent_progress.setName("waiting for response");
+
+ while (true) {
+ const b = try net.get_u32(r);
+ switch (b) {
+ @intFromEnum(Message.BOOT_SUCCESS) => break,
+ @intFromEnum(Message.BAD_CODE_CKSUM) => return Error.BadChecksum,
+ else => {
+ var buffer: [4]u8 = undefined;
+ std.mem.writeInt(u32, &buffer, b, .little);
+ std.debug.print("{s}", .{buffer});
+ },
+ }
+ }
+
+ var stdout_buffer: [1024]u8 = undefined;
+ var stdout = std.fs.File.stdout().writer(&stdout_buffer);
+ const stdout_writer = &stdout.interface;
+
+ parent_progress.setName("got response, booting");
+ parent_progress.end();
+
+ while (true) {
+ const b = try net.get_u8(r);
+ try stdout_writer.writeByte(b);
+ try stdout_writer.flush();
+ }
+}
diff --git a/installer/net.zig b/installer/net.zig
@@ -29,10 +29,6 @@ pub fn get_u32(r: *std.Io.Reader) !u32 {
try r.readSliceAll(&buffer);
const v = std.mem.readInt(u32, &buffer, .little);
- if (@import("builtin").abi.isGnu()) {
- std.debug.print("GET32:0x{X}\n", .{v});
- }
-
return v;
}
@@ -43,10 +39,6 @@ pub fn get_u8(r: *std.Io.Reader) !u8 {
}
pub fn put_u32(w: *std.Io.Writer, v: u32) !void {
- if (@import("builtin").abi.isGnu()) {
- std.debug.print("PUT32:0x{X}\n", .{v});
- }
-
var buffer: [@sizeOf(u32)]u8 = undefined;
std.mem.writeInt(u32, &buffer, v, .little);
try w.writeAll(&buffer);
@@ -82,10 +74,12 @@ pub fn receive_header(r: *std.Io.Reader, max_size: u32) !Header {
return .{ .arm_base = arm_base, .checksum = checksum, .n_bytes = n_bytes };
}
-pub fn transmit_code(w: *std.Io.Writer, code: []const u8) !void {
+pub fn transmit_code(w: *std.Io.Writer, progress: std.Progress.Node, code: []const u8) !void {
+ defer progress.end();
try put_message(w, .PUT_CODE);
for (code) |byte| {
try put_u8(w, byte);
+ progress.completeOne();
}
}
diff --git a/installer/root.zig b/installer/root.zig
@@ -0,0 +1,93 @@
+const std = @import("std");
+const clap = @import("clap");
+
+const bootloader = @import("./bootloader.zig");
+const c = @cImport({
+ @cInclude("setup-tty.h");
+});
+
+pub fn main() !void {
+ var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
+ const allocator = gpa.allocator();
+ defer _ = gpa.deinit();
+
+ const params = comptime clap.parseParamsComptime(
+ \\-h, --help display this help and exit
+ \\-b, --baud <baud> baud rate
+ \\-d, --device <device> device path
+ \\-t, --timeout <float> UART timeout
+ \\-b, --arm-base <addr> executable base address
+ \\<file> binary to run
+ \\
+ );
+
+ const parsers = .{
+ .baud = clap.parsers.int(u32, 10),
+ .file = clap.parsers.string,
+ .device = clap.parsers.string,
+ .float = clap.parsers.float(f64),
+ .addr = clap.parsers.int(u32, 16),
+ };
+
+ var diagnostics: clap.Diagnostic = .{};
+ var res = clap.parse(clap.Help, ¶ms, parsers, .{
+ .diagnostic = &diagnostics,
+ .allocator = allocator,
+ }) catch |e| {
+ try diagnostics.reportToFile(.stderr(), e);
+ return e;
+ };
+ defer res.deinit();
+
+ if (res.args.help != 0) {
+ std.debug.print("pi-install ", .{});
+ try clap.usageToFile(.stderr(), clap.Help, ¶ms);
+ std.debug.print("\n", .{});
+
+ return clap.helpToFile(.stderr(), clap.Help, ¶ms, .{
+ .description_on_new_line = false,
+ .spacing_between_parameters = 0,
+ });
+ }
+
+ const tty_path = res.args.device orelse "/dev/ttyUSB0";
+ const baud = c.parse_baud(res.args.baud orelse 115200);
+ const path = res.positionals[0] orelse return error.MissingFile;
+ const base_addr = res.args.@"arm-base" orelse 0x8000;
+ const timeout = res.args.timeout orelse 10.0;
+
+ std.debug.print("tty-usb=<{s}> baud=<{d}> program=<{s}> base-addr=<0x{X}> timeout=<{d}s>\n", .{ tty_path, baud, path, base_addr, timeout });
+
+ const tty = try std.fs.openFileAbsolute(tty_path, .{ .allow_ctty = true, .mode = .read_write });
+
+ if (c.set_tty_to_8n1(tty.handle, baud, timeout) == -1) {
+ return error.SetTTY;
+ }
+
+ var code: []u8 = undefined;
+
+ {
+ const cwd = std.fs.cwd();
+
+ var reader_buffer: [1024]u8 = undefined;
+ var code_file = try cwd.openFile(path, .{ .mode = .read_only });
+ var code_reader = code_file.reader(&reader_buffer);
+ var r = &code_reader.interface;
+
+ const size = (try code_file.stat()).size;
+
+ code = try r.readAlloc(allocator, size);
+ }
+
+ defer allocator.free(code);
+
+ var reader_buffer: [1024]u8 = undefined;
+ var file_reader = tty.reader(&reader_buffer);
+ const r = &file_reader.interface;
+
+ var writer_buffer: [1024]u8 = undefined;
+ var file_writer = tty.writer(&writer_buffer);
+ const w = &file_writer.interface;
+
+ try bootloader.boot(w, r, code, base_addr);
+}