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/hal/systimer.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/hal/systimer.zig')
| -rw-r--r-- | src/hal/systimer.zig | 151 |
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; + } +} |
