2026-06-15 · Davide Carrese

STM32 IWDG Watchdog:
Silent Reset Prevention in Production Firmware

STM32 · IWDG · Watchdog · STM32F4 · STM32U5 · Embedded

Nothing worse than a field return that "sometimes resets" with no crash log, no Serial Wire Viewer trace, and no UART output because the reset happened faster than the bootloader printed anything. A misconfigured or improperly-fed watchdog is the top suspect. The STM32 Independent Watchdog (IWDG) is a simple hardware timer driven by its own dedicated RC oscillator — it runs independently of the main system clock and will reset the MCU if the firmware fails to refresh it in time. Getting the IWDG right in production firmware means choosing the right timeout, feeding at the right granularity, and — crucially — detecting why the watchdog fired in the first place.

The IWDG is not the WWDG. The Window Watchdog (WWDG) uses the system clock (APB), can be configured for early-warning interrupt before reset, and requires the refresh to happen within a specific window — too early or too late triggers a reset. The IWDG uses an independent RC oscillator (LSI, typically 32 kHz ± several percent across temperature), has no early-warning interrupt capability (only reset), and has an optional window feature. For most production applications, the IWDG is the right choice: simpler, more robust against main-clock failures, and harder to accidentally misconfigure.

IWDG Architecture in STM32

The IWDG peripheral consists of a 12-bit down-counter clocked from the LSI after a programmable prescaler. The counter runs continuously after being started, and when it reaches zero the IWDG asserts a system reset. The firmware must reload the counter with the value in the Reload Register (RLR) before it underflows — this is called "feeding" or "kicking" the watchdog.

Key registers:

RegisterAddress offsetPurpose
KR (Key Register)0x00Write 0x5555 to enable PR/RLR writes; 0xCCCC to start IWDG; 0xAAAA to reload counter
PR (Prescaler Register)0x04Selects prescaler division ratio: [0=4, 1=8, 2=16, 3=32, 4=64, 5=128, 6=256]
RLR (Reload Register)0x0812-bit reload value (0–4095); counter reloads to this on each feed
SR (Status Register)0x0CPVU (Prescaler Update) and RVU (Reload Update) busy flags — must poll before changing PR/RLR after start
WINR (Window Register)0x10Optional upper limit: counter must be between WINR and 0 to feed; feeding above WINR triggers immediate reset

The LSI frequency varies significantly across temperature — ST specifies 30–50 kHz for most STM32 families, with a typical value near 40 kHz. This means the actual watchdog timeout has ±20% tolerance. Never rely on the IWDG for sub-millisecond precision; it is a safety net against complete firmware lockup, not a high-precision timing peripheral.

Timeout Calculation

t(IWDG) = (RLR + 1) × PrescalerDiv / f(LSI)

Example with LSI = 40 kHz typical:
  Prescaler = 64 (PR=4)  →  PrescalerDiv = 64
  RLR = 1250
  t = (1250 + 1) × 64 / 40000
  t ≈ 2.00 s

With minimum LSI = 30 kHz:
  t ≈ 2.67 s

With maximum LSI = 50 kHz:
  t ≈ 1.60 s

The timeout range spans from about 100 µs (PR=4, RLR=0) to 26.2 s (PR=256, RLR=4095 at 40 kHz). For production firmware, a timeout between 1 and 8 seconds is typical — long enough to survive a burst of interrupt latency during flash erase or ECC correction, short enough that a lockup does not cause a visible glitch on an actuator or communication bus.

Practical Example: IWDG Configuration on STM32F4

Here is a complete configuration sequence for STM32F401/F411 using register-level access, which works identically on STM32F4, STM32L4, STM32G4, and most STM32U5 family members:

/* iwdg_f4.c — production-grade IWDG setup for STM32F4 */

#include "stm32f4xx.h"

/* Desired timeout ≈ 3.0 s @ 40 kHz LSI typical */
#define IWDG_PRESCALER   4    /* PR=4 → /64 */
#define IWDG_RLR         1875 /* (1875+1)*64/40000 ≈ 3.0 s */

/* Status register busy flags */
#define IWDG_SR_PVU      (1U << 0)
#define IWDG_SR_RVU      (1U << 1)

static void iwdg_write_enable(void)
{
    IWDG->KR = 0x5555;   /* Enable write access to PR and RLR */
}

static void iwdg_write_disable(void)
{
    IWDG->KR = 0x0000;   /* Disable write access */
}

static void iwdg_wait_for_update(void)
{
    /* Poll until both PVU and RVU are cleared */
    while (IWDG->SR & (IWDG_SR_PVU | IWDG_SR_RVU)) { }
}

void IWDG_Init(void)
{
    /* Step 1: Enable write access to PR and RLR */
    iwdg_write_enable();

    /* Step 2: Set prescaler to /64 */
    IWDG->PR = IWDG_PRESCALER;
    iwdg_wait_for_update();   /* Wait for PVU to clear */

    /* Step 3: Set reload value */
    IWDG->RLR = IWDG_RLR;
    iwdg_wait_for_update();   /* Wait for RVU to clear */

    /* Step 4: Start the watchdog by writing the key */
    IWDG->KR = 0xCCCC;

    /* Step 5: Disable write access */
    iwdg_write_disable();
}

