How to Implement Watchdog Timers Correctly in Safety-Critical Firmware

Somewhere right now, a firmware engineer is shipping a device with a watchdog timer that will never fire. The watchdog is configured, the kick is in the main loop, and every code review slide says “watchdog protected.” But the system could deadlock for 30 seconds, starve a critical task indefinitely, or spin in a tight fault loop, and that watchdog would keep happily resetting its counter, oblivious to all of it.
I’ve seen this exact failure on a medical device prototype that locked up during field testing. The RTOS scheduler was still running. The main loop was still cycling. The watchdog was getting fed on schedule. But the sensor acquisition task had been deadlocked for 11 seconds before a nurse noticed the screen was frozen. The watchdog “protection” did nothing.
The fix isn’t complicated. But it requires rethinking what a watchdog is actually supposed to prove. This guide walks through the correct architecture, with STM32-focused examples, and maps it to what safety standards expect.
What Your Watchdog Actually Needs to Prove
Most engineers think of a watchdog as answering: “Is the CPU still running?” That’s the wrong question. The right one: “Is every critical function in my system completing on time?”
A properly designed watchdog should catch:
- A task stuck waiting on a semaphore that’ll never post (deadlock)
- A loop completing in 200μs instead of 50ms (runaway execution)
- Stack corruption causing a wild jump into unrelated code
- Priority inversion starving your safety-critical task for seconds
And here’s what a watchdog can’t catch, no matter how clever: silent data corruption, logic errors that produce wrong-but-timely results, and hardware failures outside the MCU. A watchdog is your last line of defense against control flow failures, not a general-purpose safety net.
IWDG vs WWDG on STM32: Know Your Hardware
STM32 microcontrollers give you two watchdog peripherals. They solve different problems.
┌──────────────────┬────────────────────────────┬────────────────────────────┐
│ Feature │ IWDG │ WWDG │
├──────────────────┼────────────────────────────┼────────────────────────────┤
│ Clock Source │ LSI (~32 kHz, independent) │ APB1 (system-derived) │
│ Reset Trigger │ Countdown reaches 0 │ Countdown reaches 0x3F │
│ │ │ OR refresh outside window │
│ Window Mode │ Available on some STM32s │ Always available │
│ │ (e.g., STM32L4, STM32H7) │ │
│ Independence │ Runs if main clock fails │ Dies with system clock │
│ Typical Use │ System-level liveness │ Timing-critical task guard │
│ Freeze in Debug │ Configurable via DBGMCU │ Configurable via DBGMCU │
└──────────────────┴────────────────────────────┴────────────────────────────┘The IWDG runs on its own clock. If your main oscillator dies, the IWDG still fires. That clock independence is why safety-critical designs use it as the primary watchdog.
The WWDG (or IWDG window mode on newer families) adds a different capability: it catches code that runs too fast. A refresh arriving before the window opens triggers a reset. A refresh arriving after the counter expires also triggers a reset. A tight spin loop can’t game its way past both constraints.
Safety-critical designs should use both. The IWDG handles system-level recovery with clock independence. The WWDG (or IWDG window mode) handles timing verification. Check RM0433 (H7), RM0351 (L4), or RM0440 (G4) for your specific family’s IWDG window support.
The Anti-Patterns That Pass Code Review
Kicking in a timer ISR. The ISR keeps firing even when your application code is completely hung. The watchdog sees regular refreshes and stays quiet while your system is brain-dead.
Kicking unconditionally in the main loop. If you’re running an RTOS, the scheduler can keep the idle task (and your main loop) running just fine while a critical task is deadlocked. A hung task stretches the timeout silently, maybe long enough that nobody notices for hours.
Letting every task kick the watchdog independently. You’ve fragmented the timeout across multiple call sites, and no single point in the code knows whether the whole system is healthy. A healthy task can mask a failing one indefinitely. Even worse, you’ve made it nearly impossible to reason about the system’s aggregate health during review.
Then there’s the classic: disabling the watchdog during development and forgetting to re-enable it. This one usually gets caught. Usually. Ship what you test.
The Correct Pattern: Task Check-In Architecture
Here’s the architecture that actually works, sometimes called “task passport” or “health aggregator”:
ANTI-PATTERN: CORRECT PATTERN:
┌──────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Main Loop│ │ Task A │ │ Task B │ │ Task C │
│ │ │ ✓ check │ │ ✓ check │ │ ✓ check │
│ do_work()│ │ in │ │ in │ │ in │
│ │ └────┬────┘ └────┬────┘ └────┬────┘
│ KICK WDG │◄── always │ │ │
│ │ (blind) ▼ ▼ ▼
└──────────┘ ┌──────────────────────────────────┐
│ Watchdog Manager │
│ All checked in? ──► KICK WDG │
│ Any missing? ──► LET IT DIE │
└──────────────────────────────────┘The rules are simple:
- Each supervised task sets a flag (or bumps a counter) at a known point in its execution cycle, after completing its critical work.
- A single watchdog manager, running periodically or as the lowest-priority task, checks whether all tasks have reported in.
- Only when every check-in is present does the manager refresh the watchdog.
- If any task missed its window, the manager withholds the refresh. The counter drains. The system resets.
Here’s the pseudocode:
#define NUM_TASKS 3
static volatile uint32_t checkin_flags = 0;
/* Called by each task after completing its critical work */
void watchdog_checkin(uint8_t task_id) {
checkin_flags |= (1U << task_id);
}
/* Watchdog manager — runs periodically or as lowest-priority task */
void watchdog_manager(void) {
uint32_t expected = (1U << NUM_TASKS) - 1; /* all bits set */
if ((checkin_flags & expected) == expected) {
HAL_IWDG_Refresh(&hiwdg);
checkin_flags = 0; /* reset for next cycle */
}
/* If not all tasks checked in, don't refresh. Let it bark. */
}Timing matters. Your IWDG timeout must be longer than the slowest task’s worst-case check-in period, but short enough that a hang gets caught before it causes real harm. If your slowest task cycle is 500ms, a 2s timeout gives you reasonable headroom.
A note on atomicity: on Cortex-M, single word-aligned writes are atomic, so the bitwise OR on checkin_flags is safe without a mutex for this simple case. If you need per-task deadline tracking or variable-rate check-ins, you’ll want a more structured approach with proper synchronization.
FreeRTOS users can bolt the manager into vApplicationIdleHook or run it as a dedicated low-priority software timer task. The key constraint is that the manager must run at lower priority than the tasks it’s supervising, so a hung high-priority task will preempt the manager and starve the watchdog.
If you’re working with Hubble’s device SDK on BLE-based systems, the same pattern applies: your timing management architecture needs to account for watchdog refresh windows alongside transmission scheduling.
Using the IWDG Window to Catch Runaway Execution
On STM32L4, H7, G4, U5, and other newer families, the IWDG supports a window register. Refreshing the counter before it drops below the window value triggers a reset, just like letting it expire does.
Counter
Value
│
├─ Reload ──────┐
│ │ ← Refresh here = RESET (too early)
│ │
├─ Window ──────┤
│ │ ← Refresh here = OK (valid window)
│ │
├─ 0x3F ────────┤
│ │ ← Counter reaches here = RESET (too late)
└───────────────┴──────────────────────────► TimeConfiguration via HAL:
hiwdg.Init.Prescaler = IWDG_PRESCALER_64;
hiwdg.Init.Reload = 1000; /* ~2s timeout at LSI=32kHz */
hiwdg.Init.Window = 750; /* refresh allowed only after counter
drops below 750 (~500ms minimum wait) */The counter starts at Reload (1000) and counts down. Refreshing while the counter is still above 750 means your code completed too quickly, maybe because a loop body got optimized out, maybe because you’re stuck in a tight spin. The window catches it.
What Safety Standards Actually Expect
If your project touches IEC 61508 (industrial), ISO 26262 (automotive), or IEC 62304 (medical), your watchdog implementation will get scrutinized. Here’s what reviewers look for.
Logical monitoring. The watchdog must verify that the correct program sequence executed, not just that the CPU is ticking. The check-in pattern satisfies this; a bare HAL_IWDG_Refresh() in the main loop doesn’t.
Independent clock source. The watchdog timer’s clock should be independent of the main system clock, so a clock failure doesn’t silently disable your safety mechanism. The IWDG’s LSI oscillator qualifies. The WWDG’s APB1-derived clock doesn’t.
Temporal monitoring is about proving the program executes within expected time bounds, both minimum and maximum. This is exactly what window mode addresses.
Diagnostic coverage is where the pattern really pays off. IEC 61508-7, Table A.5 rates temporal and logical monitoring techniques for how well they detect dangerous failures. A simple kick-in-main approach scores low. The check-in pattern with window mode achieves medium to high, depending on implementation rigor.
You don’t need to memorize these standards right now. But knowing these expectations exist means you can design the pattern in from day one instead of bolting it on during a certification crunch 6 months before launch.
Implementation Checklist
Pin this somewhere visible during code review:
WATCHDOG IMPLEMENTATION CHECKLIST
──────────────────────────────────
[ ] Watchdog is NEVER refreshed unconditionally
[ ] Every critical task must check in to earn a refresh
[ ] IWDG used with independent LSI clock
[ ] Window mode enabled (IWDG or WWDG) to catch early refresh
[ ] Timeout tuned: long enough for worst-case, short enough to be useful
[ ] Watchdog enabled during development (freeze in debug via DBGMCU)
[ ] Reset cause logged (RCC_CSR flags) for post-mortem analysis
[ ] Behavior verified by deliberately injecting a hang in each taskThat last item is the one people skip. Deliberately kill each supervised task, one at a time, and confirm a reset within the expected window. If you haven’t done that, you don’t actually know it works.
Go Break Your Watchdog on Purpose
A watchdog is only as good as the conditions you require before feeding it. The check-in pattern isn’t hard to build: a few dozen lines of code, a bitmask, one manager function. The real discipline is maintaining it as your system grows. Every new task needs a check-in slot. Every new failure mode needs consideration.
Start with your current project. Even if it’s not headed for IEC 61508 certification, the check-in pattern costs almost nothing and catches real failures that an unconditional kick never will. Add the window feature if your STM32 family supports it. Log your reset causes. And inject faults, deliberately, regularly, to prove the thing actually works.
Your watchdog should be the meanest part of your firmware. Make it earn that reputation.
Hubble Network connects directly to Bluetooth chips in embedded devices from satellite, so your watchdog-protected firmware can report health data from anywhere on Earth — no extra radios or ground infrastructure required. See how it works →