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/io/chip.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/io/chip.zig')
| -rw-r--r-- | src/io/chip.zig | 100 |
1 files changed, 100 insertions, 0 deletions
diff --git a/src/io/chip.zig b/src/io/chip.zig new file mode 100644 index 0000000..c5e4127 --- /dev/null +++ b/src/io/chip.zig @@ -0,0 +1,100 @@ +//! The ESP32-P4 half of the scheduler's machine seam: time, critical sections, idling, entropy and +//! a way to print when nothing else works. +//! +//! This is the only file in `src/io/` that touches the chip, and it is deliberately thin - eight +//! functions - because everything above it is portable and gets tested on the host through +//! `host.zig`, which implements the same eight. Nothing here re-derives a register address or a +//! clock: the timebase is `hal.systimer`, the critical section is `hal.intr`'s (which is +//! `clkrst.Guard` under another name, so a critical section written against either module is the +//! same one), and the console is the mask-ROM `ets_printf` that `soc.rom` already declares. + +const std = @import("std"); +const hal = @import("hal"); +const soc = @import("soc"); +const regs = @import("regs"); +const mmio = @import("mmio"); + +/// The one timebase on this board that does not move. SYSTIMER is XTAL/2.5 = 16 MHz, fixed +/// (`clk_tree_defs.h:196-198`, and `hal/systimer.zig:28-31`): it is not derived from the CPU clock, +/// which the bootloader left at a measured ~90 MHz and which nothing here reconfigures. Using the +/// cycle counter instead would make every timeout in the stack wrong by a factor of four the moment +/// somebody raises the PLL. +pub const ticks_hz: u64 = hal.systimer.hz; + +/// Last value the counter gave us, so `ticks` can be monotonic even when the read fails. +var last_ticks: u64 = 0; + +/// Bring the timebase up. Idempotent, and specifically does *not* reprogram the clock source or +/// divider - `hal.systimer.init` documents why: re-running that on a live counter makes the +/// timebase jump, which would corrupt every deadline already computed from it. +pub fn init() void { + hal.systimer.init(); + last_ticks = hal.systimer.read(.unit0) orelse 0; +} + +/// The 52-bit counter, through the update/valid handshake `hal.systimer.read` implements. +/// +/// A gated-off systimer never sets VALUE_VALID, and `hal.systimer.read` reports that as `null` +/// rather than hanging. Returning the previous value there is the only safe answer: returning zero +/// would send time backwards, and every deadline in the scheduler is an unsigned comparison against +/// it, so one backwards step would turn every pending sleep into "already expired". +pub fn ticks() u64 { + const t = hal.systimer.read(.unit0) orelse return last_ticks; + last_ticks = t; + return t; +} + +/// A critical section against interrupt handlers. `hal.intr.Guard` nests correctly - `release` only +/// sets mstatus.MIE if MIE was set on entry - so the scheduler can take one inside a handler. +pub const Guard = hal.intr.Guard; + +pub inline fn mask() Guard { + return hal.intr.mask(); +} + +/// Wait for something to change. Called with interrupts in whatever state the caller had them, +/// which is normally enabled, and free to return at any time: every caller re-checks its condition. +/// +/// This **spins** rather than issuing `wfi`, and that is a decision worth stating. `wfi` is what a +/// power-managed system would do, but it can only be woken by an interrupt, and on this board there +/// is no timer interrupt to wake it: `hal/systimer.zig` exposes the counters and none of the six +/// comparators, and nothing in `hal.intr` is wired to them. A `wfi` with a pending deadline and no +/// alarm configured is a hang, and a `wfi` with no deadline at all is a hang that the scheduler's +/// deadlock watchdog cannot even report, because the watchdog needs to keep running to fire. So +/// this spins on the counter, which costs power and finds every bug. +/// +/// The follow-up is small and worth doing when power matters: a SYSTIMER comparator (`TARGET0`, +/// `SYSTIMER_TARGET0_INT`) routed through `hal.intr` would let this be `wfi` with an exact wake. +pub fn idle(deadline: ?u64) void { + _ = deadline; + // One counter read is ~20 cycles of handshake, which is a fine spin quantum and re-reads the + // register the caller is about to compare against anyway. + _ = ticks(); +} + +/// Print, when the machinery that would normally print has failed. `ets_printf` is a mask ROM +/// address (`esp32p4.rom.ld:24`, re-declared by this project's linker script), so it allocates +/// nothing, takes no lock, and works before or after any of this project's code is functional. +pub inline fn print(comptime fmt: [*:0]const u8, args: anytype) void { + soc.rom.print(fmt, args); +} + +/// The hardware random number register: `WDEV_RND_REG`, which on a pre-v3 P4 die is +/// `LP_SYSTEM_REG_RNG_DATA_REG` (`components/soc/esp32p4/register/hw_ver1/soc/wdev_reg.h:16`) at +/// `DR_REG_LP_SYS_BASE + 0x1a4` = 0x501101a4. `esp_random` reads exactly this register and nothing +/// else (`components/esp_hw_support/hw_random.c:78,86`). +/// +/// How much entropy is behind it is a separate question, and the answer for this image is "not +/// established" - see `p4.zig`'s `random` for what is done about that. The register itself is real. +const rng_data = mmio.Reg.at(regs.LP_SYSTEM_REG_RNG_DATA_REG); + +pub inline fn entropyWord() u32 { + return rng_data.raw(); +} + +/// Anything else the machine can contribute to a seed. The cycle counter is not a second timebase - +/// it is the same instant measured with a different, unknown divisor - but its low bits carry the +/// jitter of however many bus stalls happened since reset, which is exactly what a seed wants. +pub inline fn noise() u64 { + return soc.cycles(); +} |