void IWDG_Feed(void)
{
    /* Write the reload key — reloads RLR into the counter */
    IWDG->KR = 0xAAAA;
}

/*
 * NOTE: Once enabled, the IWDG can ONLY be stopped by a system reset.
 * There is no "disable IWDG" register write — this is intentional.
 * If you need to pause the watchdog during debug, configure DBGMCU freeze.
 */

The "Feed in Main Loop" Anti-Pattern

The most common mistake I see in production firmware is feeding the watchdog inside the main while(1) loop at the top of every iteration. This makes the IWDG almost useless: as long as the scheduler keeps spinning, every blocking subroutine is invisible to the watchdog. A SPI transaction that hangs waiting for a TXE flag that will never set? The main loop keeps feeding right past it. A FreeRTOS task that blocks on a queue forever? The idle hook or main loop keeps feeding.

⚠ Anti-Pattern

Never feed the watchdog in the main loop or in a high-frequency timer interrupt. The IWDG should validate that all critical subsystems made progress within the timeout window.

The correct pattern is to feed at strategic checkpoints — after a block of work that the firmware must complete to guarantee system safety. For example:

void main(void)
{
    SystemInit();
    IWDG_Init();              /* Start IWDG, ~3 s timeout */

    while (1)
    {
        sensor_read();        /* Blocking I2C read — must complete */
        IWDG_Feed();          /* Checkpoint 1 */

        control_loop();       /* PID update — must complete */
        IWDG_Feed();          /* Checkpoint 2 */

        can_transmit();       /* CAN TX — must complete */
        IWDG_Feed();          /* Checkpoint 3 */

        if (sdcard_write_pending()) {
            sdcard_write_block();  /* Blocking SDIO — must complete */
            IWDG_Feed();          /* Checkpoint 4 */
        }

        __WFI();              /* Enter sleep, no feeding here */
    }
}

If sensor_read() hangs forever, the watchdog fires after ~3 seconds. If can_transmit() blocks on a full mailbox, the watchdog fires. This is the behaviour you want: a single fault in any subsystem causes a controlled recovery, not a silent stall.

For FreeRTOS-based firmware, I use a dedicated watchdog task at the lowest priority that feeds only after receiving notifications from the other critical tasks:

/* FreeRTOS watchdog task pattern */
void vWatchdogTask(void *pvParameters)
{
    TickType_t last_wake = xTaskGetTickCount();

    for (;;)
    {
        /* Wait for all critical tasks to report in */
        ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(2500));

        /* If any task missed its deadline, we don't get here — reset! */
        IWDG_Feed();
    }
}

/* Each critical task calls this at the end of its cycle: */
void Watchdog_CheckIn(TaskHandle_t watchdog_task)
{
    xTaskNotifyGive(watchdog_task);
}

The IWDG timeout must be longer than the combined worst-case execution time of all critical tasks, including their stacked interrupt latency. A rule of thumb: set the timeout to 3× the expected cycle time of the slowest critical path.

Debug Mode: Freezing the IWDG

By default, when the debugger halts the CPU (breakpoint, single-step), the IWDG counter keeps running. If the timeout expires while you are stepping through code, the watchdog resets the MCU — and you lose your debug session. The fix is to configure the DBGMCU peripheral to freeze the IWDG counter during debug halt:

/* In SystemInit() or early startup — before starting IWDG */

/* STM32F4: DBGMCU_APB1_FZ register */
DBGMCU->APB1FZ |= DBGMCU_APB1_FZ_DBG_IWDG_STOP;

/* STM32U5/STM32G4: same register, same bit */
DBGMCU->APB1FZ |= DBGMCU_APB1_FZ_DBG_IWDG_STOP;

This is one of the first things I add to any project template.

Detecting Watchdog Resets at Boot

Without a mechanism to detect that a watchdog reset occurred, you are blind to field faults. Every STM32 has a Reset and Clock Control (RCC) register that latches the last reset source:

/* Check if the last reset was caused by the IWDG */

uint32_t rcc_csr = RCC->CSR;  /* Or RCC->RSR on newer families */

if (rcc_csr & RCC_CSR_IWDGRSTF) {
    /* IWDG caused the last reset — log it */
    log_event("WATCHDOG: IWDG reset at %lu ms since power-on", get_uptime_ms());

    /* Clear the flag (write 1 to clear on most STM32) */
#if defined(STM32F4)
    RCC->CSR |= RCC_CSR_RMVF;
#elif defined(STM32U5)
    RCC->RSR |= RCC_RSR_RMVF;
#endif
}

/* Also check for WWDG reset on the same register */
if (rcc_csr & RCC_CSR_WWDGRSTF) {
    log_event("WATCHDOG: WWDG reset");
    /* Clear flag — same RMVF bit */
}
⚠ Important

