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