summaryrefslogtreecommitdiff
path: root/src/hal/rwdt.zig
blob: 5002400a73b0d3cd31e5772d56ef73f07d034e00 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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;
}