On STM32F4, the CSR register is write-protected by the LSI oscillator enable bit. You must enable the LSI in the RCC before writing to CSR, even if you only want to read it. For a read-only check, RCC->CSR is accessible, but clearing RMVF requires RCC->CSR |= RCC_CSR_RMVF which may need LSION set first. On STM32U5 and newer families, this is handled differently — read the reference manual.

In production firmware, I store the reset cause in a battery-backed backup register (RCC Backup SRAM or RTC Backup Register) so it survives a power cycle or a subsequent watchdog reset that clears the CSR flags. This way, even after multiple consecutive watchdog resets, the bootloader can read the backup register and decide whether to enter safe mode or continue normal boot.

/* Store reset cause in RTC backup register for post-mortem analysis */
void store_reset_cause(void)
{
    uint32_t rcc_csr = RCC->CSR;
    uint32_t cause = 0;

    if (rcc_csr & RCC_CSR_IWDGRSTF)    cause = 1;
    else if (rcc_csr & RCC_CSR_WWDGRSTF) cause = 2;
    else if (rcc_csr & RCC_CSR_SFTRSTF) cause = 3;  /* Software reset */
    else if (rcc_csr & RCC_CSR_PINRSTF) cause = 4;  /* NRST pin */
    else if (rcc_csr & RCC_CSR_PORRSTF) cause = 5;  /* Power-on reset */

    if (cause) {
        /* Enable backup domain write */
        PWR->CR |= PWR_CR_DBP;
        RTC->BKP0R = (RTC->BKP0R & 0xFF00) | cause;
        PWR->CR &= ~PWR_CR_DBP;
    }

    /* Clear the reset flags */
#if defined(STM32F4)
    RCC->CSR |= RCC_CSR_RMVF;
#endif
}

IWDG vs WWDG: When to Use Which

FeatureIWDGWWDG
Clock sourceLSI (independent, 32 kHz RC)HCLK / APB (system clock dependent)
Timeout precision±20–30% (RC drift)High (clocked from PLL/HSE)
Early-warning interrupt❌ No✅ Yes (EWI flag before reset)
Window featureOptional (WINR)Mandatory (counter vs window)
Survives clock failure✅ Yes (independent oscillator)❌ No (stops with clock)
Best forSafety net, brown-out, crystal failurePrecise task deadline monitoring

My rule: use the IWDG as the primary system-level watchdog (always enabled in production builds), and use the WWDG only when a specific task has a hard real-time deadline that must be met within microseconds. For most embedded applications, the IWDG alone is sufficient.

Practical Checklist

ScenarioAction
Setting up IWDG for the first timeConfigure PR, RLR, start with 0xCCCC; poll SR before changing PR/RLR
Feeding the watchdogWrite 0xAAAA to KR at strategic checkpoints, NOT in main loop
Debugging with IWDG enabledFreeze IWDG via DBGMCU_APB1_FZ before starting
Detecting watchdog resetsRead RCC->CSR (or RSR) at boot; log IWDGRSTF/WWDGRSTF
Making reset cause survive multiple resetsStore in RTC backup register
IWDG timeout selection3× worst-case critical path; account for LSI tolerance
Production vs debug buildEnable IWDG in release builds only; disable or freeze in debug
FreeRTOS + IWDGDedicated watchdog task with task notification pattern
WWDG for hard deadlinesUse WWDG only when IWDG precision is insufficient

How I Would Approach This on a Client Project

Here is my standard IWDG integration template for any STM32 production firmware:

  1. Add IWDG configuration during board init, right after clock setup. Use a target timeout of 3–5 seconds with a prescaler that keeps RLR in the middle of the 12-bit range (512–2048) for flexibility.
  2. Freeze the IWDG in debug mode immediately — this avoids losing debug sessions and saves hours of frustration.
  3. Implement a store_reset_cause() function as the very first thing in main(), before any peripheral init. Store the result in a backup register and in a persistent log area in flash.
  4. Define strategic feed points based on the application's control flow — not the scheduler loop. Each feed point should be preceded by a complete operation that must not hang.
  5. On each watchdog reset, increment a counter in the backup register and implement a "reset escalation" policy: if the watchdog fires more than N times within T minutes, enter a safe mode that disables actuators and logs diagnostics before attempting full recovery.
  6. Disable the IWDG in debug builds (via a preprocessor conditional on NDEBUG or a custom PRODUCTION_BUILD define). Testing with the watchdog enabled is important during system integration, but during task-level debugging it costs time.

This approach has rescued several field deployments. On a recent STM32U5-based IoT sensor node, the watchdog caught a race condition in the SD-card write-back cache that only appeared after three weeks of continuous operation — the kind of fault no lab test ever reproduces. The backup register log showed exactly "IWDG reset at 1872000 ms", which pointed straight to the SD card routine.

Sources and Further Reading

💬 Comments by email

Reply to this article by email — I read and respond to every message.