Embedded HandbookApps
All sections › IV. FreeRTOS
STM32F407 · level intermediate · 17 min read
Open in the interactive handbook

33а. A watchdog for several tasks: who has hung and how to notice it

In section 12 there is one watchdog for the whole program: its single loop feeds the IWDG, and the WWDG watches the rhythm. In section 29.7 tasks set bits in an event group, and a software timer feeds the watchdog only if all of them have been collected. The same place shows that you must not feed it "because the daemon is alive". Here is the next step: a separate supervisor task with its own deadline for each task — it names exactly who went silent, writes this into memory that survives a reset, and is itself watched by the WWDG. We look at detection time, false triggers and what happens when the supervisor itself dies.

Foundations: sections 12.2 and 12.3 (IWDG and WWDG, the window on the F407), the DS8626 datasheet (LSI frequency), the stm32f407xx.h header from the Keil pack, the FreeRTOS 10.5.1 kernel from the CMSIS-FreeRTOS pack — the ARM_CM4F port for the F407 and the POSIX port for the PC. The code is built with armclang for the F407; the same supervisor task code is run on the real FreeRTOS scheduler on a PC with a model of both watchdogs and with injected failures.

Terms used in the documentation

Term Meaning
watchdog task, supervisor the task that checks all the others
check-in, heartbeat an "I am alive" mark
deadline how long a task is allowed to stay silent
independent watchdog (IWDG) the independent watchdog — resets the chip
window watchdog (WWDG) the window watchdog — watches the rhythm
early wakeup interrupt (EWI) an early WWDG signal one tick before the reset
starvation a task is not given the processor
false positive a trigger without a real fault
debug freeze (DBGMCU_APB1_FZ) stopping the watchdogs while debugging

33а.1 The scheme: a supervisor task and two hardware watchdogs

After its useful work each task checks in: it writes the current tick into its own slot. Each has its own deadline — how long it is allowed to stay silent. The supervisor task wakes up every 45 ms and for each task computes how long it has been silent.

Differences from 29.7: instead of bits — the time of the last check-in and its own deadline for each task, so you can see who went silent and for how long; instead of the timer daemon — a dedicated task with the highest priority; there is a second hardware watchdog for the supervisor itself.

The sensor task with a check-in every 10 ms hangs at millisecond 1000; the supervisor task feeds the WWDG every 45 ms, and the IWDG while everyone is within the deadline; at ms 1125 sensor has been silent for 125 ms with a deadline of 100 — a snapshot, the IWDG is no longer fed; the last feed is at ms 1080, the IWDG reset at ms 1330


33а.2 The code: check-in, deadline and check

A check-in is a single write of the 32-bit tick; on Cortex-M it is atomic, and neither a mutex nor disabling interrupts is needed. The check is in the supervisor task (taskwd.c):

void twd_task(void *arg)
{
    (void)arg;
    TickType_t wake = xTaskGetTickCount();
    for (;;) {
        vTaskDelayUntil(&wake, pdMS_TO_TICKS(TWD_PERIOD_MS));
        hw_wwdg_feed();                                    /* сторож жив и в ритме */
        TickType_t now = xTaskGetTickCount();
        int late = -1;
        TickType_t worst = 0;
        for (int i = 0; i < nslot; i++) {
            TickType_t silent = now - slot[i].last;        /* беззнаковая разность переживает переполнение тика */
            if ((int32_t)(now - slot[i].quiet_until) < 0)
                continue;                                  /* объявил долгую работу */
            if (silent > slot[i].deadline && silent - slot[i].deadline >= worst) {
                worst = silent - slot[i].deadline;         /* кто просрочил больше всех — тот и завис первым */
                late = i;
            }
        }
        if (late >= 0 && tripped < 0) {
            tripped = late;
            twd_snapshot(late, slot[late].name, (uint32_t)((now - slot[late].last) * portTICK_PERIOD_MS),
                         (uint32_t)(now * portTICK_PERIOD_MS));
        }
        if (tripped < 0)
            hw_iwdg_feed();                                /* все в срок — только тогда */
    }
}

