summaryrefslogtreecommitdiff
path: root/src/hal/systimer.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/hal/systimer.zig')
-rw-r--r--src/hal/systimer.zig151
1 files changed, 151 insertions, 0 deletions
diff --git a/src/hal/systimer.zig b/src/hal/systimer.zig
new file mode 100644
index 0000000..dc316bf
--- /dev/null
+++ b/src/hal/systimer.zig
@@ -0,0 +1,151 @@
+//! SYSTIMER: two 52-bit counters on a fixed clock, plus three comparators each.
+//!
+//! This is the most useful peripheral on the chip for bring-up work and the cheapest to trust. Its
+//! source is fixed - XTAL at 40 MHz, divided to 16 MHz (`clk_tree_defs.h:196-198`) - so unlike the
+//! CPU cycle counter its rate does not move when the clock tree is reconfigured, and unlike the
+//! timer groups it needs no divider arithmetic and no pads.
+//!
+//! Reading it is a **sequence**, not a load, and that is the interesting part:
+//!
+//! write UNIT0_UPDATE = 1 -> ask the peripheral to latch its counter
+//! poll UNIT0_VALUE_VALID -> wait for the latch
+//! read VALUE_HI, then VALUE_LO -> read the latched pair
+//!
+//! Skip the handshake and you read a value that is being incremented underneath you: the low word
+//! can wrap between the two loads, so `hi` belongs to one instant and `lo` to the next, and the
+//! result jumps backwards by 2^32 ticks about once every 268 seconds at 16 MHz. A register
+//! snapshot taken after either version looks identical - which is exactly why the differential
+//! harness records the *write trace* as well as the final state.
+
+const std = @import("std");
+const regs = @import("regs");
+const mmio = @import("mmio");
+const clkrst = @import("clkrst.zig");
+
+const Reg = mmio.Reg;
+const Field = mmio.Field;
+
+/// Ticks per second. XTAL/2.5 = 16 MHz, fixed: `SYSTIMER_CLK_SRC_XTAL` with the divider ESP-IDF
+/// programs in `systimer_hal_init`. Not derived from the CPU clock, which on this board is whatever
+/// the bootloader left (measured ~90 MHz, not the 360 the part is rated for).
+pub const hz: u32 = 16_000_000;
+
+const conf = Reg.at(regs.SYSTIMER_CONF_REG);
+const clk_en = Field.of(regs.SYSTIMER_CLK_EN_S, regs.SYSTIMER_CLK_EN_V);
+
+/// The two counter units. `unit_op` holds the update/valid handshake bits, `value_hi`/`value_lo` the
+/// latched result. Strides are derived from consecutive macros, not assumed.
+const unit_op = mmio.RegArray(regs.SYSTIMER_UNIT0_OP_REG, regs.SYSTIMER_UNIT1_OP_REG, 2);
+const unit_value_hi = mmio.RegArray(regs.SYSTIMER_UNIT0_VALUE_HI_REG, regs.SYSTIMER_UNIT1_VALUE_HI_REG, 2);
+const unit_value_lo = mmio.RegArray(regs.SYSTIMER_UNIT0_VALUE_LO_REG, regs.SYSTIMER_UNIT1_VALUE_LO_REG, 2);
+
+// The per-unit fields split into two groups, and the split is not obvious from the names.
+//
+// `update` and `valid` live in a *per-unit* register (UNIT0_OP_REG, UNIT1_OP_REG) and therefore sit
+// at the same bit in each - asserted below, so indexing the register is enough.
+//
+// `work_en` is different: both units' enables live in the *shared* SYSTIMER_CONF_REG, at bits 30 and
+// 29 respectively. A first draft of this file used unit 0's field for both, which would have enabled
+// the wrong counter and left the requested one dead; the comptime assert caught it before it ever
+// reached the chip. Hence a per-unit lookup rather than one constant.
+const update = Field.of(regs.SYSTIMER_TIMER_UNIT0_UPDATE_S, regs.SYSTIMER_TIMER_UNIT0_UPDATE_V);
+const valid = Field.of(regs.SYSTIMER_TIMER_UNIT0_VALUE_VALID_S, regs.SYSTIMER_TIMER_UNIT0_VALUE_VALID_V);
+const value_hi = Field.of(regs.SYSTIMER_TIMER_UNIT0_VALUE_HI_S, regs.SYSTIMER_TIMER_UNIT0_VALUE_HI_V);
+
+comptime {
+ const update1 = Field.of(regs.SYSTIMER_TIMER_UNIT1_UPDATE_S, regs.SYSTIMER_TIMER_UNIT1_UPDATE_V);
+ const valid1 = Field.of(regs.SYSTIMER_TIMER_UNIT1_VALUE_VALID_S, regs.SYSTIMER_TIMER_UNIT1_VALUE_VALID_V);
+ if (update1.shift != update.shift or valid1.shift != valid.shift)
+ @compileError("the systimer units' OP registers disagree on bit positions; index per unit");
+ // The other half of the same story: these two MUST differ, because they share a register.
+ if (workEn(.unit0).shift == workEn(.unit1).shift)
+ @compileError("both work_en fields claim the same bit of SYSTIMER_CONF; one macro is wrong");
+}
+
+inline fn workEn(comptime unit: Unit) Field {
+ return switch (unit) {
+ .unit0 => Field.of(regs.SYSTIMER_TIMER_UNIT0_WORK_EN_S, regs.SYSTIMER_TIMER_UNIT0_WORK_EN_V),
+ .unit1 => Field.of(regs.SYSTIMER_TIMER_UNIT1_WORK_EN_S, regs.SYSTIMER_TIMER_UNIT1_WORK_EN_V),
+ };
+}
+
+pub const Unit = enum(u1) { unit0 = 0, unit1 = 1 };
+
+/// The counter's own clock gate, inside the peripheral and separate from the bus clock gate in
+/// HP_SYS_CLKRST.
+pub fn setEnabled(on: bool) void {
+ conf.modify(.{clk_en.is(@intFromBool(on))});
+}
+
+pub fn setUnitEnabled(comptime unit: Unit, on: bool) void {
+ conf.modify(.{workEn(unit).is(@intFromBool(on))});
+}
+
+/// Bring the peripheral up: bus clock and reset through CLKRST, then its internal gate and unit.
+///
+/// Deliberately does *not* reprogram the clock source or divider. The bootloader has already set
+/// those, ESP-IDF's own `systimer_hal_init` would set them the same way, and re-running that on a
+/// live counter makes the timebase jump - which would corrupt any measurement taken across the call.
+pub fn init() void {
+ clkrst.setClockEnabled(.systimer, true);
+ setEnabled(true);
+ setUnitEnabled(.unit0, true);
+}
+
+/// The 52-bit counter, latched through the update/valid handshake.
+///
+/// Returns null if the peripheral does not acknowledge within `spins` reads, rather than spinning
+/// forever: a systimer whose clock is gated off never sets `valid`, and hanging in a HAL call with
+/// no output is the worst possible way to report that.
+pub fn read(unit: Unit) ?u64 {
+ const i: u32 = @intFromEnum(unit);
+ const op = unit_op.at(i);
+
+ // Ask for a snapshot, and clear the previous handshake in the same store.
+ //
+ // This has to be a read-modify-write, and a whole-word `write` is a bug. UPDATE is bit 30 and
+ // `WT`, so writing it as a single store looks right - but VALUE_VALID is bit 29 of the same word
+ // and is `R/SS/WTC`, write-1-to-clear (systimer_reg.h). A whole-word store writes 0 there, which
+ // is the no-op for a W1C bit, so the valid flag from the *previous* snapshot is never cleared:
+ // after one successful read it stays set forever, the poll below exits immediately on a stale
+ // flag, and the HI/LO pair that follows can straddle two different snapshots - precisely the
+ // tearing this handshake exists to prevent.
+ //
+ // ESP-IDF gets this right by accident of its idiom: `systimer_ll_counter_snapshot` assigns a
+ // bitfield of a `volatile` union, which compiles to a 32-bit read-modify-write that writes bit
+ // 29 back as 1 whenever it read 1, clearing it and re-arming in one store. This does the same
+ // thing deliberately.
+ //
+ // The register differential cannot see this: once any snapshot has completed, UNIT0_OP reads
+ // 0x2000_0000 under either version.
+ op.writeRaw(op.raw() | update.mask());
+
+ var spins: u32 = 0;
+ while (op.get(valid) == 0) {
+ spins += 1;
+ if (spins > 10_000) return null;
+ }
+
+ // Order matters less than the latch does - both words are frozen now - but read high first to
+ // match ESP-IDF's LL, so the write/read trace lines up under differential test.
+ const hi: u64 = unit_value_hi.at(i).get(value_hi);
+ const lo: u64 = unit_value_lo.at(i).raw();
+ return (hi << 32) | lo;
+}
+
+/// Microseconds since the counter started, from the 16 MHz tick.
+pub fn micros(unit: Unit) ?u64 {
+ const ticks = read(unit) orelse return null;
+ return ticks / (hz / 1_000_000);
+}
+
+/// Busy-wait. Uses the counter rather than the CPU cycle count, so the delay is right regardless of
+/// what the CPU clock happens to be.
+pub fn delayMicros(us: u32) void {
+ const start = read(.unit0) orelse return;
+ const target = start + @as(u64, us) * (hz / 1_000_000);
+ while (true) {
+ const now = read(.unit0) orelse return;
+ if (now >= target) return;
+ }
+}