commit da6ca5a829f07c4bfcf9ffafb5eece2828ee4c09
parent 1831e7bc39f00ce900135d03aa188bb1cc1afb90
Author: Sylvia Ivory <git@sivory.net>
Date: Fri, 30 Jan 2026 19:06:25 -0800
Bootloading
Diffstat:
| M | build.zig | | | 24 | ++++++++++++++++++++++++ |
| A | installer/net.zig | | | 103 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | pi/mem.zig | | | 33 | +++++++++++++++++++++++++++++++++ |
| A | src/bootloader.zig | | | 90 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/devices/mini-uart.zig | | | 99 | +++++++++++++++++++++++++++++++++++++++++++------------------------------------ |
| A | src/lists.zig | | | 49 | +++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/main.zig | | | 28 | +++++++++++++++++++++++++++- |
| M | src/root.zig | | | 82 | ++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------- |
8 files changed, 434 insertions(+), 74 deletions(-)
diff --git a/build.zig b/build.zig
@@ -65,6 +65,12 @@ fn add_exe_real(exe_name: []const u8, path: std.Build.LazyPath, b: *std.Build) !
.optimize = .ReleaseSafe,
});
+ const net = b.createModule(.{
+ .root_source_file = b.path("installer/net.zig"),
+ .target = b.graph.host,
+ .optimize = .ReleaseSafe,
+ });
+
const boot = b.createModule(.{
.root_source_file = path,
.target = b.resolveTargetQuery(target),
@@ -75,6 +81,7 @@ fn add_exe_real(exe_name: []const u8, path: std.Build.LazyPath, b: *std.Build) !
.no_builtin = true,
});
boot.addImport("pi", pi);
+ boot.addImport("net", net);
pi.addImport("boot", boot);
const exe = b.addExecutable(.{
@@ -178,4 +185,21 @@ pub fn build(b: *std.Build) !void {
}
_ = try add_exe(@tagName(config.name), b.path("src/root.zig"), b);
+
+ const installer = b.createModule(.{
+ .root_source_file = b.path("installer/root.zig"),
+ .target = b.graph.host,
+ .optimize = .ReleaseSafe,
+ .link_libc = true,
+ });
+ installer.addCSourceFile(.{ .file = b.path("include/setup-tty.c") });
+ installer.addIncludePath(b.path("include/"));
+
+ const exe = b.addExecutable(.{
+ .name = "pi-install",
+ .linkage = .dynamic,
+ .root_module = installer,
+ });
+
+ b.installArtifact(exe);
}
diff --git a/installer/net.zig b/installer/net.zig
@@ -0,0 +1,103 @@
+const std = @import("std");
+
+pub const Message = enum(u32) {
+ GET_PROG_INFO = 0x11112222,
+ PUT_PROG_INFO = 0x33334444,
+
+ GET_CODE = 0x55556666,
+ PUT_CODE = 0x77778888,
+
+ BOOT_SUCCESS = 0x9999AAAA,
+ BAD_CODE_ADDR = 0xDEADBEEF,
+ BAD_CODE_CKSUM = 0xFEEDFACE,
+};
+
+pub const Error = error{
+ BadAddress,
+ BadChecksum,
+ Timeout,
+};
+
+pub const Header = struct {
+ arm_base: u32,
+ n_bytes: u32,
+ checksum: u32,
+};
+
+pub fn get_u32(r: *std.Io.Reader) !u32 {
+ var buffer: [@sizeOf(u32)]u8 = undefined;
+ 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;
+}
+
+pub fn get_u8(r: *std.Io.Reader) !u8 {
+ var buffer: [1]u8 = undefined;
+ try r.readSliceAll(&buffer);
+ return buffer[0];
+}
+
+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);
+ try w.flush();
+}
+
+pub fn put_u8(w: *std.Io.Writer, v: u8) !void {
+ try w.writeByte(v);
+ try w.flush();
+}
+
+pub fn put_message(w: *std.Io.Writer, message: Message) !void {
+ try put_u32(w, @intFromEnum(message));
+}
+
+pub fn transmit_header(w: *std.Io.Writer, header: Header) !void {
+ try put_message(w, .PUT_PROG_INFO);
+ try put_u32(w, header.arm_base);
+ try put_u32(w, header.n_bytes);
+ try put_u32(w, header.checksum);
+}
+
+pub fn receive_header(r: *std.Io.Reader, max_size: u32) !Header {
+ const arm_base = try get_u32(r);
+ const n_bytes = try get_u32(r);
+ const checksum = try get_u32(r);
+
+ // TODO; better check
+ if (arm_base + n_bytes >= max_size) {
+ return Error.BadAddress;
+ }
+
+ return .{ .arm_base = arm_base, .checksum = checksum, .n_bytes = n_bytes };
+}
+
+pub fn transmit_code(w: *std.Io.Writer, code: []const u8) !void {
+ try put_message(w, .PUT_CODE);
+ for (code) |byte| {
+ try put_u8(w, byte);
+ }
+}
+
+pub fn receive_code(r: *std.Io.Reader, buffer: []u8, header: Header) !void {
+ try r.readSliceAll(buffer);
+
+ if (std.hash.Crc32.hash(buffer) != header.checksum) {
+ return Error.BadChecksum;
+ }
+}
+
+pub fn request_code(w: *std.Io.Writer, crc: u32) !void {
+ try put_message(w, .GET_CODE);
+ try put_u32(w, crc);
+}
diff --git a/pi/mem.zig b/pi/mem.zig
@@ -61,3 +61,36 @@ pub inline fn get_u32_barrier(address: *allowzero u32) u32 {
barrier(.Read);
return get_u32(address);
}
+
+pub const CriticalSection = struct {
+ const Self = @This();
+
+ saved_cpsr: u32,
+
+ // TODO; barrier?
+ pub inline fn enter() Self {
+ var cpsr: u32 = undefined;
+
+ asm volatile (
+ \\ mrs %[cpsr], cpsr
+ \\ cpsid i
+ : [cpsr] "=r" (cpsr),
+ :
+ : .{ .memory = true });
+
+ return .{ .saved_cpsr = cpsr };
+ }
+
+ pub inline fn exit(self: Self) void {
+ asm volatile (
+ \\ msr cpsr_c, %[cpsr]
+ :
+ : [cpsr] "r" (self.saved_cpsr),
+ : .{
+ .cpsr = true,
+ .memory = true,
+ });
+ }
+};
+
+pub const enter_critical_section = CriticalSection.enter;
diff --git a/src/bootloader.zig b/src/bootloader.zig
@@ -0,0 +1,90 @@
+const std = @import("std");
+
+const net = @import("net");
+const pi = @import("pi");
+
+const uart = @import("devices/mini-uart.zig");
+const clock = @import("devices/clock.zig");
+
+const Message = net.Message;
+const Error = net.Error;
+
+const MAX_SIZE = 0x200000 - 0x8004;
+
+// w - UART (safe)
+// r - UART (safe)
+pub fn boot(w: *std.Io.Writer, r: *std.Io.Reader) void {
+ while (true) {
+ // UART cannot fail
+ net.put_message(w, .GET_PROG_INFO) catch @panic("get_prog_info");
+
+ if (uart.read_queue_length() >= 4) {
+ const response = net.get_u32(r) catch @panic("put_prog_info");
+
+ if (response == @intFromEnum(Message.PUT_PROG_INFO)) break;
+ }
+
+ clock.delay_ms(300);
+ }
+
+ const header = net.receive_header(r, MAX_SIZE) catch |e| {
+ if (e == Error.BadAddress) {
+ net.put_message(w, .BAD_CODE_ADDR) catch @panic("bad_code_addr");
+
+ // TODO; reboot
+ return;
+ }
+
+ @panic("header");
+ };
+
+ net.request_code(w, header.checksum) catch @panic("checksum");
+
+ while (true) {
+ if (uart.read_queue_length() >= 4) {
+ const response = net.get_u32(r) catch @panic("put_code");
+
+ if (response == @intFromEnum(Message.PUT_CODE)) break;
+ }
+ }
+
+ var start_ptr: [*]u8 = @ptrFromInt(header.arm_base);
+ // var start_ptr: [4096]u8 = .{0} ** 4096;
+
+ // var hasher = std.hash.Crc32.init();
+ // var n: usize = 0;
+ // while (n < header.n_bytes) {
+ // const read = uart.read_byte_blocking(null).?;
+
+ // start_ptr[n] = read;
+ // uart.writer.print("TRACE:PUT8:{X}\n", .{read}) catch @panic("read");
+ // n += 1;
+ // }
+ // start_ptr[header.n_bytes - 1] = 0;
+ // uart.writer.print("read {d} bytes\nhash: 0x{X}, hash2: 0x{X}, expected 0x{X}\nb64: {b64}\n", .{ n, hasher.final(), std.hash.Crc32.hash(start_ptr[0..header.n_bytes]), header.checksum, start_ptr[0..n] }) catch @panic("debug");
+
+ net.receive_code(r, start_ptr[0..(header.n_bytes)], header) catch |e| {
+ if (e == Error.BadChecksum) {
+ net.put_message(w, .BAD_CODE_CKSUM) catch @panic("bad_code_cksum");
+
+ // TODO; reboot
+ return;
+ }
+
+ @panic("code");
+ };
+
+ net.put_message(w, .BOOT_SUCCESS) catch @panic("boot_success");
+ uart.flush();
+
+ branch_to(@ptrCast(start_ptr));
+}
+
+comptime {
+ asm (
+ \\ .globl branch_to
+ \\ branch_to:
+ \\ bx r0
+ );
+}
+extern fn branch_to(to: *u8) noreturn;
diff --git a/src/devices/mini-uart.zig b/src/devices/mini-uart.zig
@@ -1,6 +1,8 @@
const std = @import("std");
const pi = @import("pi");
+const lists = @import("../lists.zig");
+const clock = @import("clock.zig");
const gpio = @import("./gpio.zig");
const interrupts = pi.interrupts;
@@ -109,6 +111,7 @@ pub fn initialize(baud: u32, tx_pin: TxPin, rx_pin: RxPin) Error!void {
initialized = true;
}
+// Separate function to allow TX used during early boot
pub fn enable_interrupts() Error!void {
if (!initialized) return Error.NotInitialized;
if (interrupts_enabled) return Error.AlreadyInitialized;
@@ -135,13 +138,14 @@ pub fn enable_interrupts() Error!void {
// var tx_buffer: [1024]u8 = undefined;
// var tx_list: std.ArrayList(u8) = .initBuffer(&tx_buffer);
-var rx_buffer: [1024]u8 = undefined;
-var rx_list: std.ArrayList(u8) = .initBuffer(&rx_buffer);
+var rx_list: lists.StackRingBuffer(u8, 4096) = .init();
noinline fn uart_handler(pc: usize) void {
_ = pc;
mem.barrier(.Write);
+ defer mem.barrier(.Write);
+ if (!initialized) return;
if (!interrupts.pending_peripheral_interrupt(.AuxInterrupt)) return;
while (true) {
@@ -151,7 +155,8 @@ noinline fn uart_handler(pc: usize) void {
if ((IIR & 0b1) == 1) return;
if ((IIR & 0b100) != 0) {
- rx_list.appendAssumeCapacity(read_byte(true));
+ // can't do anything with error
+ rx_list.push(read_byte_raw()) catch {};
}
// TODO; figure out why TX interrupts aren't working
@@ -167,8 +172,6 @@ noinline fn uart_handler(pc: usize) void {
// // }
// }
}
-
- mem.barrier(.Write);
}
pub fn is_initialized() bool {
@@ -197,36 +200,51 @@ pub fn status() Status {
return @bitCast(mem.get_u32_barrier(@ptrFromInt(AUX_MU_STAT_REG)));
}
+pub fn read_queue_length() usize {
+ const cs = mem.enter_critical_section();
+ defer cs.exit();
+
+ return rx_list.length();
+}
+
pub fn can_read() bool {
// Check if FIFO has data
// Page 15: AUX_MU_LSR_REG Register
// Bit 1 is set when FIFO holds at least 1 byte
- return (mem.get_u32_barrier(@ptrFromInt(AUX_MU_LSR_REG)) & 0x01) == 1;
+ return rx_list.length() > 0; // or can_read_raw();
}
-pub fn read_queued() usize {
- return rx_list.items.len;
+// // TODO; is this necessary
+// inline fn can_read_raw() bool {
+// return (mem.get_u32_barrier(@ptrFromInt(AUX_MU_LSR_REG)) & 0x01) == 1;
+// }
+
+pub fn read_byte() ?u8 {
+ const cs = mem.enter_critical_section();
+ defer cs.exit();
+
+ return rx_list.pop();
}
-pub fn read_byte(force: bool) u8 {
- if (force) {
- // Read from FIFO
- // Page 11: AUX_MU_IO_REG Register
- // Bits 7:0 holds data from the FIFO
- return @truncate(mem.get_u32_barrier(@ptrFromInt(AUX_MU_IO_REG)));
+pub fn read_byte_blocking(timeout: ?u64) ?u8 {
+ var wait_until: u64 = std.math.maxInt(u64);
+ if (timeout) |t| {
+ wait_until = clock.current_count() + t;
}
- if (rx_list.items.len > 0) {
- // Pop from RX buffer instead
- // TODO; use FIFO
- const byte = rx_list.orderedRemove(0);
- return byte;
- }
+ while (true) {
+ if (clock.current_count() > wait_until) {
+ return null;
+ }
- // TODO; support timeout/async
- while (!can_read()) {}
+ if (read_byte()) |b| {
+ return b;
+ }
+ }
+}
- return read_byte(true);
+inline fn read_byte_raw() u8 {
+ return @truncate(mem.get_u32_barrier(@ptrFromInt(AUX_MU_IO_REG)));
}
pub fn can_write() bool {
@@ -240,34 +258,25 @@ pub fn can_write() bool {
// return tx_list.items.len;
// }
-pub fn write_byte(byte: u8, force: bool) void {
- if (force or can_write()) {
- // Write into FIFO
- // Page 11: AUX_MU_IO_REG Register
- // Bits 7:0 holds data from the FIFO
- mem.put_u32_barrier(@ptrFromInt(AUX_MU_IO_REG), @as(u32, byte) & 0xFF);
- return;
- }
-
- // if (interrupts_enabled) {
- // // Throw onto queue, will handle later
- // tx_list.appendAssumeCapacity(byte);
- // return;
- // }
-
+pub fn write_byte(byte: u8) void {
while (!can_write()) {}
- write_byte(byte, false);
+
+ mem.put_u32_barrier(@ptrFromInt(AUX_MU_IO_REG), @as(u32, byte) & 0xFF);
}
pub fn write_slice(bytes: []const u8) void {
for (bytes) |b| {
- write_byte(b, false);
+ write_byte(b);
}
}
pub fn flush() void {
// Loop until there's nothing left in TX queue
- while (status().tx_fill_level != 0) {}
+ while (true) {
+ const s = status();
+
+ if (s.tx_is_empty and s.tx_is_idle) break;
+ }
}
fn writer_drain(io_w: *std.Io.Writer, data: []const []const u8, splat: usize) !usize {
@@ -278,7 +287,7 @@ fn writer_drain(io_w: *std.Io.Writer, data: []const []const u8, splat: usize) !u
_ = splat;
for (data[0]) |b| {
- write_byte(b, false);
+ write_byte(b);
}
return data[0].len;
@@ -300,9 +309,9 @@ fn stream(io_r: *std.Io.Reader, io_w: *std.Io.Writer, limit: std.Io.Limit) !usiz
// We don't care about getting the "parent"
_ = io_r;
- if (limit.toInt()) |*max| {
+ if (limit.toInt()) |max| {
for (0..max) |_| {
- try io_w.writeByte(read_byte(false));
+ try io_w.writeByte(read_byte_blocking(null).?);
}
return max;
@@ -312,4 +321,4 @@ fn stream(io_r: *std.Io.Reader, io_w: *std.Io.Writer, limit: std.Io.Limit) !usiz
}
const reader_vtable: std.Io.Reader.VTable = .{ .stream = stream };
-pub var reader = std.Io.Reader{ .buffer = undefined, .vtable = &reader_vtable };
+pub var reader = std.Io.Reader{ .buffer = undefined, .seek = 0, .end = 0, .vtable = &reader_vtable };
diff --git a/src/lists.zig b/src/lists.zig
@@ -0,0 +1,49 @@
+pub const Error = error{
+ BufferOverflow,
+};
+
+pub fn StackRingBuffer(comptime T: type, comptime capacity: comptime_int) type {
+ return struct {
+ const Self = @This();
+
+ items: [capacity]T,
+ write_idx: usize,
+ read_idx: usize,
+ count: usize,
+
+ pub fn init() Self {
+ return Self{
+ .items = undefined,
+ .write_idx = 0,
+ .read_idx = 0,
+ .count = 0,
+ };
+ }
+
+ pub fn push(self: *Self, value: T) !void {
+ if (self.count >= capacity) {
+ return Error.BufferOverflow;
+ }
+
+ self.items[self.write_idx] = value;
+ self.write_idx = (self.write_idx + 1) % capacity;
+ self.count += 1;
+ }
+
+ pub fn pop(self: *Self) ?T {
+ if (self.count == 0) {
+ return null;
+ }
+
+ const value = self.items[self.read_idx];
+ self.read_idx = (self.read_idx + 1) % capacity;
+ self.count -= 1;
+
+ return value;
+ }
+
+ pub fn length(self: *const Self) usize {
+ return self.count;
+ }
+ };
+}
diff --git a/src/main.zig b/src/main.zig
@@ -1 +1,27 @@
-pub fn main() !void {}
+const std = @import("std");
+
+const uart = @import("devices/mini-uart.zig");
+const clock = @import("devices/clock.zig");
+
+pub fn main() !void {
+ while (true) {
+ const status = uart.status();
+
+ uart.writer.print("rx queue: {d}, rx hw: {d}, rx ready: {any}, tx queue: {d}, tx hw: {d}, tx ready: {any}\n", .{
+ uart.read_queue_length(),
+ status.rx_fill_level,
+ uart.can_read(),
+ 0,
+ status.tx_fill_level,
+ uart.can_write(),
+ }) catch {};
+
+ while (uart.can_read()) {
+ var buffer: [1]u8 = .{0};
+ uart.reader.readSliceAll(&buffer) catch {};
+ uart.writer.print(" read: {s}\n", .{buffer}) catch {};
+ }
+
+ clock.delay_s(1);
+ }
+}
diff --git a/src/root.zig b/src/root.zig
@@ -2,27 +2,12 @@ const std = @import("std");
const pi = @import("pi");
pub const uart = @import("devices/mini-uart.zig");
+const bootloader = @import("./bootloader.zig");
const user_main = @import("main.zig").main;
-extern const __bss_start__: usize;
-extern const __bss_end__: usize;
-
-fn zero_bss() void {
- // No bss data allocated
- if (__bss_start__ >= __bss_end__) return;
-
- const n: usize = @divFloor(__bss_end__ - __bss_start__, @sizeOf(u32));
-
- for (0..n) |b| {
- // Force a write
- const ptr: *allowzero u32 = @ptrFromInt(__bss_start__ + b * @sizeOf(u32));
- pi.mem.put_u32(ptr, 0);
- }
-}
-
fn initialize_interrupts() void {
uart.write_slice(" Disabling interrupts\n");
- pi.interrupts.disable_interrupts();
+ _ = pi.interrupts.disable_interrupts();
pi.mem.barrier(.Write);
uart.write_slice(" Clearing interrupt flags\n");
@@ -37,11 +22,6 @@ fn initialize_interrupts() void {
export fn kmain() void {
uart.initialize(115200, .Gpio14, .Gpio15) catch {};
- uart.write_slice("Clearing bss\n");
- uart.writer.print(" __bss_start__ = 0x{X}\n", .{__bss_start__}) catch {};
- uart.writer.print(" __bss_end__ = 0x{X}\n", .{__bss_end__}) catch {};
- zero_bss();
-
uart.write_slice("Initializing interrupts\n");
initialize_interrupts();
@@ -58,18 +38,64 @@ export fn kmain() void {
uart.enable_interrupts() catch {};
pi.interrupts.enable_interrupts();
- uart.write_slice("Calling user main\n");
+ uart.write_slice("Starting bootloader:\n");
+ bootloader.boot(&uart.writer, &uart.reader);
- user_main() catch |e| {
- uart.writer.print("main returned error: {t}\n", .{e}) catch {};
- };
+ // uart.write_slice("Calling user main\n");
+ // user_main() catch |e| {
+ // uart.writer.print("main returned error: {t}\n", .{e}) catch {};
+ // };
}
+export const __stack_init__: u32 = 0x08000000;
+
export fn _start() linksection(".kmain") callconv(.naked) noreturn {
+ // Add space for bootloader
+ // asm volatile (
+ // \\ mov sp, 0x08000000
+ // \\ mov fp, 0x0
+ // \\ bl kmain
+ // \\ bl abort
+ // );
asm volatile (
- \\ mov sp, 0x800000
+ \\ b skip
+ \\ .space 0x200000-0x8004,0
+ \\ skip:
+ \\ // Switch to Super mode and disable FIQ/IRQ
+ \\ mrs r0, cpsr
+ \\ and r0, r0, %[CLEAR_MODE_MASK]
+ \\ orr r0, r0, %[SUPER_MODE]
+ \\ orr r0, r0, %[CLEAR_MODE_IRQ_FIQ]
+ \\ msr cpsr, r0
+ \\ // Prefetch flush
+ \\ mov r0, #0
+ \\ mcr p15, 0, r0, c7, c5, 4
+ \\ // Clear BSS
+ \\ mov r0, #0
+ \\ ldr r1, bss_start
+ \\ ldr r2, bss_end
+ \\ subs r2, r2, r1
+ \\ beq L3
+ \\ L2:
+ \\ strb r0, [r1], #1
+ \\ subs r2, r2, #1
+ \\ bne L2
+ \\ L3:
+ \\ // Initialize SP
+ \\ ldr sp, stack_init
+ \\ and sp, sp, -16
+ \\ // Clear the frame pointer
+ \\ mov fp, #0
+ \\ // Boot
\\ bl kmain
\\ bl abort
+ \\ bss_start: .word __bss_start__
+ \\ bss_end: .word __bss_end__
+ \\ stack_init: .word __stack_init__
+ :
+ : [CLEAR_MODE_MASK] "i" (@as(u32, 0b11111)),
+ [SUPER_MODE] "i" (@as(u32, 0b10011)),
+ [CLEAR_MODE_IRQ_FIQ] "i" ((@as(u32, 1) << 7) | (@as(u32, 1) << 6)),
);
}
@@ -84,7 +110,7 @@ fn panic_handler(msg: []const u8, trace_addr: ?usize) noreturn {
if (uart.is_initialized()) {
uart.write_slice("kernel panic: ");
uart.write_slice(msg);
- uart.write_byte('\n', false);
+ uart.write_byte('\n');
var it = std.debug.StackIterator.init(trace_addr, null);
var ix: usize = 0;