Three details. vTaskDelayUntil keeps a steady 45 ms rhythm — the WWDG window depends on it. The culprit is the one who is most overdue: when one task is stuck in a loop, the tasks below it in priority also go silent, but later. The tripped flag latches: once the supervisor has named a culprit, the IWDG is no longer fed, even if the task suddenly checks in.

Deadlines are set at registration (app33b.c): the sensor polling task runs every 10 ms and gets 100 ms, the communication task waits for a byte no longer than 200 ms and gets 500 ms, the log is written every 250 ms and gets 1000 ms.

    id_sensor = twd_register("sensor", 100);
    id_comm = twd_register("comm", 500);
    id_log = twd_register("logger", 1000);

33а.3 🔧 F407: IWDG at 250 ms, WWDG on the supervisor's rhythm

Setting up both watchdogs (wd_f407.c) uses the same registers as in 12.2 and 12.3, but different numbers:

void wd_hw_start(void)
{
    DBGMCU->APB1FZ |= DBGMCU_APB1_FZ_DBG_IWDG_STOP | DBGMCU_APB1_FZ_DBG_WWDG_STOP;
    IWDG->KR = IWDG_KEY_ENABLE;
    IWDG->KR = IWDG_KEY_UNLOCK;
    IWDG->PR = IWDG_PR_PR_0;                    /* 001b -> делитель 8 */
    IWDG->RLR = 999u;
    while (IWDG->SR) { }
    IWDG->KR = IWDG_KEY_RELOAD;

    RCC->APB1ENR |= RCC_APB1ENR_WWDGEN;
    WWDG->CFR = WWDG_CFR_EWI | WWDG_CFR_WDGTB | WWDG_W;  /* ранний сигнал за тик до сброса */
    WWDG->CR = WWDG_CR_WDGA | WWDG_T0;
    NVIC_SetPriority(WWDG_IRQn, 0);
    NVIC_EnableIRQ(WWDG_IRQn);
}
Watchdog Setting Time
IWDG LSI, prescaler 8, RLR 999 250 ms at 32 kHz; 470.6 ms at 17 kHz and 170.2 ms at 47 kHz
WWDG PCLK1 42 MHz, WDGTB = 3, window 0x50, start 0x7F tick 780.19 µs; feed from 36.67 to 49.93 ms
supervisor task vTaskDelayUntil every 45 ms — inside the WWDG window

The F407 LSI frequency according to DS8626 is from 17 to 47 kHz with a typical 32. The order of magnitude holds at any of them: the supervisor feeds every 45 ms, the WWDG resets after 49.93 ms, the shortest IWDG is 170.2 ms. The conditions are checked by static assertions in wd_f407.c: prescaler 8, 250 ms at 32 kHz, 45 ms inside the window at 42 MHz. In the armclang listing these are the writes PR = 1 and RLR = 0x3E7 at addresses 0x40003004 and 0x40003008, CFR = 0x3D0 and CR = 0xFF for the WWDG (0x40002C00).

The IWDG needs to be shorter than in 12.2 (there 2 s on F1 and 2.5 s on F4): it no longer catches a hang by itself, but only carries out the supervisor task's verdict — the shorter, the faster the reset. The FreeRTOS kernel for the F407 is configured for 168 MHz, software timers are disabled: the supervisor is our own and alone at the highest priority (PRIO_WD = configMAX_PRIORITIES − 1 is checked by a static assertion).


33а.4 Experiment: a hang on the real scheduler

The same taskwd.c and app33b.c are built with gcc with the POSIX port of kernel 10.5.1: tasks are threads, the 1 ms tick is a timer signal. The tick hook plays the role of the hardware: the IWDG counts 250 ms from the feed, the WWDG — the time from the feed with a window of 36.67…49.93 ms. Failure points are built into the tasks: at the right moment a task goes to sleep forever (vTaskSuspend(NULL), like waiting for something that will never come) or enters an infinite loop.

