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