Embedded HandbookПриложения
Все разделы › IV. FreeRTOS
STM32F407 · уровень средний · 17 мин чтения
Открыть в интерактивном справочнике

33а. Сторожевой таймер для нескольких задач: кто завис и как это заметить

В разделе 12 сторож один на всю программу: её единственный цикл кормит IWDG, а WWDG следит за ритмом. В разделе 29.7 задачи ставят биты в группе событий, и программный таймер кормит сторож, только если собраны все. Там же показано, что кормить «раз демон жив» нельзя. Здесь следующий шаг: отдельная задача-сторож со своим сроком для каждой задачи — она называет, кто именно замолчал, пишет это в память, переживающую сброс, и сама находится под присмотром WWDG. Разобраны время обнаружения, ложные срабатывания и то, что бывает, когда умирает сам сторож.

Опоры: разделы 12.2 и 12.3 (IWDG и WWDG, окно на F407), даташит DS8626 (частота LSI), заголовок stm32f407xx.h из пака Keil, ядро FreeRTOS 10.5.1 из пака CMSIS-FreeRTOS — порт ARM_CM4F для F407 и порт POSIX для компьютера. Код собран armclang для F407; тот же код задачи-сторожа прогнан на настоящем планировщике FreeRTOS на компьютере с моделью обоих сторожей и с заданными отказами.

Как это называется в документации

Оригинал Перевод
watchdog task, supervisor задача-сторож
check-in, heartbeat отметка «жив»
deadline срок: сколько задача вправе молчать
independent watchdog (IWDG) независимый сторож — сбрасывает камень
window watchdog (WWDG) оконный сторож — следит за ритмом
early wakeup interrupt (EWI) ранний сигнал WWDG за тик до сброса
starvation голодание: задаче не дают процессор
false positive ложное срабатывание
debug freeze (DBGMCU_APB1_FZ) остановка сторожей на время отладки

33а.1 Схема: задача-сторож и два аппаратных сторожа

Каждая задача после полезной работы отмечается: записывает в свою ячейку текущий тик. У каждой свой срок — сколько она вправе молчать. Задача-сторож просыпается раз в 45 мс и для каждой считает, сколько та молчит.

Отличия от 29.7: вместо битов — время последней отметки и свой срок у каждой задачи, поэтому видно, кто замолчал и на сколько; вместо демона таймеров — своя задача с высшим приоритетом; есть второй аппаратный сторож на самого сторожа.

Задача sensor с отметкой раз в 10 мс зависла на 1000-й миллисекунде; задача-сторож раз в 45 мс кормит WWDG, а IWDG — пока все в срок; на 1125-й мс sensor молчит 125 мс при сроке 100 — слепок, IWDG больше не кормят; последнее кормление на 1080-й, сброс IWDG на 1330-й


33а.2 Код: отметка, срок и проверка

Отметка — одна запись 32-битного тика, на Cortex-M она атомарна, и ни мьютекс, ни запрет прерываний не нужны. Проверка — в задаче-стороже (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();                                /* все в срок — только тогда */
    }
}

Три детали. vTaskDelayUntil держит ровный ритм 45 мс — от него зависит окно WWDG. Виновником считается тот, кто просрочил больше всех: когда одна задача зациклилась, задачи ниже её по приоритету тоже замолкают, но позже. Флаг tripped защёлкивается: раз сторож назвал виновника, IWDG уже не кормится, даже если задача вдруг отмечется.

Сроки задаются при регистрации (app33b.c): задача опроса датчика работает раз в 10 мс и получает 100 мс, задача связи ждёт байт не дольше 200 мс и получает 500 мс, журнал пишется раз в 250 мс и получает 1000 мс.

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

33а.3 🔧 F407: IWDG на 250 мс, WWDG на ритм сторожа