What happened at ms 1000 Snapshot Silent for Reset
no failure, 4000 ms — — none; 88 feeds each of IWDG and WWDG
sensor hung (deadline 100 ms) at ms 1125: sensor 125 ms IWDG at ms 1330
comm hung (deadline 500 ms) at ms 1530: comm 530 ms IWDG at ms 1735
logger hung (deadline 1000 ms) at ms 2025: logger 1025 ms IWDG at ms 2230
sensor in an infinite loop at ms 1125: sensor 125 ms IWDG at ms 1330

A task stuck in a loop at its own priority prevents comm and logger from running, but the supervisor is above everyone — it keeps going round and names the culprit correctly: sensor.

Detection time. A task that went silent at moment t will be noticed on the first round of the supervisor after t + deadline, that is, after deadline…deadline + 45 ms. The reset comes 250 − 45 ms after detection: the IWDG was last fed one round earlier. For sensor this is 101…145 ms to the snapshot and 306…350 ms to the reset. Nine runs with a hang at ms 1000, 1010, …, 1080:

Hang, ms 1000 1010 1020 1030 1040 1050 1060 1070 1080
detected after, ms 125 115 105 140 130 120 110 145 135
reset after, ms 330 320 310 345 335 325 315 350 340

All are within the calculated bounds: from 105 to 145 ms to the snapshot and from 310 to 350 ms to the reset. With the IWDG at the edges of the LSI frequency, the reset after the same hang at ms 1000 comes at ms 250 with 170 ms and at ms 551 with 471 ms.


33а.5 False triggers and long legitimate work

The supervisor cannot tell a hung task from a busy one. In the experiment the log task gets 1500 ms of legitimate work — this is how a flash memory sector is erased (on the F407 a 128 KB sector takes up to 4 s to erase, section 48б). Its deadline is 1000 ms:

logger busy for 1500 ms from ms 1000 Result
silently snapshot at ms 2025: "logger silent for 1025 ms", IWDG reset at ms 2230 — a false one
twd_expect(id_log, 2000) before the work logger became free at ms 2500, no reset within 4000 ms

twd_expect tells the supervisor "do not judge me for the next 2 s":

void twd_expect(int id, uint32_t ms)
{
    slot[id].quiet_until = xTaskGetTickCount() + pdMS_TO_TICKS(ms);
    slot[id].last = xTaskGetTickCount();
}

Hence the rule for deadlines. A task's deadline must be greater than its worst period plus the worst time by which tasks of higher priority and interrupts can delay it. A task that waits on a queue without a time limit will not be able to check in — for the supervisor this is the same as a hang (the comm row in 33а.4). That is why the communication task waits for a byte no longer than 200 ms and checks in on a timeout as well. A task that legitimately sleeps for a long time is either excluded from the check or given a deadline longer than its sleep.


33а.6 Before the reset: the snapshot and the early WWDG signal

Between the snapshot and the reset there are 205 ms with the nominal LSI (125 ms at 47 kHz, 426 ms at 17 kHz): time for the record of the culprit to land in memory that survives a reset. The record goes into the .noinit section (like the crash record in section 35в), and the valid flag is written last:

void twd_snapshot(int id, const char *name, uint32_t silent_ms, uint32_t now_ms)
{
    wd_note.task = id;
    wd_note.silent_ms = silent_ms;
    wd_note.now_ms = now_ms;
    strncpy(wd_note.name, name, sizeof wd_note.name - 1u);
    wd_note.name[sizeof wd_note.name - 1u] = '\0';
    wd_note.magic = WD_MAGIC;                   /* признак — последним: запись целиком или никак */
}

After startup wd_last_reset looks at the IWDGRSTF and WWDGRSTF flags in RCC->CSR (section 12.4) and, if there was a watchdog reset, returns the record. A record with a CRC, a build number and the core registers is already section 35в.

