//! 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; }