diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-25 12:40:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 12:46:51 -0300 |
| commit | f5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch) | |
| tree | 2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/soc.zig | |
| download | esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.tar.gz esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.zip | |
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.
Diffstat (limited to 'src/soc.zig')
| -rw-r--r-- | src/soc.zig | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/src/soc.zig b/src/soc.zig new file mode 100644 index 0000000..f022b50 --- /dev/null +++ b/src/soc.zig @@ -0,0 +1,132 @@ +//! ESP32-P4 peripherals, modelled at comptime. +//! +//! There is no HAL here and no generated 20k-line register header: a `Reg` is a typed pointer to +//! an MMIO word, and a peripheral is a struct of them. Everything is `inline`, so `gpio.setHigh(20)` +//! compiles to the single `sw` instruction it should be, and a wrong bit index is a compile error +//! rather than a silent write. +//! +//! Addresses are from ESP-IDF v6.0.2 `components/soc/esp32p4/register/hw_ver1/soc/` - the pre-v3 +//! header set, which is the one that matches this silicon (rev v1.3) - except the GPIO matrix +//! signal index, which lives in `components/soc/esp32p4/include/soc/gpio_sig_map.h`. + +const std = @import("std"); + +/// A 32-bit memory-mapped register. +pub fn Reg(comptime addr: usize) type { + return struct { + pub const address = addr; + const ptr: *volatile u32 = @ptrFromInt(addr); + + pub inline fn read() u32 { + return ptr.*; + } + pub inline fn write(value: u32) void { + ptr.* = value; + } + pub inline fn set(mask: u32) void { + ptr.* = ptr.* | mask; + } + pub inline fn clear(mask: u32) void { + ptr.* = ptr.* & ~mask; + } + /// Read-modify-write a bitfield: `modify(.{ .shift = 12, .width = 3 }, 5)`. + pub inline fn modify(comptime field: Field, value: u32) void { + const mask: u32 = ((@as(u32, 1) << field.width) - 1) << field.shift; + ptr.* = (ptr.* & ~mask) | ((value << field.shift) & mask); + } + }; +} + +pub const Field = struct { shift: u5, width: u5 }; + +/// A register with a named layout: pass a packed struct whose bit width is 32 and the accessors +/// become typed, so a pad is configured by naming fields instead of shifting bits. Read-modify- +/// write stays explicit - `var v = reg.read(); v.mcu_sel = 1; reg.write(v);` - because that is one +/// load and one store, and hiding it behind a partial-update type buys nothing here. +pub fn Typed(comptime T: type, comptime addr: usize) type { + comptime std.debug.assert(@bitSizeOf(T) == 32); + return struct { + pub const address = addr; + const ptr: *volatile T = @ptrFromInt(addr); + + pub inline fn read() T { + return ptr.*; + } + pub inline fn write(value: T) void { + ptr.* = value; + } + /// Apply `f` to the current value and write the result back. + pub inline fn modify(comptime f: fn (T) T) void { + ptr.* = f(ptr.*); + } + }; +} + +/// An array of identical registers. The index type is narrowed to the array's real range, so an +/// out-of-range access is a compile error in every optimize mode - an `assert` would have been +/// compiled out under ReleaseSmall, which is this project's default. +pub fn RegArray(comptime base: usize, comptime stride: usize, comptime count: usize) type { + return struct { + pub const Index = std.math.IntFittingRange(0, count - 1); + + /// comptime, because `IntFittingRange` rounds up to a whole width: for a 57-entry array the + /// index type is u6, which would happily accept 57..63. Every caller here passes a comptime + /// pin anyway, so this costs nothing and makes the bound real in all optimize modes. + pub inline fn at(comptime index: Index) *volatile u32 { + comptime std.debug.assert(index < count); + return @ptrFromInt(base + @as(usize, index) * stride); + } + }; +} + +const hp_periph1 = 0x500C0000; + +/// GPIO and the IO MUX now live in the HAL, which builds them out of ESP-IDF's own register macros +/// (`hal/gpio.zig`) instead of the hand-transcribed addresses that used to be here. The +/// transcription is exactly the kind of thing that goes quietly wrong: this file's matrix constant +/// said 256 with a comment warning that the S3's is 128, and the first hand-written replacement in +/// the HAL used 128 anyway. It now comes from `SIG_GPIO_OUT_IDX` in IDF's `gpio_sig_map.h`. +pub const gpio = @import("hal").gpio; + +/// Mask ROM routines. These are the only "library" a bare image links against: the addresses come +/// from `components/esp_rom/esp32p4/ld/esp32p4.rom.ld` and the linker script re-declares them. +pub const rom = struct { + pub extern fn ets_printf(fmt: [*:0]const u8, ...) c_int; + pub extern fn ets_delay_us(us: u32) void; + + pub inline fn print(comptime fmt: [*:0]const u8, args: anytype) void { + _ = @call(.auto, ets_printf, .{fmt} ++ args); + } +}; + +/// Busy-wait for a number of CPU cycles, using the cycle counter rather than the mask ROM. Useful +/// when an image must not depend on ROM entry points at all, and for delays shorter than the ROM's +/// microsecond granularity. +pub inline fn delayCycles(n: u64) void { + const start = cycles(); + while (cycles() - start < n) {} +} + +/// Cycle counter: CSR 0xC00/0xC80, i.e. `cycle`/`cycleh` - the unprivileged shadows of mcycle, and +/// what ESP-IDF itself reads on this part (`rv_utils.h`: `RV_READ_CSR(cycle)`, because +/// SOC_CPU_HAS_CSR_PC is not defined for the P4). +/// +/// Read high-low-high: two separate CSR reads can straddle a wrap of the low word, which would +/// otherwise report a value 2^32 too large roughly every 47 seconds at 90 MHz. +pub inline fn cycles() u64 { + while (true) { + var hi0: u32 = undefined; + var lo: u32 = undefined; + var hi1: u32 = undefined; + asm volatile ("csrr %[r], 0xC80" + : [r] "=r" (hi0), + ); + asm volatile ("csrr %[r], 0xC00" + : [r] "=r" (lo), + ); + asm volatile ("csrr %[r], 0xC80" + : [r] "=r" (hi1), + ); + if (hi0 == hi1) return (@as(u64, hi0) << 32) | lo; + } +} |
