summaryrefslogtreecommitdiff
path: root/src/hal/rwdt.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/rwdt.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/rwdt.zig')
-rw-r--r--src/hal/rwdt.zig123
1 files changed, 123 insertions, 0 deletions
diff --git a/src/hal/rwdt.zig b/src/hal/rwdt.zig
new file mode 100644
index 0000000..5002400
--- /dev/null
+++ b/src/hal/rwdt.zig
@@ -0,0 +1,123 @@
+//! The RTC watchdog (RWDT) and the super watchdog (SWD), in the always-on LP domain.
+//!
+//! This module exists because of a bug that had been in every application in this project since the
+//! first one, and was invisible for a simple reason: nothing had ever run for more than eight
+//! seconds.
+//!
+//! The second-stage bootloader arms the RTC watchdog to cover the handover to the application, and
+//! expects the application to take it over - ESP-IDF disables it in `esp_system`'s startup, which a
+//! bare image never runs. So the board resets, and the console says so if anyone looks:
+//!
+//! MARK ZIG_P4_ALIVE beat=8 gpio20 high=1 low=0
+//! rst:0x10 (CHIP_LP_WDT_RESET),boot:0x30f (SPI_FAST_FLASH_BOOT)
+//!
+//! Every demo, every example and every hardware test in this repo had been silently rebooting on a
+//! roughly ten-second cycle. It surfaced only when the differential harness grew past 26 cases and
+//! the run stopped fitting inside one watchdog period - which first looked like "the UART suite
+//! crashes the board", and was not.
+//!
+//! Two watchdogs live here and both have to be dealt with:
+//!
+//! * **RWDT**, the RTC watchdog proper, in `LP_WDT_CONFIG0_REG`. Write-protected.
+//! * **SWD**, the super watchdog, a separate always-on timer whose job is to catch a system that
+//! has stopped feeding everything else. It has its own key and its own register, and the
+//! bootloader leaves it auto-feeding (`bootloader_super_wdt_auto_feed`); an application that
+//! disables RWDT and forgets SWD gets a longer fuse rather than no fuse.
+//!
+//! The write-protect scheme is the same as the timer groups': the key register's *reset value* is
+//! the unlock key, so writing anything else locks it. Writes to a locked register are dropped
+//! silently - no fault, no status bit - which is why `disable()` verifies afterwards and returns
+//! whether it took.
+
+const std = @import("std");
+const regs = @import("regs");
+const mmio = @import("mmio");
+
+const Reg = mmio.Reg;
+const Field = mmio.Field;
+
+const config0 = Reg.at(regs.LP_WDT_CONFIG0_REG);
+const wprotect = Reg.at(regs.LP_WDT_WPROTECT_REG);
+const swd_config = Reg.at(regs.LP_WDT_SWD_CONFIG_REG);
+const swd_wprotect = Reg.at(regs.LP_WDT_SWD_WPROTECT_REG);
+
+const wdt_en = Field.of(regs.LP_WDT_WDT_EN_S, regs.LP_WDT_WDT_EN_V);
+/// Flash-boot mode runs the watchdog independently of `wdt_en`, which is how the bootloader keeps
+/// the fuse lit across the handover. Clearing `wdt_en` alone leaves this armed.
+const flashboot_en = Field.of(regs.LP_WDT_WDT_FLASHBOOT_MOD_EN_S, regs.LP_WDT_WDT_FLASHBOOT_MOD_EN_V);
+const swd_disable = Field.of(regs.LP_WDT_SWD_DISABLE_S, regs.LP_WDT_SWD_DISABLE_V);
+const swd_auto_feed = Field.of(regs.LP_WDT_SWD_AUTO_FEED_EN_S, regs.LP_WDT_SWD_AUTO_FEED_EN_V);
+const swd_feed = Field.of(regs.LP_WDT_SWD_FEED_S, regs.LP_WDT_SWD_FEED_V);
+const feed_reg = Reg.at(regs.LP_WDT_FEED_REG);
+const feed_bit = Field.of(regs.LP_WDT_FEED_S, regs.LP_WDT_FEED_V);
+
+/// The unlock key for both blocks, which is also each key register's reset value: "if the register
+/// contains a different value than its reset value, write protection is enabled"
+/// (lp_wdt_reg.h). `LP_WDT_WKEY_VALUE` and `LP_WDT_SWD_WKEY_VALUE` in
+/// esp_hal_wdt/esp32p4/include/hal/lpwdt_ll.h:25,27 are both this number.
+const wkey: u32 = 0x50D8_3AA1;
+
+/// Unlocked access to the RTC watchdog. `defer guard.release()` re-locks.
+pub const Guard = struct {
+ pub inline fn release(_: Guard) void {
+ // Anything that is not the key locks it. ESP-IDF writes 0 (lpwdt_ll.h), so this does too:
+ // it keeps the register comparable against IDF's in a differential test.
+ wprotect.writeRaw(0);
+ }
+};
+
+pub inline fn unlock() Guard {
+ wprotect.writeRaw(wkey);
+ return .{};
+}
+
+/// Turn the RTC watchdog off, and stop the super watchdog behind it.
+///
+/// Returns false if the write did not take, which means the key was wrong: a protected register
+/// swallows writes without complaint, so the only way to know is to read back.
+pub fn disable() bool {
+ {
+ const guard = unlock();
+ defer guard.release();
+ // Both bits, in one store: clearing `wdt_en` while leaving flash-boot mode armed is the
+ // half-fix that still reboots.
+ config0.modify(.{ wdt_en.is(0), flashboot_en.is(0) });
+ }
+
+ // The super watchdog is a separate block with its own key.
+ swd_wprotect.writeRaw(wkey);
+ swd_config.modify(.{ swd_disable.is(1), swd_auto_feed.is(0) });
+ swd_wprotect.writeRaw(0);
+
+ return config0.get(wdt_en) == 0 and config0.get(flashboot_en) == 0 and swd_config.get(swd_disable) == 1;
+}
+
+/// Feed the RTC watchdog instead of disabling it, for an application that would rather keep the
+/// protection.
+///
+/// The counter is fed through its own register, `LP_WDT_FEED_REG`, not through anything in
+/// CONFIG0 - ESP-IDF's `lpwdt_ll_feed` writes `hw->feed.feed = 1`. An earlier version of this
+/// function read a CONFIG0 field and wrote the same value back, which is a pure no-op: the register
+/// ended with the bits it started with and the counter kept running. An application that took this
+/// module's own advice - keep the protection, feed it - would have been reset about ten seconds
+/// later with nothing on the console, which is the exact failure this file exists to document. The
+/// differential harness could not have caught it either, because a no-op leaves the register
+/// bit-identical.
+pub fn feed() void {
+ const guard = unlock();
+ defer guard.release();
+ feed_reg.write(.{feed_bit.is(1)});
+}
+
+/// Feed the super watchdog once. Independent of RWDT and of its own auto-feed setting.
+pub fn feedSuper() void {
+ swd_wprotect.writeRaw(wkey);
+ swd_config.modify(.{swd_feed.is(1)});
+ swd_wprotect.writeRaw(0);
+}
+
+/// Whether either watchdog is still armed - worth printing once at startup, because the symptom of
+/// getting this wrong is a reset ten seconds later with no other clue.
+pub fn armed() bool {
+ return config0.get(wdt_en) == 1 or config0.get(flashboot_en) == 1 or swd_config.get(swd_disable) == 0;
+}