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 мс и для каждой считает, сколько та молчит.
- Все в срок — сторож кормит IWDG.
- Кто-то просрочил — сторож пишет слепок «кто и сколько молчал» и больше IWDG не кормит. Сбросит камень IWDG.
- WWDG сторож кормит на каждом обороте, что бы ни случилось с задачами. WWDG следит за ритмом самого сторожа: сторож завис или опаздывает — сброс через 49,93 мс.
Отличия от 29.7: вместо битов — время последней отметки и свой срок у каждой задачи, поэтому видно, кто замолчал и на сколько; вместо демона таймеров — своя задача с высшим приоритетом; есть второй аппаратный сторож на самого сторожа.
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 Итоги
- Задачи отмечают время, задача-сторож раз в 45 мс сверяет его со сроком каждой. IWDG — только если все в срок; WWDG — на каждом обороте: он следит за самим сторожем.
- Виновник — тот, кто просрочил больше всех: при зацикленном sensor называется sensor, а не задачи ниже его.
- Обнаружение — через срок…срок + 45 мс, сброс — ещё через 205 мс: у sensor в девяти прогонах 105…145 и 310…350 мс; при LSI 17…47 кГц IWDG от 170,2 до 470,6 мс.
- Долгую законную работу объявляют (
twd_expect) — иначе ложный сброс, как у logger на 2230-й мс. - Перед сбросом — запись «кто и сколько молчал» в
.noinit; за сторожа отвечаетEWIWWDG. - Сторож ниже задач по приоритету — сброс без имени виновника.
Полные исходники
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 и временная диаграмма задач» — как записать переключения задач в кольцо и увидеть на диаграмме инверсию, дрожание и голодание.