If the supervisor itself was late, there is no one to write. Here EWI helps: one tick before the reset (780 µs on the F407) the WWDG raises an interrupt, and its handler writes "-1, wd" — "the supervisor did not make it", if there is no record yet. You must not feed the WWDG from this interrupt (12.7): the interrupt is alive even when the supervisor is dead.


33а.7 Pitfalls

A supervisor with low priority. The supervisor task at priority 1, sensor at 3 enters an infinite loop: the supervisor does not get the processor, in the experiment — a WWDG "late" reset at ms 1040, and there is no snapshot. The chip is restarted, but who is to blame is unknown. The supervisor must be above all tasks.

The supervisor itself died. The supervisor task was stopped at ms 1000: the WWDG reset the chip at ms 1040, 50 ms after the last feed at ms 990. Without the WWDG only the IWDG would remain — after 250 ms and also without a name. That is why the WWDG is fed from the supervisor, and the EWI writes "the supervisor did not make it".

Someone else feeds the IWDG. The idle hook, a timer, an interrupt — and the supervisor becomes pointless (29.7, 12.7). Only the supervisor task feeds the IWDG.

A false reset while writing to flash memory. A long legitimate operation is longer than the task's deadline — declare it with twd_expect or raise the deadline.

A task waits without a timeout. portMAX_DELAY on a queue — and a quiet communication channel looks like a hang. The wait must be shorter than the deadline.

A reset during debugging. You stopped at a breakpoint — the watchdogs keep counting. DBGMCU_APB1_FZ_DBG_IWDG_STOP and …_WWDG_STOP at the beginning of wd_hw_start.

A deadline shorter than the supervisor period. A deadline of 30 ms with a 45 ms round does not speed anything up: detection still happens no earlier than the next round. There is no point in deadlines shorter than the supervisor period.

The WWDG window at the wrong bus frequency. 45 ms falls inside the window at 42 MHz; at a different PCLK1 the window is different (12.3) — recalculate it and keep the assertion.


33а.8 Summary

Full sources

taskwd.c — the supervisor task.

/* 33а: задача-сторож. Один и тот же файл собирается armclang для F407 и gcc с портом POSIX. */
#include "taskwd.h"

static twd_slot_t slot[TWD_MAX];
static int nslot;
static volatile int tripped = -1;          /* кто первым пропустил срок; после этого IWDG не кормим */

int twd_register(const char *name, uint32_t deadline_ms)
{
    int id = nslot++;
    configASSERT(id < TWD_MAX);
    slot[id].name = name;
    slot[id].deadline = pdMS_TO_TICKS(deadline_ms);
    slot[id].last = xTaskGetTickCount();
    slot[id].quiet_until = slot[id].last;
    return id;
}

void twd_alive(int id)
{
    slot[id].last = xTaskGetTickCount();
}

void twd_expect(int id, uint32_t ms)
{
    slot[id].quiet_until = xTaskGetTickCount() + pdMS_TO_TICKS(ms);
    slot[id].last = xTaskGetTickCount();
}

void twd_task(void *arg)
{
    (void)arg;
    TickType_t wake = xTaskGetTickCount();
    for (;;) {
        vTaskDelayUntil(&wake, pdMS_TO_TICKS(TWD_PERIOD_MS));
        hw_wwdg_feed();                                    /* сторож жив и в ритме */
        TickType_t now = xTaskGetTickCount();
        int late = -1;
        TickType_t worst = 0;
        for (int i = 0; i < nslot; i++) {
            TickType_t silent = now - slot[i].last;        /* беззнаковая разность переживает переполнение тика */
            if ((int32_t)(now - slot[i].quiet_until) < 0)
                continue;                                  /* объявил долгую работу */
            if (silent > slot[i].deadline && silent - slot[i].deadline >= worst) {
                worst = silent - slot[i].deadline;         /* кто просрочил больше всех — тот и завис первым */
                late = i;
            }
        }
        if (late >= 0 && tripped < 0) {
            tripped = late;
            twd_snapshot(late, slot[late].name, (uint32_t)((now - slot[late].last) * portTICK_PERIOD_MS),
                         (uint32_t)(now * portTICK_PERIOD_MS));
        }
        if (tripped < 0)
            hw_iwdg_feed();                                /* все в срок — только тогда */
    }
}

