Embedded Systems · Safety-Critical Systems
Windowed Watchdogs and the Program-Flow Contract
Why a watchdog that accepts a refresh too early is as dangerous as one that never fires, how the window and the sequence check constrain the timing, and where the supervision must live to be independent.
A watchdog timer is usually described as a device that resets the processor if the software stops running. That description is too weak to be useful, because it only covers one of the ways software fails. A program that runs but skips its control loop, or that spins in a fault handler while a healthy-looking task refreshes the timer, or that executes its refresh from a periodic interrupt that keeps firing after the main loop has wedged, is running by every clock-based definition and is nonetheless not performing its function. The mechanisms that catch these cases add a second dimension to the timer: not just “did the refresh arrive”, but “did it arrive at the right time, from the right place, in the right order”.
The single-sided timeout and its blind spot
A plain watchdog counts down from a reload value and resets when the count reaches zero. Software refreshes it, conventionally by writing a key sequence to a register, and as long as the interval between refreshes is shorter than the timeout, no reset occurs. The constraint is therefore one-sided: any refresh interval below the limit is accepted.
That one-sidedness is a real gap in supervision. Consider a control loop that is supposed to complete a cycle every 10 milliseconds and refresh the watchdog at the end of each cycle. If the loop instead completes in 200 microseconds because a sensor read returned immediately, the refresh rate goes up, not down, and the watchdog sees nothing wrong. The loop is still executing and the plant may be receiving stale inputs, but the supervisor is satisfied. The same blind spot covers a task that is running at ten times its intended rate, a control law whose period has collapsed because a timer prescaler was misconfigured, and an interrupt-driven refresh that has become decoupled from the work it was supposed to attest.
The consequence is not that the timeout is wrong but that it supervises only one failure direction. A fast-loop failure and a stalled-loop failure are both departures from the specified behaviour, and a mechanism that only detects one of them leaves the other entirely unmonitored.
Adding a lower bound: the window
A windowed watchdog closes that gap by defining an interval in which a refresh is legal. The refresh is accepted only after a minimum time has elapsed since the last one, and only before the timeout expires. A refresh that arrives before the window opens is itself a fault, and the device responds by resetting immediately rather than waiting for the timeout, because there is nothing to wait for: the timing contract has already been violated and the software has demonstrated that it cannot keep it.
Implementation is a comparator on the counter value, not a separate timer. The counter counts down from the reload value, and the refresh is permitted only while the counter sits between two thresholds: the upper one, at which the window opens, and zero, at which the reset occurs. Writing the refresh key outside that band, whether above the upper threshold or after the counter has passed zero, produces the reset. Many devices implement the lower bound by comparing against a window register and the upper bound by the counter’s own underflow, so the window is expressed as a fraction of the timeout, and the granularity of the lower bound is one counter tick.
The design constraint behind the lower bound is that a refresh must be evidence of work, not evidence of a clock. If the refresh happens at the end of the loop iteration, then the earliest legal refresh time is the minimum plausible duration of an iteration, and any iteration that finishes faster than that has done less work than the specification requires. Choosing the window means estimating the fastest legitimate path through the supervised code, which is a lower bound on execution time, and it is the same kind of estimate as the worst-case figure discussed in Worst-Case Execution Time Analysis and Its Uses, only in the opposite direction.
A window that is too tight creates false resets on a legitimate fast path. A window that is too loose is decorative. The honest way to set it is to instrument the code once, find the minimum observed and the structural minimum, and then leave a margin that accounts for the parts of the path that can be skipped without violating the specification.
Sequence checks: refreshing from the right place
Time bounds say when the refresh happened. They say nothing about which code performed it. A design where any task can refresh the watchdog has a supervisor whose only input is the aggregate progress of the whole system, and if one task is refreshing on behalf of the others, the others can fail silently.
Two mechanisms address this. The first is a challenge-response register: the watchdog exposes a small number of question values, and the software must answer each with the corresponding response value, in the order the device dictates. The answers are derived by a small hardware state machine, so a fixed response written unconditionally into the register does not satisfy the sequence. The second is a program-flow check performed in software, where each supervised activity increments a shared sequence counter and the refresh is performed only by the last activity in the cycle, after verifying that every earlier activity recorded its own completion in the current round.
A software version of the check has a shape like this. The counter is a plain unsigned integer, the compare is against a per-round expectation, and the whole structure lives in one task so the check itself has no locking.
/* Each supervised stage records completion once per cycle. */
enum { STAGE_SENSE, STAGE_CONTROL, STAGE_ACTUATE, STAGE_COUNT };
static volatile uint32_t stage_mask; /* bit n set when stage n completed */
void stage_done(unsigned n)
{
if (n >= STAGE_COUNT)
return;
/* __atomic_fetch_or keeps the record visible to the checker
without a lock; the checker only ever reads. */
__atomic_fetch_or(&stage_mask, 1u << n, __ATOMIC_RELEASE);
}
void supervision_task(void)
{
uint32_t seen = __atomic_exchange_n(&stage_mask, 0u, __ATOMIC_ACQUIRE);
/* All stages must have reported in the cycle just ended. A stage that
missed its slot leaves its bit clear and the refresh is skipped. */
if (seen == (1u << STAGE_COUNT) - 1u)
watchdog_refresh();
/* A missed bit is not retried here: the absence of a refresh is the
fault report, and the timer's window turns it into a reset. */
}
The subtlety is that the check is only as good as the granularity of the stages. If STAGE_ACTUATE is a function that returns without doing anything when a hardware handshake fails, the bit is set and the supervisor is misinformed. Completion has to mean “the output was confirmed”, which usually means the stage reads back a status or a measured value rather than assuming its own write took effect.
Placing the supervisor in an independent clock domain
The most common reason a watchdog fails to reset a broken system is that it shares a failure source with the system it supervises. If the watchdog is clocked from the same oscillator as the core, a stopped oscillator stops the watchdog too, and the counter never reaches zero. If the watchdog’s refresh path runs through the same interrupt controller and the same priority scheme as the supervised tasks, a priority inversion or a masked interrupt that wedges the tasks also wedges the refresh, and the watchdog fires for the wrong reason or not at all.
The architectural fix is an external supervisor with its own oscillator, its own supply monitoring, and a reset line to the processor. The processor then cannot disable its own supervisor, and the refresh becomes a physical signal on a pin rather than a register write in an address space the software controls. Many microcontrollers provide an independent on-chip watchdog clocked from an internal low-power oscillator for the same reason, and the internal oscillator’s accuracy is deliberately poor: a tolerance of tens of percent is normal, which is why the timeout must be specified with margin and the window must be wide enough to survive the spread.
Certification frameworks treat this independence as a claim that must be argued. IEC 61508-2 requires that the watchdog be shown to be independent of the function it monitors, and a watchdog implemented entirely in the same software and clocked from the same source is not independent, however carefully its refresh is sequenced. The argument is about shared failure sources, which is the subject of Common-Cause Failure and What Design Diversity Actually Buys.
Reset behaviour, cause registers, and what happens next
A watchdog reset is not the end of the sequence; it is the beginning of a recovery problem. Three details determine whether the recovery is safe.
The first is whether the reset returns the outputs to a defined state. A processor reset does not necessarily reset the peripherals that drive actuators, so a system whose watchdog fires while a PWM channel is enabled may resume with the channel still driving. The reset handler must actively drive outputs to their safe values before doing anything that depends on state, and the hardware should be arranged so that the reset line also clears or disables the output stages.
The second is the cause register. Most devices latch the reason for the last reset, distinguishing power-on, external reset, watchdog reset, and software-requested reset. Reading it and recording it before it is cleared is what makes a repeated watchdog reset diagnosable later, and the recording must survive the reset, so it belongs in non-volatile memory written early in the startup path.
The third is what the recovery does with the information. A system that resets and resumes the same operation that caused the reset will loop, and the loop may be more dangerous than the original fault if the operation has a physical effect. A safer pattern is a bounded retry count in persistent storage: the first watchdog reset restarts normally, the second restarts into a restricted mode with the actuator outputs held safe, and further resets latch a fault that requires external intervention. The number of attempts and the mode each attempt selects are design decisions, but the existence of a ladder is not optional, because the failure mode of an unbounded restart loop is a system that repeatedly performs a partial, harmful action.
A watchdog is therefore three things at once: a timer with two bounds, a checker with an expected sequence, and a reset path with a defined state. Weakness in any of the three leaves a mechanism that reports health without measuring it, which is worse than no mechanism at all because it produces evidence that a reviewer will accept.
References
- IEC 61508-2:2010, Part 2: Requirements for Electrical/Electronic/Programmable Electronic Safety-Related Systems — International Electrotechnical Commission
- ISO 26262-5:2018, Road Vehicles — Functional Safety — Part 5: Product Development at the Hardware Level — International Organization for Standardization
- ARMv7-M Architecture Reference Manual — Arm Limited