1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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;
}
}
|