app33b.c — the three device tasks and their registration with the supervisor.

/* 33а: три задачи прибора и задача-сторож. Сроки молчания — с запасом над худшим периодом задачи.
   С -DTWD_MODEL (прогон на компьютере) в задачи встроены точки отказа из fault33b.h. */
#include "taskwd.h"
#include "queue.h"
#ifdef TWD_MODEL
#include "fault33b.h"
#else
#define FAULT(who) do { } while (0)
#endif

enum { PRIO_LOG = 1, PRIO_COMM = 2, PRIO_SENSOR = 3, PRIO_WD = 4 };
_Static_assert(PRIO_WD == configMAX_PRIORITIES - 1, "сторож — высший приоритет среди задач");

int id_sensor, id_comm, id_log;
QueueHandle_t rx_queue;                    /* байты из прерывания UART */
TaskHandle_t wd_handle;

static void sensor_task(void *arg)         /* опрос датчика раз в 10 мс */
{
    (void)arg;
    TickType_t wake = xTaskGetTickCount();
    for (;;) {
        vTaskDelayUntil(&wake, pdMS_TO_TICKS(10));
        /* ... отсчёт ... */
        twd_alive(id_sensor);
        FAULT(0);
    }
}

static void comm_task(void *arg)           /* ждёт байт не дольше 200 мс — иначе отметиться не сможет */
{
    (void)arg;
    uint8_t b;
    for (;;) {
        if (xQueueReceive(rx_queue, &b, pdMS_TO_TICKS(200)) == pdTRUE) {
            /* ... разбор ... */
        }
        twd_alive(id_comm);
        FAULT(1);
    }
}

static void log_task(void *arg)            /* запись журнала раз в 250 мс */
{
    (void)arg;
    for (;;) {
        vTaskDelay(pdMS_TO_TICKS(250));
        /* ... запись ... */
        twd_alive(id_log);
        FAULT(2);
    }
}

void app_start(int wd_prio)
{
    rx_queue = xQueueCreate(16, 1);
    id_sensor = twd_register("sensor", 100);
    id_comm = twd_register("comm", 500);
    id_log = twd_register("logger", 1000);
    xTaskCreate(sensor_task, "sensor", configMINIMAL_STACK_SIZE, NULL, PRIO_SENSOR, NULL);
    xTaskCreate(comm_task, "comm", configMINIMAL_STACK_SIZE, NULL, PRIO_COMM, NULL);
    xTaskCreate(log_task, "logger", configMINIMAL_STACK_SIZE, NULL, PRIO_LOG, NULL);
    xTaskCreate(twd_task, "wd", configMINIMAL_STACK_SIZE, NULL, wd_prio, &wd_handle);
}

int app_wd_prio(void) { return PRIO_WD; }

wd_f407.c — IWDG and WWDG on the F407 and the snapshot before the reset.

/* 33а: STM32F407 — сторожа IWDG и WWDG для задачи-сторожа и слепок перед сбросом.
   IWDG: LSI 32 кГц, делитель 8, RLR 999 -> 250 мс (LSI 17…47 кГц по DS8626: 470…170 мс).
   WWDG: PCLK1 42 МГц, WDGTB = 3, окно 0x50, пуск 0x7F -> кормить с 36,7 до 49,9 мс (раздел 12.3).
   Собирается armclang: -DSTM32F407xx -include stm32f4xx.h, ядро FreeRTOS 10.5.1 (порт ARM_CM4F). */
#include <stdint.h>
#include <string.h>
#include "taskwd.h"

#define IWDG_KEY_RELOAD 0xAAAAu
#define IWDG_KEY_ENABLE 0xCCCCu
#define IWDG_KEY_UNLOCK 0x5555u
#define WWDG_T0 0x7Fu
#define WWDG_W  0x50u
#define WD_MAGIC 0x57443341u                    /* «WD3A» */