Настройка обоих сторожей (wd_f407.c) — те же регистры, что в 12.2 и 12.3, но другие числа:

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);
}
Сторож Настройка Время
IWDG LSI, делитель 8, RLR 999 250 мс при 32 кГц; 470,6 мс при 17 кГц и 170,2 мс при 47 кГц
WWDG PCLK1 42 МГц, WDGTB = 3, окно 0x50, пуск 0x7F тик 780,19 мкс; кормить с 36,67 до 49,93 мс
задача-сторож vTaskDelayUntil раз в 45 мс — внутри окна WWDG

Частота LSI у F407 по DS8626 — от 17 до 47 кГц при типовых 32. Порядок величин держится при любой: сторож кормит раз в 45 мс, WWDG сбрасывает через 49,93 мс, самый короткий IWDG — 170,2 мс. Условия проверяют статические утверждения в wd_f407.c: делитель 8, 250 мс на 32 кГц, 45 мс внутри окна на 42 МГц. В листинге armclang это записи PR = 1 и RLR = 0x3E7 по адресам 0x40003004 и 0x40003008, CFR = 0x3D0 и CR = 0xFF у WWDG (0x40002C00).

IWDG нужен короче, чем в 12.2 (там 2 с на F1 и 2,5 с на F4): он теперь не ловит зависание сам, а только исполняет приговор задачи-сторожа — чем короче, тем быстрее сброс. Ядро FreeRTOS для F407 настроено на 168 МГц, программные таймеры выключены: сторож свой и один на высшем приоритете (PRIO_WD = configMAX_PRIORITIES − 1 проверено статическим утверждением).


33а.4 Опыт: зависание на настоящем планировщике

Тот же taskwd.c и app33b.c собраны gcc с портом POSIX ядра 10.5.1: задачи — потоки, тик 1 мс — сигнал таймера. Хук тика играет роль железа: IWDG считает 250 мс от кормления, WWDG — время от кормления с окном 36,67…49,93 мс. В задачи встроены точки отказа: в нужный момент задача засыпает навсегда (vTaskSuspend(NULL), как ожидание того, что не придёт) или уходит в бесконечный цикл.

Что случилось на 1000-й мс Слепок Молчит Сброс
нет отказа, 4000 мс — — нет; кормлений IWDG и WWDG по 88
sensor завис (срок 100 мс) на 1125-й мс: sensor 125 мс IWDG на 1330-й
comm завис (срок 500 мс) на 1530-й мс: comm 530 мс IWDG на 1735-й
logger завис (срок 1000 мс) на 2025-й мс: logger 1025 мс IWDG на 2230-й
sensor в бесконечном цикле на 1125-й мс: sensor 125 мс IWDG на 1330-й

Задача, зациклившаяся на своём приоритете, не даёт работать comm и logger, но сторож выше всех — он продолжает обходить и называет верно: sensor.

Время обнаружения. Задача, замолчавшая в момент t, будет замечена на первом обороте сторожа после t + срок, то есть через срок…срок + 45 мс. Сброс — через 250 − 45 мс после обнаружения: последний раз IWDG кормили оборотом раньше. Для sensor это 101…145 мс до слепка и 306…350 мс до сброса. Девять прогонов с зависанием на 1000, 1010, …, 1080-й мс:

Зависание, мс 1000 1010 1020 1030 1040 1050 1060 1070 1080
замечено через, мс 125 115 105 140 130 120 110 145 135
сброс через, мс 330 320 310 345 335 325 315 350 340

Все в границах расчёта: от 105 до 145 мс до слепка и от 310 до 350 мс до сброса. С IWDG на краях частоты LSI сброс после того же зависания на 1000-й мс — на 250-й мс при 170 мс и на 551-й при 471 мс.


33а.5 Ложные срабатывания и долгая законная работа

Сторож не отличает зависшую задачу от занятой. Задача журнала в опыте получает законную работу на 1500 мс — так стирают сектор флеш-памяти (у F407 сектор 128 КБ стирается до 4 с, раздел 48б). Срок у неё 1000 мс:

