summaryrefslogtreecommitdiff
path: root/src/io/chip.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/io/chip.zig')
-rw-r--r--src/io/chip.zig100
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();
+}