summaryrefslogtreecommitdiff
path: root/src/hal/timg.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-25 12:40:53 -0300
committerGabriel Schneider <[email protected]>2026-08-25 12:46:51 -0300
commitf5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch)
tree2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/hal/timg.zig
downloadesp32p4-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/timg.zig')
-rw-r--r--src/hal/timg.zig513
1 files changed, 513 insertions, 0 deletions
diff --git a/src/hal/timg.zig b/src/hal/timg.zig
new file mode 100644
index 0000000..f669387
--- /dev/null
+++ b/src/hal/timg.zig
@@ -0,0 +1,513 @@
+//! The timer groups: TIMG0 and TIMG1, each two general-purpose 54-bit timers plus one MWDT.
+//!
+//! Three unrelated functions share one register block (timg_ll.h:7 says so in as many words):
+//! the general-purpose timers, the main watchdog, and RTC clock calibration. Only the first two are
+//! here; calibration belongs to the clock tree, and ETM and interrupts are deliberately absent.
+//!
+//! Four things about this block cost real care, all of them taken from ESP-IDF's LL rather than
+//! guessed at:
+//!
+//! **Reading the counter is a sequence, not a load** (timer_ll.h:248-269). The counter lives in a
+//! different clock domain from the register file, so its value only appears in `TxLO`/`TxHI` after a
+//! software capture:
+//!
+//! write TIMG_TxUPDATE = 1 -> ask for a capture
+//! poll until TIMG_Tx_UPDATE == 0 -> the hardware clears it when the pair is latched
+//! read TxHI, then TxLO -> 22 bits + 32 bits = the 54-bit count
+//!
+//! Note the polarity: unlike SYSTIMER, which sets a separate `VALUE_VALID` bit, this peripheral
+//! *clears the request bit* to acknowledge. Waiting for it to become 1 hangs forever; not waiting at
+//! all returns whatever the last capture left, which for a never-captured timer is 0 and therefore
+//! looks like a stopped timer rather than like a bug.
+//!
+//! **The watchdog registers are write-protected, and the key is the reset value** (mwdt_ll.h:231-244
+//! and timer_group_reg.h, TIMG_WDT_WKEY: "If the register contains a different value than its reset
+//! value, write protection is enabled", default 1356348065 = 0x50D83AA1). So "unlock" means writing
+//! the key back, and "lock" means writing anything else - IDF writes 0. A watchdog register write
+//! made while locked is silently dropped, which is the failure mode this file's API shape exists to
+//! prevent: every MWDT operation is a method on the `Watchdog` handle returned by `unlock`, and
+//! there is no way to reach one without holding it:
+//!
+//! const wdt = timg.unlock(.timg1);
+//! defer wdt.release();
+//! wdt.setStage(.stage0, 2_000_000, .reset_system);
+//!
+//! **Watchdog configuration is committed asynchronously.** Every write to WDTCONFIG0-5 has to be
+//! followed by `WDT_CONF_UPDATE_EN` (mwdt_ll.h:122, and again after every other config write), which
+//! is a write-to-trigger bit. The exception is `WDT_EN` itself: `mwdt_ll_enable`/`_disable`
+//! (mwdt_ll.h:61-77) do *not* pulse it, so neither does `setEnabled` - matching IDF exactly matters
+//! more here than consistency, because the differential harness compares the resulting word.
+//!
+//! **Do not resurrect a watchdog you are not feeding.** TIMG0 hosts MWDT0, which this image's
+//! bootloader has already disabled, and `TIMG_WDT_FLASHBOOT_MOD_EN` defaults to 1 and runs the
+//! watchdog *independently of* `WDT_EN` (mwdt_ll.h:186-188). Resetting a timer group therefore
+//! re-arms flash-boot protection and reboots the board a moment later with nothing on the console to
+//! explain it; `clkrst.resetPeripheral` clears the bit as part of the reset for exactly this reason
+//! (its `clears_flashboot` flag), which is why nothing in this file pulses a reset bit itself.
+
+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;
+
+/// TIMG_LL_INST_NUM (timg_ll.h:20).
+pub const group_count = 2;
+/// TIMG_LL_GPTIMERS_PER_INST (timg_ll.h:23). Two per group on the P4, unlike the C-series parts.
+pub const timers_per_group = 2;
+/// TIMER_LL_COUNTER_BIT_WIDTH (timer_ll.h:25). 32 bits in `TxLO` plus 22 in `TxHI`.
+pub const counter_bits = 54;
+
+pub const Group = enum(u1) { timg0 = 0, timg1 = 1 };
+pub const Timer = enum(u1) { t0 = 0, t1 = 1 };
+
+// -------------------------------------------------------------------------------- addressing
+//
+// The macros are indexed two different ways at once and neither is derivable from the other:
+// `TIMG_T0CONFIG_REG(i)` takes the *group*, while the *timer* is baked into the macro name
+// (`T0CONFIG` vs `T1CONFIG`). Rather than duplicate every accessor per timer, the timer index is
+// turned into a stride - but a stride assumed is a stride that eventually writes into the next
+// register, so both strides are checked at comptime against the macros for the other instance.
+
+const group_stride = mmio.addr(regs.TIMG_T0CONFIG_REG(1)) - mmio.addr(regs.TIMG_T0CONFIG_REG(0));
+const timer_stride = mmio.addr(regs.TIMG_T1CONFIG_REG(0)) - mmio.addr(regs.TIMG_T0CONFIG_REG(0));
+
+// Absolute addresses of group 0 / timer 0's registers. Every other (group, timer) is these plus a
+// multiple of the two strides.
+const a_config = mmio.addr(regs.TIMG_T0CONFIG_REG(0));
+const a_lo = mmio.addr(regs.TIMG_T0LO_REG(0));
+const a_hi = mmio.addr(regs.TIMG_T0HI_REG(0));
+const a_update = mmio.addr(regs.TIMG_T0UPDATE_REG(0));
+const a_alarm_lo = mmio.addr(regs.TIMG_T0ALARMLO_REG(0));
+const a_alarm_hi = mmio.addr(regs.TIMG_T0ALARMHI_REG(0));
+const a_load_lo = mmio.addr(regs.TIMG_T0LOADLO_REG(0));
+const a_load_hi = mmio.addr(regs.TIMG_T0LOADHI_REG(0));
+const a_load = mmio.addr(regs.TIMG_T0LOAD_REG(0));
+
+comptime {
+ // The timer sub-block is contiguous and uniform - assert it, per register, rather than trust
+ // that 0x24 happens to be right for all nine.
+ const pairs = .{
+ .{ a_config, mmio.addr(regs.TIMG_T1CONFIG_REG(0)) },
+ .{ a_lo, mmio.addr(regs.TIMG_T1LO_REG(0)) },
+ .{ a_hi, mmio.addr(regs.TIMG_T1HI_REG(0)) },
+ .{ a_update, mmio.addr(regs.TIMG_T1UPDATE_REG(0)) },
+ .{ a_alarm_lo, mmio.addr(regs.TIMG_T1ALARMLO_REG(0)) },
+ .{ a_alarm_hi, mmio.addr(regs.TIMG_T1ALARMHI_REG(0)) },
+ .{ a_load_lo, mmio.addr(regs.TIMG_T1LOADLO_REG(0)) },
+ .{ a_load_hi, mmio.addr(regs.TIMG_T1LOADHI_REG(0)) },
+ .{ a_load, mmio.addr(regs.TIMG_T1LOAD_REG(0)) },
+ };
+ for (pairs) |p| {
+ if (p[1] - p[0] != timer_stride) @compileError(
+ "the two timers' registers are not a uniform stride apart; index them per timer",
+ );
+ }
+ // And the group stride is the same for a register other than CONFIG.
+ if (mmio.addr(regs.TIMG_T0LO_REG(1)) - a_lo != group_stride)
+ @compileError("the two timer groups are not a uniform stride apart");
+
+ // T0's and T1's *fields* sit at the same bit positions in their respective registers, which is
+ // what makes one set of Field constants enough. If a future register set moves one of them,
+ // this stops the build instead of writing the divider into the alarm enable.
+ const t1_divider = Field.of(regs.TIMG_T1_DIVIDER_S, regs.TIMG_T1_DIVIDER_V);
+ const t1_en = Field.of(regs.TIMG_T1_EN_S, regs.TIMG_T1_EN_V);
+ const t1_update = Field.of(regs.TIMG_T1_UPDATE_S, regs.TIMG_T1_UPDATE_V);
+ const t1_hi = Field.of(regs.TIMG_T1_HI_S, regs.TIMG_T1_HI_V);
+ if (t1_divider.shift != divider.shift or t1_divider.width != divider.width or
+ t1_en.shift != counter_en.shift or t1_update.shift != update.shift or
+ t1_hi.width != count_hi.width)
+ @compileError("timer 0 and timer 1 disagree on field positions; look up fields per timer");
+}
+
+inline fn tReg(comptime a0: u32, g: Group, t: Timer) Reg {
+ return Reg.atAddress(a0 +
+ group_stride * @as(u32, @intFromEnum(g)) +
+ timer_stride * @as(u32, @intFromEnum(t)));
+}
+
+/// `a0` is not comptime: the stage-timeout registers are picked by a runtime `Stage`
+/// (`stageHoldAddr`), and every other caller passes a constant that folds anyway.
+inline fn gReg(a0: u32, g: Group) Reg {
+ return Reg.atAddress(a0 + group_stride * @as(u32, @intFromEnum(g)));
+}
+
+// TxCONFIG fields. `divcnt_rst` is write-to-trigger; the rest are plain R/W.
+const alarm_en = Field.of(regs.TIMG_T0_ALARM_EN_S, regs.TIMG_T0_ALARM_EN_V);
+const divcnt_rst = Field.of(regs.TIMG_T0_DIVCNT_RST_S, regs.TIMG_T0_DIVCNT_RST_V);
+const divider = Field.of(regs.TIMG_T0_DIVIDER_S, regs.TIMG_T0_DIVIDER_V);
+const autoreload = Field.of(regs.TIMG_T0_AUTORELOAD_S, regs.TIMG_T0_AUTORELOAD_V);
+const increase = Field.of(regs.TIMG_T0_INCREASE_S, regs.TIMG_T0_INCREASE_V);
+const counter_en = Field.of(regs.TIMG_T0_EN_S, regs.TIMG_T0_EN_V);
+const update = Field.of(regs.TIMG_T0_UPDATE_S, regs.TIMG_T0_UPDATE_V);
+const count_hi = Field.of(regs.TIMG_T0_HI_S, regs.TIMG_T0_HI_V);
+const alarm_value_hi = Field.of(regs.TIMG_T0_ALARM_HI_S, regs.TIMG_T0_ALARM_HI_V);
+const load_value_hi = Field.of(regs.TIMG_T0_LOAD_HI_S, regs.TIMG_T0_LOAD_HI_V);
+
+// ------------------------------------------------------------------------------ timer clocks
+//
+// The timers' function clock is selected and gated in HP_SYS_CLKRST, not in the timer group: group 0
+// in PERI_CLK_CTRL20 and group 1 in PERI_CLK_CTRL21 (timer_ll.h:117-129, :146-160). Two shared
+// registers, so both operations take the interrupt guard - the same read-modify-write hazard
+// `clkrst` exists for.
+
+const peri_clk_ctrl20 = Reg.at(regs.HP_SYS_CLKRST_PERI_CLK_CTRL20_REG);
+const peri_clk_ctrl21 = Reg.at(regs.HP_SYS_CLKRST_PERI_CLK_CTRL21_REG);
+
+/// The three function clocks a GP timer can run from, with the encodings from
+/// `timer_ll_set_clock_source` (timer_ll.h:100-116). The numbering is not the enum order anyone
+/// would pick: XTAL is 0, RC_FAST is 1, PLL_F80M is 2.
+pub const ClockSource = enum(u2) {
+ xtal = 0,
+ rc_fast = 1,
+ pll_f80m = 2,
+};
+
+/// Where the group/timer's source-select and gate fields live. Both are in one word per group, and
+/// the bit positions differ per timer, so this is a genuine per-instance lookup rather than a stride.
+const TimerClock = struct {
+ reg: Reg,
+ src_sel: Field,
+ clk_en: Field,
+};
+
+inline fn timerClock(comptime g: Group, comptime t: Timer) TimerClock {
+ return switch (g) {
+ .timg0 => switch (t) {
+ .t0 => .{
+ .reg = peri_clk_ctrl20,
+ .src_sel = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP0_T0_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_TIMERGRP0_T0_SRC_SEL_V),
+ .clk_en = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP0_T0_CLK_EN_S, regs.HP_SYS_CLKRST_REG_TIMERGRP0_T0_CLK_EN_V),
+ },
+ .t1 => .{
+ .reg = peri_clk_ctrl20,
+ .src_sel = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP0_T1_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_TIMERGRP0_T1_SRC_SEL_V),
+ .clk_en = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP0_T1_CLK_EN_S, regs.HP_SYS_CLKRST_REG_TIMERGRP0_T1_CLK_EN_V),
+ },
+ },
+ .timg1 => switch (t) {
+ .t0 => .{
+ .reg = peri_clk_ctrl21,
+ .src_sel = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP1_T0_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_TIMERGRP1_T0_SRC_SEL_V),
+ .clk_en = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP1_T0_CLK_EN_S, regs.HP_SYS_CLKRST_REG_TIMERGRP1_T0_CLK_EN_V),
+ },
+ .t1 => .{
+ .reg = peri_clk_ctrl21,
+ .src_sel = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP1_T1_SRC_SEL_S, regs.HP_SYS_CLKRST_REG_TIMERGRP1_T1_SRC_SEL_V),
+ .clk_en = Field.of(regs.HP_SYS_CLKRST_REG_TIMERGRP1_T1_CLK_EN_S, regs.HP_SYS_CLKRST_REG_TIMERGRP1_T1_CLK_EN_V),
+ },
+ },
+ };
+}
+
+/// Select a timer's function clock. Comptime instance because the field pairing really does differ
+/// per (group, timer) - four different bit positions in two registers.
+pub fn setClockSource(comptime g: Group, comptime t: Timer, src: ClockSource) void {
+ const c = comptime timerClock(g, t);
+ const guard = clkrst.maskInterrupts();
+ defer guard.release();
+ c.reg.modify(.{c.src_sel.is(@intFromEnum(src))});
+}
+
+/// The timer's function-clock gate, distinct from the group's bus clock in `clkrst`. Defaults to 1
+/// at power-on (hp_sys_clkrst_reg.h: REG_TIMERGRP0_T0_CLK_EN default 1).
+pub fn setClockEnabled(comptime g: Group, comptime t: Timer, on: bool) void {
+ const c = comptime timerClock(g, t);
+ const guard = clkrst.maskInterrupts();
+ defer guard.release();
+ c.reg.modify(.{c.clk_en.is(@intFromBool(on))});
+}
+
+// ------------------------------------------------------------------------ general purpose timer
+
+pub const Direction = enum { up, down };
+
+/// Prescaler on the function clock. 2 is the smallest the hardware accepts and 65536 the largest,
+/// encoded as 0 (timer_ll.h:191-199). The divider counter is reset in a second store afterwards,
+/// exactly as IDF does it: without that the new divider only takes effect after the old one's
+/// current period ends, so the first tick after a change is the wrong length.
+pub fn setDivider(g: Group, t: Timer, div: u32) void {
+ std.debug.assert(div >= 2 and div <= 65536);
+ const cfg = tReg(a_config, g, t);
+ cfg.modify(.{divider.is(if (div >= 65536) 0 else div)});
+ cfg.modify(.{divcnt_rst.is(1)});
+}
+
+pub fn setDirection(g: Group, t: Timer, dir: Direction) void {
+ tReg(a_config, g, t).modify(.{increase.is(@intFromBool(dir == .up))});
+}
+
+/// Reload the counter from `TxLOADLO`/`TxLOADHI` automatically on every alarm.
+pub fn setAutoReload(g: Group, t: Timer, on: bool) void {
+ tReg(a_config, g, t).modify(.{autoreload.is(@intFromBool(on))});
+}
+
+pub fn setCounterEnabled(g: Group, t: Timer, on: bool) void {
+ tReg(a_config, g, t).modify(.{counter_en.is(@intFromBool(on))});
+}
+
+pub fn setAlarmEnabled(g: Group, t: Timer, on: bool) void {
+ tReg(a_config, g, t).modify(.{alarm_en.is(@intFromBool(on))});
+}
+
+/// The 54-bit alarm value. Low word first would be equally correct - the comparator only sees the
+/// pair - but IDF writes high then low (timer_ll.h:279-283) and matching its order keeps the write
+/// trace comparable.
+pub fn setAlarmValue(g: Group, t: Timer, value: u64) void {
+ tReg(a_alarm_hi, g, t).modify(.{alarm_value_hi.is(@truncate(value >> 32))});
+ tReg(a_alarm_lo, g, t).writeRaw(@truncate(value));
+}
+
+/// The value a reload puts into the counter, whether triggered by `load` or by an auto-reload.
+pub fn setLoadValue(g: Group, t: Timer, value: u64) void {
+ tReg(a_load_hi, g, t).modify(.{load_value_hi.is(@truncate(value >> 32))});
+ tReg(a_load_lo, g, t).writeRaw(@truncate(value));
+}
+
+pub fn getLoadValue(g: Group, t: Timer) u64 {
+ const hi: u64 = tReg(a_load_hi, g, t).get(load_value_hi);
+ return (hi << 32) | tReg(a_load_lo, g, t).raw();
+}
+
+/// Copy the load value into the counter now. `TIMG_TxLOAD_REG` is a whole-word write-to-trigger
+/// register: the value written is irrelevant, so this is a bare store rather than a field write.
+pub fn load(g: Group, t: Timer) void {
+ tReg(a_load, g, t).writeRaw(1);
+}
+
+/// The counter, through the capture handshake described at the top of this file.
+///
+/// Returns null rather than spinning forever if the peripheral never acknowledges: with the group's
+/// bus clock gated off, or the timer's function clock gated off, `UPDATE` never clears, and hanging
+/// inside a HAL call with no output is the worst possible way to report that. The bound is the same
+/// 10,000 reads `systimer.read` uses.
+pub fn read(g: Group, t: Timer) ?u64 {
+ const upd = tReg(a_update, g, t);
+
+ // Ask for a capture. IDF assigns to the struct bitfield, which is a read-modify-write of a word
+ // whose only other bits are reserved, so `modify` is both the honest operation and the one that
+ // produces the same store.
+ upd.modify(.{update.is(1)});
+
+ var spins: u32 = 0;
+ while (upd.get(update) != 0) {
+ spins += 1;
+ if (spins > 10_000) return null;
+ }
+
+ const hi: u64 = tReg(a_hi, g, t).get(count_hi);
+ return (hi << 32) | tReg(a_lo, g, t).raw();
+}
+
+// ----------------------------------------------------------------------------------- watchdog
+
+const a_wdtconfig0 = mmio.addr(regs.TIMG_WDTCONFIG0_REG(0));
+const a_wdtconfig1 = mmio.addr(regs.TIMG_WDTCONFIG1_REG(0));
+const a_wdtconfig2 = mmio.addr(regs.TIMG_WDTCONFIG2_REG(0));
+const a_wdtconfig3 = mmio.addr(regs.TIMG_WDTCONFIG3_REG(0));
+const a_wdtconfig4 = mmio.addr(regs.TIMG_WDTCONFIG4_REG(0));
+const a_wdtconfig5 = mmio.addr(regs.TIMG_WDTCONFIG5_REG(0));
+const a_wdtfeed = mmio.addr(regs.TIMG_WDTFEED_REG(0));
+const a_wdtwprotect = mmio.addr(regs.TIMG_WDTWPROTECT_REG(0));
+
+const wdt_en = Field.of(regs.TIMG_WDT_EN_S, regs.TIMG_WDT_EN_V);
+const wdt_conf_update_en = Field.of(regs.TIMG_WDT_CONF_UPDATE_EN_S, regs.TIMG_WDT_CONF_UPDATE_EN_V);
+const wdt_flashboot_mod_en = Field.of(regs.TIMG_WDT_FLASHBOOT_MOD_EN_S, regs.TIMG_WDT_FLASHBOOT_MOD_EN_V);
+const wdt_cpu_reset_length = Field.of(regs.TIMG_WDT_CPU_RESET_LENGTH_S, regs.TIMG_WDT_CPU_RESET_LENGTH_V);
+const wdt_sys_reset_length = Field.of(regs.TIMG_WDT_SYS_RESET_LENGTH_S, regs.TIMG_WDT_SYS_RESET_LENGTH_V);
+const wdt_clk_prescale = Field.of(regs.TIMG_WDT_CLK_PRESCALE_S, regs.TIMG_WDT_CLK_PRESCALE_V);
+const wdt_divcnt_rst = Field.of(regs.TIMG_WDT_DIVCNT_RST_S, regs.TIMG_WDT_DIVCNT_RST_V);
+
+/// The write-protect key, and also `TIMG_WDT_WKEY`'s reset value: protection is on whenever the
+/// register holds anything *else* (timer_group_reg.h, TIMG_WDT_WKEY, default 1356348065). IDF's
+/// `mwdt_ll_write_protect_disable` writes this exact constant (mwdt_ll.h:243).
+pub const wkey: u32 = 0x50D8_3AA1;
+
+/// What IDF writes to re-enable protection (mwdt_ll.h:233). Any non-key value would do; using the
+/// same one keeps the register comparable against IDF's.
+const wkey_locked: u32 = 0;
+
+// The headers carry no reset-value macro to check `wkey` against - `TIMG_WDT_WKEY_V` is the field
+// mask, 0xffffffff - so the constant is copied from the two places that state it: the register
+// description's "default: 1356348065" and mwdt_ll.h:243's 0x50D83AA1. The unit test at the end of
+// this file pins those two against each other, which is the only check available without a chip.
+
+/// MWDT stages, each with its own timeout and its own action. Stage 0 fires first; a stage that is
+/// not fed escalates to the next.
+pub const Stage = enum(u2) { stage0 = 0, stage1 = 1, stage2 = 2, stage3 = 3 };
+
+/// What a stage does when it expires (mwdt_ll.h:23-26).
+pub const Action = enum(u2) {
+ off = 0,
+ interrupt = 1,
+ reset_cpu = 2,
+ reset_system = 3,
+};
+
+/// Length of the reset pulse a `reset_cpu`/`reset_system` stage asserts (mwdt_ll.h:28-35).
+pub const ResetLength = enum(u3) {
+ ns_100 = 0,
+ ns_200 = 1,
+ ns_300 = 2,
+ ns_400 = 3,
+ ns_500 = 4,
+ ns_800 = 5,
+ us_1_6 = 6,
+ us_3_2 = 7,
+};
+
+/// A group's MWDT with write protection lifted, and the only way to reach an MWDT operation:
+///
+/// const wdt = timg.unlock(.timg1);
+/// defer wdt.release();
+/// wdt.setStage(.stage0, ticks, .reset_system);
+///
+/// The handle exists because a watchdog register write made while protection is on is silently
+/// dropped - no fault, no status bit, just a watchdog that keeps its old timeout - and that is not a
+/// mistake worth making twice.
+pub const Watchdog = struct {
+ group: Group,
+
+ /// Re-enable write protection. Not idempotent-with-`unlock` in the composable sense that
+ /// `clkrst.Guard` is: the hardware has one key register and no nesting count, so an inner
+ /// `release` really does lock an outer caller out. There is nothing in this HAL that nests.
+ pub inline fn release(self: Watchdog) void {
+ gReg(a_wdtwprotect, self.group).writeRaw(wkey_locked);
+ }
+
+ /// WDTCONFIG0-5 are shadowed; the hardware only takes them at a `CONF_UPDATE_EN` pulse
+ /// (mwdt_ll.h:121-122). Write-to-trigger, so this is a single deliberate store.
+ inline fn commit(self: Watchdog) void {
+ gReg(a_wdtconfig0, self.group).modify(.{wdt_conf_update_en.is(1)});
+ }
+
+ inline fn config0(self: Watchdog) Reg {
+ return gReg(a_wdtconfig0, self.group);
+ }
+
+ /// The stage's action bits and its timeout live in different registers - the action in
+ /// WDTCONFIG0, the timeout in WDTCONFIG2+stage - which is why this takes both at once
+ /// (mwdt_ll.h:98-123). `timeout` is in MWDT clock cycles, i.e. after the prescaler.
+ pub fn setStage(self: Watchdog, stage: Stage, timeout: u32, action: Action) void {
+ self.config0().modify(.{stageAction(stage).is(@intFromEnum(action))});
+ gReg(stageHoldAddr(stage), self.group).writeRaw(timeout);
+ self.commit();
+ }
+
+ /// Turn one stage off without disturbing its timeout (mwdt_ll.h:131-152).
+ pub fn disableStage(self: Watchdog, stage: Stage) void {
+ self.config0().modify(.{stageAction(stage).is(@intFromEnum(Action.off))});
+ self.commit();
+ }
+
+ pub fn getStageTimeout(self: Watchdog, stage: Stage) u32 {
+ return gReg(stageHoldAddr(stage), self.group).raw();
+ }
+
+ /// Prescaler from the MWDT's source clock (XTAL on this chip - mwdt_ll.h:273-283 asserts it and
+ /// selects nothing). 1 to 65535; IDF's default is 20000, which gives 500 ticks/us
+ /// (mwdt_ll.h:20).
+ pub fn setPrescaler(self: Watchdog, prescaler: u32) void {
+ std.debug.assert(prescaler >= 1 and prescaler <= 0xffff);
+ gReg(a_wdtconfig1, self.group).modify(.{wdt_clk_prescale.is(prescaler)});
+ self.commit();
+ }
+
+ pub fn setCpuResetLength(self: Watchdog, len: ResetLength) void {
+ self.config0().modify(.{wdt_cpu_reset_length.is(@intFromEnum(len))});
+ self.commit();
+ }
+
+ pub fn setSysResetLength(self: Watchdog, len: ResetLength) void {
+ self.config0().modify(.{wdt_sys_reset_length.is(@intFromEnum(len))});
+ self.commit();
+ }
+
+ /// Flash-boot protection: a second, independent way for this watchdog to run. It ignores
+ /// `WDT_EN` entirely (mwdt_ll.h:186-188), it defaults to 1, and a group reset re-arms it - so
+ /// clearing it is part of every sane bring-up, and `clkrst.resetPeripheral` does it.
+ pub fn setFlashbootEnabled(self: Watchdog, on: bool) void {
+ self.config0().modify(.{wdt_flashboot_mod_en.is(@intFromBool(on))});
+ self.commit();
+ }
+
+ /// Start or stop the watchdog. No `CONF_UPDATE_EN` pulse: `mwdt_ll_enable` and `_disable`
+ /// (mwdt_ll.h:61-77) do not, so neither does this. Disabling does *not* stop flash-boot mode.
+ pub fn setEnabled(self: Watchdog, on: bool) void {
+ self.config0().modify(.{wdt_en.is(@intFromBool(on))});
+ }
+
+ pub fn isEnabled(self: Watchdog) bool {
+ return self.config0().get(wdt_en) == 1;
+ }
+
+ /// Reset the count and the stage. `TIMG_WDTFEED_REG` is a whole-word write-to-trigger register,
+ /// so the value is irrelevant (mwdt_ll.h:219-222).
+ pub fn feed(self: Watchdog) void {
+ gReg(a_wdtfeed, self.group).writeRaw(1);
+ }
+
+ /// Reset the watchdog's clock divider counter. Write-to-trigger, in WDTCONFIG1 alongside the
+ /// prescaler.
+ pub fn resetDividerCount(self: Watchdog) void {
+ gReg(a_wdtconfig1, self.group).modify(.{wdt_divcnt_rst.is(1)});
+ self.commit();
+ }
+};
+
+/// Lift write protection and hand back the only handle that can touch the MWDT.
+pub fn unlock(g: Group) Watchdog {
+ gReg(a_wdtwprotect, g).writeRaw(wkey);
+ return .{ .group = g };
+}
+
+/// Feed a watchdog, protection dance included. The one MWDT operation that is worth a shortcut,
+/// because it is the one called from a loop.
+pub fn feed(g: Group) void {
+ const wdt = unlock(g);
+ defer wdt.release();
+ wdt.feed();
+}
+
+/// True if write protection is currently on, i.e. the key register holds something other than the
+/// key. Reads the register, so it reports the hardware rather than what this module last wrote.
+pub fn isWriteProtected(g: Group) bool {
+ return gReg(a_wdtwprotect, g).raw() != wkey;
+}
+
+inline fn stageAction(stage: Stage) Field {
+ // Stage 0 is at the *top* of the word (bits 30:29) and stage 3 at 24:23, i.e. the stages run
+ // downwards through the register. The four are a uniform 2 bits apart, but in the reverse of
+ // the obvious direction, so they are looked up rather than computed.
+ return switch (stage) {
+ .stage0 => Field.of(regs.TIMG_WDT_STG0_S, regs.TIMG_WDT_STG0_V),
+ .stage1 => Field.of(regs.TIMG_WDT_STG1_S, regs.TIMG_WDT_STG1_V),
+ .stage2 => Field.of(regs.TIMG_WDT_STG2_S, regs.TIMG_WDT_STG2_V),
+ .stage3 => Field.of(regs.TIMG_WDT_STG3_S, regs.TIMG_WDT_STG3_V),
+ };
+}
+
+inline fn stageHoldAddr(stage: Stage) u32 {
+ // WDTCONFIG2 holds stage 0's timeout and WDTCONFIG5 stage 3's; the mapping is off by two and
+ // there is no macro that says so, so it comes from mwdt_ll.h:100-116.
+ return switch (stage) {
+ .stage0 => a_wdtconfig2,
+ .stage1 => a_wdtconfig3,
+ .stage2 => a_wdtconfig4,
+ .stage3 => a_wdtconfig5,
+ };
+}
+
+test "the two strides are the documented ones" {
+ // 0x1000 between groups (timer_group_reg.h:14, REG_TIMG_BASE) and 0x24 between the two timers
+ // of a group. Both are asserted against the macros at comptime above; this pins the numbers so
+ // a header change shows up as a failing test with a value in it, not only as a compile error.
+ try std.testing.expectEqual(@as(u32, 0x1000), group_stride);
+ try std.testing.expectEqual(@as(u32, 0x24), timer_stride);
+}
+
+test "the write-protect key is the register's reset value" {
+ try std.testing.expectEqual(@as(u32, 1_356_348_065), wkey);
+}