From f5f8068fac59b4f16046c2022c2fc7c7e447ef4c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 25 Aug 2026 12:40:53 -0300 Subject: zig-p4: pure-Zig ESP32-P4 toolchain build.zig generates the linker script and drives Zig's own LLD; tools/image.zig turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker. src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers; src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip; src/oracle/ diffs this HAL against ESP-IDF's on the die. --- src/main.zig | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/main.zig (limited to 'src/main.zig') diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..2e65060 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,104 @@ +//! Demo application: prove the toolchain, then blink. +//! +//! Nothing here is framework code. There is no FreeRTOS, no `app_main`, no libc and no +//! `compiler_rt`: `_start` is the reset entry the bootloader jumps to, the peripherals are the +//! comptime register model in soc.zig, and `printf` is a mask-ROM address. + +const std = @import("std"); +const config = @import("config"); +const soc = @import("soc"); + +const led: u6 = @intCast(config.led_pin); + +/// Zero-initialised statics live in L2MEM, which the bootloader does not clear. `_start` clears +/// them, and this array exists so the board can prove it happened. +export var bss_probe: [64]u32 = @splat(0); + +export fn zig_main() noreturn { + // readback: enable the pad's input buffer too, so the pin can be sampled while it is driven - + // which is what makes the blink self-verifying rather than hopeful. + soc.gpio.configureOutput(led, .{ .readback = true }); + + soc.rom.print("\r\nMARK ZIG_P4 toolchain=zig-only, no cmake, no esptool, no external linker\r\n", .{}); + soc.rom.print("MARK ZIG_P4_ROM ets_printf@0x%x reached from zig\r\n", .{@as(u32, @intFromPtr(&soc.rom.ets_printf))}); + + var bss_or: u32 = 0; + for (&bss_probe) |*w| bss_or |= @as(*volatile u32, w).*; + soc.rom.print("MARK ZIG_P4_BSS or-of-64-words=0x%08x expect=0x00000000\r\n", .{bss_or}); + + // A comptime hash of a compile-time string, checked against a value computed on the host: + // if the ABI or the linker script were wrong, this would not match. + const fnv = comptime fnv1a("0x4200.cafe"); + soc.rom.print("MARK ZIG_P4_FNV fnv1a(0x4200.cafe)=0x%08x expect=0x68440dea\r\n", .{fnv}); + + // A float the compiler cannot fold: loaded through a volatile pointer, so the arithmetic really + // happens at run time as fcvt.s.wu/fmul.s/fadd.s. Without the mstatus.FS write in _start this + // line is an unhandled illegal instruction - and with a constant the compiler would fold it + // away and the check would pass on a board whose FPU is still off. + var seven: u32 = 7; + var fx: f32 = @floatFromInt(@as(*volatile u32, &seven).*); + fx = fx * 1.5 + 0.25; + soc.rom.print("MARK ZIG_P4_FPU 7*1.5+0.25=%u.%u expect=10.75\r\n", .{ + @as(u32, @intFromFloat(fx)), @as(u32, @intFromFloat((fx - @trunc(fx)) * 100)), + }); + + const t0 = soc.cycles(); + soc.rom.ets_delay_us(1000); + const per_ms = soc.cycles() - t0; + soc.rom.print("MARK ZIG_P4_CLOCK %u cycles per ms\r\n", .{@as(u32, @intCast(per_ms))}); + + var beat: u32 = 0; + while (true) : (beat += 1) { + soc.gpio.setHigh(led); + const high = soc.gpio.getLevel(led); // sampled while driven high, so it proves the toggle + soc.rom.ets_delay_us(500_000); + soc.gpio.setLow(led); + const low = soc.gpio.getLevel(led); + soc.rom.ets_delay_us(500_000); + if (beat % 4 == 0) { + soc.rom.print("MARK ZIG_P4_ALIVE beat=%u gpio%u high=%u low=%u\r\n", .{ + beat, @as(u32, led), @as(u32, high), @as(u32, low), + }); + } + } +} + +fn fnv1a(comptime s: []const u8) u32 { + var h: u32 = 0x811c9dc5; + for (s) |b| { + h ^= b; + h = h *% 0x0100_0193; + } + return h; +} + +/// Reset entry. The bootloader hands over with an unspecified stack pointer and with the FPU +/// switched off, so: enable the F extension (mstatus.FS = Initial - the target includes `f`, and +/// ESP-IDF only ever enables the unit lazily from a trap handler, which this image does not have), +/// establish a stack in L2MEM, clear .bss, then call into Zig proper. +export fn _start() linksection(".text.entry") callconv(.naked) noreturn { + asm volatile ( + \\ li t0, 1 << 13 + \\ csrs mstatus, t0 + \\ la sp, __stack_top + \\ mv fp, sp + \\ la t0, __bss_start + \\ la t1, __bss_end + \\ bgeu t0, t1, 2f + \\1: + \\ sw zero, 0(t0) + \\ addi t0, t0, 4 + \\ bltu t0, t1, 1b + \\2: + \\ j zig_main + ); +} + +/// The current spelling: std wraps a bare three-argument `panic` in a compatibility shim whose own +/// comment calls it deprecated. +pub const panic = std.debug.FullPanic(struct { + fn call(msg: []const u8, _: ?usize) noreturn { + soc.rom.print("MARK ZIG_P4_PANIC %s\r\n", .{msg.ptr}); + while (true) {} + } +}.call); -- cgit v1.3