_Static_assert(IWDG_PR_PR_0 == 1u, "PR = 001 — делитель 8");
_Static_assert((999u + 1u) * 8u * 1000u / 32000u == 250u, "IWDG: 250 мс при LSI 32 кГц");
_Static_assert(WWDG_CFR_WDGTB == 0x180u && WWDG_CFR_EWI == 0x200u && WWDG_CR_WDGA == 0x80u, "WDGTB — биты 8:7, EWI — 9, WDGA — 7");
_Static_assert(TWD_PERIOD_MS * 42000000ull > 47ull * 4096u * 8u * 1000u
               && TWD_PERIOD_MS * 42000000ull < 64ull * 4096u * 8u * 1000u, "45 мс внутри окна WWDG на 42 МГц");

typedef struct {                                /* что пережило сброс: кто и сколько молчал */
    uint32_t magic;
    int32_t task;                               /* -1: не успел сам сторож (ранний сигнал WWDG) */
    uint32_t silent_ms, now_ms;
    char name[12];
} wd_note_t;

__attribute__((section(".noinit"))) wd_note_t wd_note;   /* секция .noinit — как в разделе 35в */

void wd_hw_start(void)
{
    DBGMCU->APB1FZ |= DBGMCU_APB1_FZ_DBG_IWDG_STOP | DBGMCU_APB1_FZ_DBG_WWDG_STOP;
    IWDG->KR = IWDG_KEY_ENABLE;
    IWDG->KR = IWDG_KEY_UNLOCK;
    IWDG->PR = IWDG_PR_PR_0;                    /* 001b -> делитель 8 */
    IWDG->RLR = 999u;
    while (IWDG->SR) { }
    IWDG->KR = IWDG_KEY_RELOAD;

    RCC->APB1ENR |= RCC_APB1ENR_WWDGEN;
    WWDG->CFR = WWDG_CFR_EWI | WWDG_CFR_WDGTB | WWDG_W;  /* ранний сигнал за тик до сброса */
    WWDG->CR = WWDG_CR_WDGA | WWDG_T0;
    NVIC_SetPriority(WWDG_IRQn, 0);
    NVIC_EnableIRQ(WWDG_IRQn);
}

void hw_iwdg_feed(void) { IWDG->KR = IWDG_KEY_RELOAD; }
void hw_wwdg_feed(void) { WWDG->CR = WWDG_T0; }

void twd_snapshot(int id, const char *name, uint32_t silent_ms, uint32_t now_ms)
{
    wd_note.task = id;
    wd_note.silent_ms = silent_ms;
    wd_note.now_ms = now_ms;
    strncpy(wd_note.name, name, sizeof wd_note.name - 1u);
    wd_note.name[sizeof wd_note.name - 1u] = '\0';
    wd_note.magic = WD_MAGIC;                   /* признак — последним: запись целиком или никак */
}

void WWDG_IRQHandler(void)                      /* сторож не накормил WWDG вовремя: осталось 780 мкс */
{
    WWDG->SR = 0;
    if (wd_note.magic != WD_MAGIC)              /* своя запись важнее: задача-сторож уже назвала виновника */
        twd_snapshot(-1, "wd", TWD_PERIOD_MS, 0);
}

int wd_last_reset(wd_note_t *out)               /* после старта: был ли сброс от сторожа и что записано */
{
    uint32_t csr = RCC->CSR;
    RCC->CSR |= RCC_CSR_RMVF;
    int wd = (csr & (RCC_CSR_IWDGRSTF | RCC_CSR_WWDGRSTF)) != 0;
    if (wd && wd_note.magic == WD_MAGIC)
        *out = wd_note;
    wd_note.magic = 0;
    return wd;
}

Next: section 33б "FreeRTOS tracing: trace macros and a task timeline" — how to record task switches into a ring and see inversion, jitter and starvation on the diagram.

Open in the interactive handbook