Deep dive
How it works
In: timeout T (optional window opening W), periodic kicks from supervision code · Out: a reset signal when kicks stop, come too early, or arrive out of pattern · State: the down-counter (own clock) and, in software, one alive count per task · Cost: O(1) per kick (one register write); O(tasks) per supervision pass
- Configure timeout
T(and, for a window watchdog, opening timeW < T), then start it. Typical independent watchdogs are started by a key write and then cannot be disabled until reset. - The counter runs from its own oscillator and decrements; reaching zero asserts reset. A kick reloads it. In window mode a kick before
Whas elapsed also asserts reset. - Software supervision decides when to kick. Each task reports "alive" once per pass of its own loop; a supervisor, run at a fixed period from the main loop or lowest-priority context, kicks only if every task reported a count inside its expected
[min, max]range, then clears all counts. - A count above
maxcatches a task that runs away or is called too often; belowmincatches a stuck or starved task. A failed period gives no credit to the next one. - After reset, read the reset-cause register first (watchdog vs. power-on vs. brown-out), store a reason code in no-init RAM or flash, and count consecutive watchdog resets so a boot loop falls back to a safe image or safe mode.
time ──► 0 W (window opens) T (timeout)
│ kick = RESET │ kick OK │ no kick = RESET
kick path: main loop ► Supervisor.healthy_and_clear() ► WDT reload
Code
Software model of a window watchdog plus alive-count supervision (std only; the register write becomes kick on target):
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Reset { TooEarly, Timeout }
/// Model of a windowed hardware watchdog. Time is a wrapping u32 tick counter (ms).
pub struct WindowWatchdog { open: u32, timeout: u32, last: u32 }
impl WindowWatchdog {
/// A kick is legal only in [open, timeout) after the previous kick.
pub fn new(open: u32, timeout: u32, now: u32) -> Self {
assert!(open < timeout);
WindowWatchdog { open, timeout, last: now }
}
/// Call from the hardware's point of view on every tick: expiry resets the CPU.
pub fn poll(&self, now: u32) -> Option<Reset> {
(now.wrapping_sub(self.last) >= self.timeout).then_some(Reset::Timeout)
}
/// The reload register write.
pub fn kick(&mut self, now: u32) -> Result<(), Reset> {
let dt = now.wrapping_sub(self.last);
if dt >= self.timeout { return Err(Reset::Timeout); }
if dt < self.open { return Err(Reset::TooEarly); }
self.last = now;
Ok(())
}
}
/// Software supervisor: one hardware kick only if every task reported
/// an in-range number of times since the last kick.
pub struct Supervisor<const N: usize> {
count: [u8; N],
min: [u8; N],
max: [u8; N],
}
impl<const N: usize> Supervisor<N> {
pub fn new(min: [u8; N], max: [u8; N]) -> Self { Supervisor { count: [0; N], min, max } }
/// Each task calls this once per pass of its own loop.
pub fn alive(&mut self, task: usize) { self.count[task] = self.count[task].saturating_add(1); }
/// Called from the lowest-priority/idle-level check, never from a task that
/// could keep running while another is dead. Returns true if the HW may be kicked.
pub fn healthy_and_clear(&mut self) -> bool {
let ok = (0..N).all(|i| (self.min[i]..=self.max[i]).contains(&self.count[i]));
self.count = [0; N]; // a failed period must not carry credit forward
ok
}
}
Tested with a simulated run: a task that stops reporting at t=2 s causes a reset at t=3 s; early kicks, late kicks and a runaway task are all rejected.
Complexity and constants
- Kick: one store to a peripheral register (an APB write; a few bus cycles, order of 10-100 ns on a Cortex-M). The supervisor is
O(tasks)compares per period. - Detection latency from hang to reset is at most
Tplus the time since the last good kick; with supervision periodPit is order ofT + P. - Timeout choice:
T >= worst-case time between supervisor runs + margin; typical values run from tens of milliseconds (motor control) to seconds (gateways). Tolerance of the low-speed RC oscillator is large (order of tens of percent on common MCU RC oscillators), so size margins accordingly and check the datasheet. - Counts are bytes:
3Nbytes of RAM forNtasks.
Efficiency
- Cost: one kick is a single peripheral register write, order of 10-100 ns on a Cortex-M; the supervisor is O(tasks) compares per period and 3N bytes of RAM. Bound: none in the data path; the real cost is energy when kicks force wake-ups from sleep.
- Levers:
- kick at the supervision period (tens to hundreds of ms), not every loop pass — fewer bus writes and wake-ups — all designs
- pause the counter in sleep (Zephyr
WDT_OPT_PAUSE_IN_SLEEP) or use the longestTthe safety case allows — fewer wake-ups on a coin cell — long-sleeping devices - Linux: one
write()orsd_notifydatagram per half timeout (systemd's recommended interval) — negligible load — services - single timeout without window or per-task counts — a few instructions — when a reset loop is harmless and tasks cannot false-kick
- Naive → standard → state of the art:
- kick from a timer ISR · cheapest, detects nothing useful
- window watchdog kicked by a supervisor after every task checked in · one write per period
- window plus flow supervision (AUTOSAR style) and an external challenge-response watchdog · adds a monitor chip or core, catches skipped steps and a dead MCU
Implementation notes
- Start the watchdog as early as possible in boot, before slow init (flash erase, radio calibration); lengthen the timeout during init rather than disabling it, or kick between init stages.
- Long blocking operations (flash page erase, firmware update) need a timeout that fits them, or an explicit supervised step that kicks between pages.
- Freeze the counter when halted by a debugger (Zephyr:
WDT_OPT_PAUSE_HALTED_BY_DBG; most vendors have a debug-freeze bit) or breakpoints will reset the target. - External watchdog ICs (separate chip, own supply monitoring) cover the case where the MCU itself is dead or its internal watchdog is misconfigured; they usually require a toggling pin, so make the toggle come from the same supervisor.
- Challenge-response ("question and answer") watchdogs make the supervisor send an answer derived from a challenge; this catches a kick path stuck in a loop that always writes the same value.
- On Linux,
/dev/watchdogarms the timer on open; writing any character kicks it, and writingVbefore closing disables it if the driver supports "magic close" (not ifCONFIG_WATCHDOG_NOWAYOUTor thenowayoutmodule parameter is set).WDIOC_SETTIMEOUTchanges the timeout where the driver supports it. systemd arms per-service watchdogs withWatchdogSec=; the service sendsWATCHDOG=1viasd_notifyand should do so at about half the interval.
Pitfalls
- Kicking from a timer ISR: the interrupt keeps firing while the application is dead, so the watchdog never trips.
- A dedicated "kick task" at high priority with the same problem; it measures scheduler health, not application health.
- Kicking inside every driver or in loops that can spin: one stuck loop with a kick inside is invisible.
- Timeout shorter than the worst-case path (flash erase, EEPROM write, retry loops): resets in the field that never appear on the bench.
- Taking the RC oscillator at its nominal frequency: at the slow end of its tolerance the real timeout is much longer (or shorter) than computed.
- Reset loops: a persistent fault (bad config in flash) makes every boot reset again. Count resets and fall back.
- Disabling the watchdog "for debugging" and shipping that build.
- Treating the reset as recovery: outputs (valves, heaters, motors) must default to a safe state while the CPU is in reset.
Variants and alternatives
In production
- Spaceflight: Ganssle cites the Clementine spacecraft, whose hung software fired thrusters, as the case for watchdogs as integral parts of a system.
- Automotive: ECUs under the AUTOSAR Watchdog Manager (alive, deadline and logical supervision).
- Embedded and IoT: Zephyr's
wdt_*API with per-timeoutwindow.min/maxand reset flags; ESP-IDF interrupt, task and RTC watchdogs; STM32 independent and window watchdogs. - Servers and gateways: the Linux watchdog framework (
/dev/watchdog) with systemd.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| Zephyr watchdog API | C | zephyrproject-rtos/zephyr | west init (Zephyr getting started) | — | — | — | — | wdt_install_timeout, wdt_setup, wdt_feed; per-timeout window and reset mode |
| ESP-IDF watchdogs | C | espressif/esp-idf | install.sh in the repo | — | — | — | — | interrupt WDT, task WDT with per-task subscription (esp_task_wdt_reset), RTC WDT |
| Linux watchdog framework | C | torvalds/linux | n/a (in kernel) | — | — | — | — | /dev/watchdog, magic close, nowayout; docs at docs.kernel.org/watchdog |
| systemd | C | systemd/systemd | distro package | — | — | — | — | service watchdog via WatchdogSec= and sd_notify; also feeds the hardware watchdog (RuntimeWatchdogSec=) |
| FreeRTOS kernel | C | FreeRTOS/FreeRTOS-Kernel | git submodule | — | — | — | — | no built-in watchdog; supervision is built from tasks, hooks and the idle task |
Sources
- Jack Ganssle, Great Watchdog Timers for Embedded Systems. article
- AUTOSAR, Specification of Watchdog Manager (Classic Platform). spec
- Linux kernel documentation, The Linux WatchDog Timer Driver Core / Watchdog API. docs
- systemd documentation, systemd.service(5) (WatchdogSec) and sd_watchdog_enabled(3). man page
- Espressif, ESP-IDF Programming Guide, Watchdogs. docs
- Zephyr Project, Watchdog API. docs