logger занят 1500 мс с 1000-й мс Итог
молча слепок на 2025-й мс: «logger молчит 1025 мс», сброс IWDG на 2230-й — ложный
перед работой twd_expect(id_log, 2000) logger освободился на 2500-й мс, сброса нет за 4000 мс

twd_expect говорит сторожу «следующие 2 с меня не судить»:

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

Отсюда правило сроков. Срок задачи — больше её худшего периода плюс худшее время, на которое её могут задержать задачи выше по приоритету и прерывания. Задача, которая ждёт очередь без ограничения времени, отметиться не сможет — для сторожа это то же, что зависание (строка comm в 33а.4). Поэтому задача связи ждёт байт не дольше 200 мс и отмечается и по тайм-ауту. Задачу, которая законно спит долго, либо не включают в проверку, либо дают ей срок больше её сна.


33а.6 Перед сбросом: слепок и ранний сигнал WWDG

Между слепком и сбросом — 205 мс при номинальном LSI (125 мс при 47 кГц, 426 мс при 17 кГц): время, чтобы запись о виновнике легла в память, переживающую сброс. Запись — в секции .noinit (как запись о сбое в разделе 35в), признак пишется последним:

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;                   /* признак — последним: запись целиком или никак */
}

После старта wd_last_reset смотрит флаги IWDGRSTF и WWDGRSTF в RCC->CSR (раздел 12.4) и, если был сброс от сторожа, отдаёт запись. Запись с CRC, номером сборки и регистрами ядра — это уже раздел 35в.

Если опоздал сам сторож, записать некому. Тут выручает EWI: WWDG за тик до сброса (780 мкс на F407) даёт прерывание, и его обработчик пишет «-1, wd» — «не успел сторож», если своей записи ещё нет. Кормить WWDG из этого прерывания нельзя (12.7): прерывание живо и тогда, когда мёртв сторож.


33а.7 Грабли

Сторож с низким приоритетом. Задача-сторож на приоритете 1, sensor на 3 уходит в бесконечный цикл: сторож не получает процессор, в опыте — сброс WWDG «поздно» на 1040-й мс, и слепка нет. Камень перезапущен, но кто виноват — неизвестно. Сторож — выше всех задач.

Умер сам сторож. Задачу-сторожа остановили на 1000-й мс: WWDG сбросил камень на 1040-й, через 50 мс после последнего кормления на 990-й. Без WWDG остался бы только IWDG — через 250 мс и тоже без имени. Поэтому WWDG кормят из сторожа, а в EWI пишут «не успел сторож».

IWDG кормит кто-то ещё. Хук простоя, таймер, прерывание — и сторож теряет смысл (29.7, 12.7). IWDG кормит только задача-сторож.

Ложный сброс при записи во флеш-память. Долгая законная операция длиннее срока задачи — объявить её twd_expect или поднять срок.

Задача ждёт без тайм-аута. portMAX_DELAY в очереди — и тихий канал связи выглядит как зависание. Ожидание — короче срока.

Сброс во время отладки. Остановились на точке — сторожа продолжают счёт. DBGMCU_APB1_FZ_DBG_IWDG_STOP и …_WWDG_STOP в начале wd_hw_start.

Срок короче периода сторожа. Срок 30 мс при обороте 45 мс ничего не ускоряет: обнаружение всё равно не раньше следующего оборота. Сроки короче периода сторожа делать незачем.

Окно WWDG не на той частоте шины. 45 мс попадает в окно на 42 МГц; на другой PCLK1 окно другое (12.3) — пересчитать и оставить утверждение.


33а.8 Итоги

Полные исходники

taskwd.c — задача-сторож.

/* 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 — три задачи прибора и регистрация у сторожа.

/* 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 и WWDG на F407 и слепок перед сбросом.

/* 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;
}

Дальше: раздел 33б «Трассировка FreeRTOS: макросы trace и временная диаграмма задач» — как записать переключения задач в кольцо и увидеть на диаграмме инверсию, дрожание и голодание.

Открыть в интерактивном справочнике