//! 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(); }