diff options
Diffstat (limited to 'src/hal/rwdt.zig')
| -rw-r--r-- | src/hal/rwdt.zig | 123 |
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; +} |
