Skip to content

esp32_wdt

The ESP32’s watchdogs.

Three exist: one in the RTC domain (RWDT) and one in each timer group (MWDT0, MWDT1). startup.S disables all three during boot, because a half-initialised kernel being reset mid-bring-up is worse than no watchdog — but nothing re-armed them afterwards, which left the chip less protected under FlintOS than it is under the ROM.

They are not redundant. Each catches something the other cannot:

  • RWDT lives in the RTC domain and survives anything short of a power cycle. Fed from the timer interrupt, so it fires when the kernel itself has stopped — interrupts stuck masked, a fault loop, a trap handler that never returns. It cannot catch a spinning task, because the tick still runs and still feeds it.

  • MWDT is fed from the idle task. Idle runs only when nothing else is runnable, so a task that never blocks starves it — and the watchdog fires on a system that is, from the tick’s point of view, perfectly healthy.

Register layout and the unlock keys are from the ESP32 TRM, chapters 12 (timer groups) and 30 (RTC), cross-checked against esp-idf’s soc/rtc_cntl_reg.h and soc/timer_group_reg.h.

enum Mwdt {
Group0,
Group1,
}

Which timer group’s watchdog.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

unsafe fn mwdt_arm(which: Mwdt, ms: u32)

Arm a timer-group watchdog to reset the system if not fed within ms.

Writes the timer group’s watchdog registers, and commits the caller to feeding it.

References: Mwdt

unsafe fn mwdt_disable(which: Mwdt)

Disable a timer-group watchdog.

Writes the timer group’s watchdog registers.

References: Mwdt

unsafe fn mwdt_feed(which: Mwdt)

Feed a timer-group watchdog.

Writes the timer group’s watchdog registers.

References: Mwdt

const fn mwdt_ticks_for_ms(ms: u32) -> u32

Watchdog ticks for ms at [MWDT_PRESCALE], saturating.

const fn rtc_cycles_for_ms(ms: u32) -> u32

RTC slow-clock cycles for ms, saturating rather than wrapping.

A wrapped divisor would arm the watchdog with a near-zero timeout, resetting the board immediately and continuously — a boot loop caused by the thing meant to recover from one.

unsafe fn rwdt_arm(ms: u32)

Arm the RTC watchdog to reset the system if it is not fed within ms.

Writes the RTC watchdog registers. Arming it means the board resets unless something feeds it; the caller owns that obligation.

unsafe fn rwdt_disable()

Disable the RTC watchdog.

Writes the RTC watchdog registers. After this nothing recovers a hung system short of a power cycle.

unsafe fn rwdt_feed()

Feed the RTC watchdog.

Writes the RTC watchdog registers. Called from the timer interrupt.

const MWDT_PRESCALE: u32;

Prescaler giving a 1 ms watchdog tick off the 80 MHz APB clock.

80_000 APB cycles is exactly 1 ms, and the field is 16 bits — so 80_000 does not fit. 40_000 gives 0.5 ms per tick and does, which is why the timeout conversion below counts half-milliseconds rather than milliseconds.

const RTC_SLOW_CLK_HZ: u32;

Nominal RTC slow-clock frequency, in Hz.

The internal 150 kHz RC oscillator, which is not precise: ±10% across parts and it drifts with temperature. That is why the RWDT timeout is specified in seconds rather than milliseconds — at a multi-second timeout a 10% error is irrelevant, and anything needing better accuracy than that wants the MWDT, which counts off APB.