В материале представлен перевод github-репозитария https://github.com/TronixLab/ESP32_Watchdog?tab=readme-ov-file.
Все примеры реализованы для плат ESP32 в версии библиотеки от Espressif Systems версии 3.0.6 и проверены на контроллере AI-Thinker ESP32-CAM.
Если ваша программа по какой-либо причине зависает, самый быстрый способ восстановления - перезагрузить компьютер. Одним из наиболее известных примеров был фатальный сбой операционной системы Windows и отображение синего экрана смерти (BSoD), после чего компьютер перезагружался, чтобы избежать дальнейшего повреждения.
В отличие от настольных компьютеров, встроенные устройства должны быть автономными. Что делать, если вы не можете перезагрузить компьютер физически? Если программа зависает, не всегда возможно дождаться, пока кто-нибудь eё перезагрузит. Некоторые встроенные устройства, такие как космические зонды, просто недоступны для людей, и ручной сброс невозможен; они могут стать необратимо отключенными, если не смогут автономно восстанавливаться после сбоев.
В таких ситуациях сторожевой таймер Watchdog почти единственный способ спасти положение.
Сторожевой таймер (WDT) - это аппаратный или программный таймер, который может использоваться для автоматического обнаружения программных аномалий и автоматической генерации сброса системы, если основная программа пренебрегает его периодическим обслуживанием.
Он часто используется для автоматического сброса встроенного устройства, зависшего из-за программного или аппаратного сбоя.
Сторожевой таймер основан на счетчике, который ведет обратный отсчет от некоторого начального значения до нуля. Встроенное программное обеспечение выбирает начальное значение счетчика и периодически перезапускает его. Если счетчик когда-либо достигает нуля до того, как программное обеспечение перезапустит его, предполагается, что программное обеспечение неисправно, и устанавливается сигнал сброса процессора.
Процесс перезапуска счетчика (сброса) сторожевого таймера иногда называют словосочетанием “пинать собаку”. Подходящей визуальной метафорой является изображение человека, на которого нападает злобная собака. Если он продолжает пинать собаку, она никогда не сможет его укусить. Но он должен продолжать пинать собаку через равные промежутки времени, чтобы избежать укуса.
Аналогично, программное обеспечение должно регулярно перезапускать (сбрасывать) сторожевой таймер, иначе возникает риск перезапуска. Другой термин для обозначения “пинка” - это “подача сторожевого таймера”. На сторожевой таймер непрерывно подается начальное значение, так что значение таймера никогда не может достичь нуля.

На рис.1 показано типичное расположение сторожевого таймера как внешнего блока по отношению к процессору. Однако он также может быть встроен в тот же чип, что и центральный процессор. Это делается во многих микроконтроллерах. В любом случае выходной сигнал сторожевого таймера напрямую связан с сигналом сброса процессора.
/** Arduino-Esp32-CAM *** ex3-0-6-0-Watchdog.ino ***
*
* Учебный пример таймера сторожевого механизма
* для плат ESP32 в версии библиотеки от Espressif Systems версии 3.0.6
* (на контроллере AI-Thinker ESP32-CAM)
*
* v1.1, 12.10.2024 Автор: Труфанов В.Е.
* Copyright © 2024 tve Дата создания: 12.10.2024
*
* Таймер сторожевого механизма в ESP32 board >3.0x сильно отличается от предыдущей
* реализации. Скетч представляет пример его реализации в Arduino IDE.
*
* Документация последней версии:
* https://docs.espressif.com/projects/esp-idf/en/v5.3.1/esp32/api-reference/system/wdts.html
*
* WDT - сторожевой таймер
* IWDT - сторожевой таймер прерываний
* TWDT - сторожевой таймер задач
*
* Ключевой момент — установить задержку минимум в 1 мс после выполнения
* esp_task_wdt_reset:
*
* esp_task_wdt_reset();
* delay(1);
*
* Замечания:
* - время ожидания установлено на 25 секунд
* - в цикле 20 секунд. Сброс выполняется в течение первых 10 секунд, затем в течение 10 секунд сброс не выполняется.
* - после 60 секунд без сброса система "выходит из строя" и перезапускается через 75 секунд.
**/
#include <esp_task_wdt.h>
#define WDT_TIMEOUT 25 // тайм-аут в секундах
esp_err_t ESP32_ERROR; // возвращенное значение при инициализации TWDT
int i = 0; // счетчик циклов
int last = millis(); // текущее время (уходящее в прошлое)
// ****************************************************************************
// * Выполнить общие настройки и настройки сторожевого механизма *
// ****************************************************************************
void setup()
{
Serial.begin(115200);
delay(100);
Serial.println("Конфигурируется TWDT ...");
Serial.print("Устанавливается Watchdog Timeout (в сек): ");
Serial.println(WDT_TIMEOUT);
// Отменяем подписку на незанятые задачи и деинициализируем таймер отслеживания задач TWDT
// (эта функция деинициализирует TWDT и отменяет подписку на любые незавершенные задачи.
// Вызов этой функции, когда другие задачи все еще подписаны на TWDT,
// или когда WDT уже деинициализирован, приведет к возвращению кода ошибки.
// Esp_task_wdt_deinit() не должна вызываться несколькими задачами одновременно.
// Возврат: ESP_OK: TWDT успешно деинициализирован, другое: не удалось деинициализировать TWDT)
esp_task_wdt_deinit();
// Конфигурируем структуру таймера контроля задач (TWDT):
// uint32_t timeout_ms - длительность тайм-аута TWDT в миллисекундах
// uint32_t idle_core_mask - битовая маска ядер контроллераа,
// простой которых должен быть подписан при инициализации, где 1 << i
// означает, что задача простоя ядра i будет отслеживаться TWDT
// bool trigger_panic - вызов паники (перезагрузки) при наступлении тайм-аута
esp_task_wdt_config_t wdt_config =
{
.timeout_ms = WDT_TIMEOUT * 1000, // длительность тайм-аута в мс
.idle_core_mask = (1 << portNUM_PROCESSORS) - 1, // битовая маска для всех ядер
.trigger_panic = true // включить перезагрузку ESP32
};
// Инициализируем таймер контроля задач (TWDT)
// (эта функция настраивает и инициализирует TWDT - подписывает незанятые задачи,
// если она настроена для этого. Для других задач пользователи могут подписаться
// с помощью esp_task_wdt_add() или esp_task_wdt_add_user(). Эта функция не
// запустит таймер, если ни одна задача еще не была зарегистрирована.
// Примечание: esp_task_wdt_init() следует вызывать только после запуска планировщика.
// Более того, она не должна вызываться несколькими задачами одновременно.
// Входной параметр конфигурационная структура, на возврате ESP_OK - инициализация
// прошла успешно, ESP_ERR_INVALID_STATE - уже инициализировано, другое -
// не удалось инициализировать TWDT)
ESP32_ERROR = esp_task_wdt_init(&wdt_config);
// Выводим текст ошибки (причину предыдущей перезагрузки) по её коду
Serial.println("Причина предыдущей перезагрузки: " + String(esp_err_to_name(ESP32_ERROR)));
// Подписываем текущую задачу под наблюдение TWDT
// (эта функция подписывает задачу на TWDT. Каждая подписанная задача должна периодически
// вызывать esp_task_wdt_reset(), чтобы предотвратить истечение срока ожидания TWDT.
// Невыполнение этого требования приведет к таймауту TWDT.
// Параметр task_handle - дескриптор задачи. Устанавливается в NULL, чтобы подписать
// текущую запущенную задачу на TWDT. Возврат: ESP_OK - успешно зарегистрирована
// задача в TWDT, другое - не удалось выполнить подписку на задачу)
esp_task_wdt_add(NULL);
}
// ****************************************************************************
// * Циклически отработать заданную задачу *
// ****************************************************************************
void loop()
{
// Запускаем задачу каждую секунду
if (millis() - last >= 1000)
{
last = millis();
Serial.print(i);
Serial.print("s: ");
i++;
// Выполняем сброс сторожевого таймера каждые 10 секунд первой минуты
// (после 60 секунд прекращаем сброс окончательно и, таким образом, инициируем
// перезагрузку контроллера по истечении тайм-аута - на 75 секунде)
if (i % 20 <= 10 && i<60)
{
Serial.println("Сбрасывается сторожевой таймер задач TWDT ...");
// Сбрасываем таймер контроля задач (TWDT) от имени текущей задачи
// (каждая подписанная задача должна периодически вызывать эту функцию,
// чтобы предотвратить истечение времени ожидания TWDT. Если одной или
// нескольким подписанным задачам не удастся сбросить TWDT от своего имени,
// произойдет тайм-аут TWDT. Возврат: ESP_OK - успешно сброшено TWDT от
// имени текущей задачи, другое - не удалось выполнить сброс)
esp_task_wdt_reset();
// Делаем задержку в, как минимум, 1 мсек
delay(1);
// Не инициируем сброса сторожевого таймера следующие 10 сек
if (i % 20 == 10)
{
Serial.println("Отключен сброс сторожевого таймера!");
}
}
else Serial.println(" ");
}
}
// ************************************************* ex3-0-6-0-Watchdog.ino ***
/** Arduino-Esp32-CAM *** ex3-0-6-1-BasicWDT.ino ***
*
* Базовый сторожевой таймер
* для плат ESP32 в версии библиотеки от Espressif Systems версии 3.0.6
* (на контроллере AI-Thinker ESP32-CAM)
*
* v1.1, 12.10.2024 Автор: Труфанов В.Е.
* Copyright © 2024 tve Дата создания: 12.10.2024
*
* Ключевой момент — установить задержку минимум в 1 мс после выполнения
* esp_task_wdt_reset:
**/
#include <esp_task_wdt.h>
#define WDT_TIMEOUT 3 // тайм-аут в секундах
esp_err_t ESP32_ERROR; // возвращенное значение при инициализации TWDT
void setup()
{
Serial.begin(115200);
Serial.println("Setup started.");
delay(2000);
// Отменяем подписку на незанятые задачи и деинициализируем таймер отслеживания задач TWDT
esp_task_wdt_deinit();
// Конфигурируем структуру таймера контроля задач (TWDT):
esp_task_wdt_config_t wdt_config =
{
.timeout_ms = WDT_TIMEOUT * 1000, // длительность тайм-аута в мс
.idle_core_mask = (1 << portNUM_PROCESSORS) - 1, // битовая маска для всех ядер
.trigger_panic = true // включить перезагрузку ESP32
};
// Инициализируем таймер контроля задач (TWDT)
ESP32_ERROR = esp_task_wdt_init(&wdt_config);
// Подписываем текущую задачу под наблюдение TWDT
esp_task_wdt_add(NULL);
}
void loop()
{
Serial.println("LOOP started.");
// Сбрасывем сторожевой таймер 10 секунд
for (int i = 0; i <= 10; i++)
{
Serial.print("Task: ");
Serial.println(i);
delay(1000);
// Сбрасываем таймер контроля задач (TWDT) от имени текущей задачи
// (kick the dog - "пинаем собаку")
esp_task_wdt_reset();
// Делаем задержку в, как минимум, 1 мсек
delay(1);
}
// Уходим в бесконечный цикл без сброса TWDT
while (1)
{
Serial.println("MCU hang event!!!");
delay(500);
}
}
// ************************************************** ex3-0-6-1-BasicWDT.ino ***
Выход из строя микроконтроллера - MCU, как неисправность железа, так и неучтеные ситуации в программном коде могут привести к полной остановке работы. Это может доставить неудобства пользователям или создать угрозу функциональной безопасности в критически важных приложениях.
Механизм безопасности чрезвычайно важен. Надежная встраиваемая система требует совместных усилий разработчика оборудования и программиста встроенного программного обеспечения. Существуют аппаратные и программные факторы, из-за которых MCU может отказать:
!!! В следующем примере показано зависание - явление, при котором две задачи находятся в заблокированном состоянии, одновременно ожидая доступа к ресурсам, которые удерживаются одной из них.
!!! Во FreeRTOS нет решения проблемы зависания. При использовании этой операционной системы реального времени такого зависания можно избежать только путём тщательного программирования приложений. Следует составлять задачи таким образом, чтобы не возникало взаимоблокировки.
Замечание от 2024-11-04: на сегодня, в версии библиотеки от Espressif Systems версии 3.0.6 и по итогам проверки на контроллере AI-Thinker ESP32-CAM проблема взаимоблокировки во FreeRTOS разрешается, а сторожевой таймер при необходимости перезагружает контроллер!
/** Arduino-Esp32-CAM *** ex3-0-6-2-DeadLockWDT.ino ***
*
* Потенциальная взаимоблокировка
* для плат ESP32 в версии библиотеки от Espressif Systems версии 3.0.6
* (на контроллере AI-Thinker ESP32-CAM)
*
*
* v1.1, 04.11.2024 Автор: Труфанов В.Е.
* Copyright © 2024 tve Дата создания: 13.10.2024
*
* В скетче показано возможное зависание - явление, при котором две задачи
* находятся в заблокированном состоянии, одновременно ожидая доступа к
* ресурсам, которые удерживаются одной из них.
*
* Замечание от 2024-11-04: на сегодня, в версии библиотеки от Espressif
* Systems версии 3.0.6 по итогам проверки на контроллере AI-Thinker ESP32-CAM
* проблема взаимоблокировки в FreeRTOS разрешается (задачам дается возможность
* выполнить свою работу), а сторожевой таймер, при необходимомти,
* перезагружает контроллер!
**/
#include <esp_task_wdt.h>
#define WDT_TIMEOUT 10 // тайм-аут в 10 секунд
esp_err_t ESP32_ERROR; // возвращенное значение при инициализации TWDT
static SemaphoreHandle_t mutex_1;
static SemaphoreHandle_t mutex_2;
// Task A (высокий приоритет = 2)
void doTaskA(void *parameters)
{
while (1)
{
// Берем мьютекс1 на 1 тик
// (начинаем ожидание для принудительной взаимоблокировки)
xSemaphoreTake(mutex_1, portMAX_DELAY);
Serial.println("Task A взяла mutex_1 и делает паузу на 1 тик");
vTaskDelay(1/portTICK_PERIOD_MS);
// Берем мьютекс2
xSemaphoreTake(mutex_2, portMAX_DELAY);
Serial.println("Task A взяла mutex_2");
// Держим критическую секцию, защищённую двумя мьютексами
Serial.println(" == А == ВЫПОЛНЯЕТ ПРОСТУЮ РАБОТУ в 500 ТИКОВ");
vTaskDelay(500 / portTICK_PERIOD_MS);
// Возвращаем мьютексы
Serial.println("Task A возвращает мьютексы");
xSemaphoreGive(mutex_2);
xSemaphoreGive(mutex_1);
// Ждем и даем возможность запуститься другой задаче
Serial.println("Task A ждет еще 500 тиков и дает запуститься второй задаче");
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
// Task B (низкий приоритет = 1)
void doTaskB(void *parameters)
{
while (1)
{
xSemaphoreTake(mutex_2, portMAX_DELAY);
vTaskDelay(1 / portTICK_PERIOD_MS);
xSemaphoreTake(mutex_1, portMAX_DELAY);
Serial.println(" == В == ВЫПОЛНЯЕТ ПРОСТУЮ РАБОТУ в 500 ТИКОВ");
vTaskDelay(500 / portTICK_PERIOD_MS);
xSemaphoreGive(mutex_1);
xSemaphoreGive(mutex_2);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
void setup()
{
Serial.begin(115200);
// Отменяем подписку незанятых задач и деинициализируем сторожевой таймер
esp_task_wdt_deinit();
// Конфигурируем структуру таймера
esp_task_wdt_config_t wdt_config =
{
.timeout_ms = WDT_TIMEOUT * 1000, // длительность тайм-аута в мс
.idle_core_mask = (1 << portNUM_PROCESSORS) - 1, // битовая маска для всех ядер
.trigger_panic = true // включить перезагрузку ESP32
};
// Инициализируем сторожевой таймер контроля задач
ESP32_ERROR = esp_task_wdt_init(&wdt_config);
// Подписываем текущую задачу под наблюдение TWDT
esp_task_wdt_add(NULL);
// Создаем мьютексы перед запуском задач
mutex_1 = xSemaphoreCreateMutex();
mutex_2 = xSemaphoreCreateMutex();
// Создаем Task A (с высоким приоритетом) и Task B (с низким приоритетом)
xTaskCreate(doTaskA, "Task A", 1024, NULL, 2, NULL);
xTaskCreate(doTaskB, "Task B", 1024, NULL, 1, NULL);
}
void loop(){}
// ********************************************** ex3-0-6-2-DeadLockWDT.ino ***
Если “пинать собаку через равные промежутки времени, это доказывает, что программное обеспечение работает. Конечно полезно пинать собаку только в том случае, если система проходит проверку работоспособности, как показано на рис.2:
основной цикл программы выполняет некоторые полезные действия, кроме этого, загружает начальным значением (тайм-аутом) сторожевой таймер. Тайм-аут превышает максимальное время выполнения основного цикла программы.
Каждый раз, когда основной цикл выполняется, код сбрасывает сторожевой таймер («пинает» или «кормит» собаку). Если возникает ошибка и основная программа не возвращается для сброса таймера до того, как он обнулится, то генерируется прерывание для сброса процессора.
При таком использовании сторожевой таймер может обнаруживать ошибки в программе Arduino, работающей без присмотра, и предпринимать корректирующие действия.

В некоторых случаях перед тем, как запустить сторожевой таймер, следует выделить некоторые буферы или проверить состояние какого-либо компонента. Грамотное проектирование таких проверок увеличит количество ошибок, которые сможет обнаружить сторожевой таймер. Один из подходов заключается в том, чтобы очищать некоторые флаги перед запуском каждого цикла, как показано на рис.3.

Каждый флаг устанавливается в определённой точке цикла. В конце цикла собаку пинают, но сначала проверяют флаги, чтобы убедиться, что все важные точки цикла были посещены.
В следующем скетче в основном цикле выполняются три деятельности. Каждая деятельность после своей работы устанавливает соответствующий флаг. В завершении цикла, в контрольном блоке проверяются все три флага. Если все флаги установлены, то они сбрасываются в ноль и счетчик сторожевого таймера запускается снова.
В скетч включена имитация зацикливания. Если в последовательный порт с компьютера вводится целое число 1, то запускается бесконечный цикл, который через некоторое время заставляет сторожевой таймер перезагрузить контроллер.
/** Arduino-Esp32-CAM *** ex3-0-6-3-SanityCheckSingleLoop.ino ***
*
* Проверка работоспособности в одном цикле
* для плат ESP32 в версии библиотеки от Espressif Systems версии 3.0.6
* (на контроллере AI-Thinker ESP32-CAM)
*
* В скетче в основном цикле выполняются три деятельности. Каждая деятельность
* после своей работы устанавливает соответствующий флаг. В завершении цикла,
* в контрольном блоке проверяются все три флага, если все флаги установлены,
* то они сбрасываются в ноль и счетчик сторожевого таймера запускается снова.
*
* В скетч включена имитация зацикливания. Если в последовательный порт с
* компьютера вводится целое число 1, то запускается бесконечный цикл, который
* через некоторое время заставляет сторожевой таймер перезагрузить контроллер
*
* v1.1, 04.11.2024 Автор: Труфанов В.Е.
* Copyright © 2024 tve Дата создания: 04.11.2024
*
**/
#include <esp_task_wdt.h>
#define WDT_TIMEOUT 4 // тайм-аут в 4 секунды
esp_err_t ESP32_ERROR; // возвращенное значение при инициализации TWDT
int flag[] = {0, 0, 0};
void setup()
{
Serial.begin(115200);
// Отменяем подписку незанятых задач и деинициализируем сторожевой таймер
esp_task_wdt_deinit();
// Конфигурируем структуру таймера
esp_task_wdt_config_t wdt_config =
{
.timeout_ms = WDT_TIMEOUT * 1000, // длительность тайм-аута в мс
.idle_core_mask = (1 << portNUM_PROCESSORS) - 1, // битовая маска для всех ядер
.trigger_panic = true // включить перезагрузку ESP32
};
// Инициализируем сторожевой таймер контроля задач
ESP32_ERROR = esp_task_wdt_init(&wdt_config);
// Подписываем текущую задачу под наблюдение TWDT
esp_task_wdt_add(NULL);
}
void loop()
{
Serial.println("Task 1");
delay(100);
flag[0] = 1;
Serial.println("Task 2");
delay(100);
flag[1] = 1;
Serial.println("Task 3");
delay(100);
flag[2] = 1;
// С помощью ввода в последовательный порт "1 + Enter" производится
// зацикливание процессора без сброса сторожевого таймера, что
// приводит к перезагрузке контроллера
if (Serial.available() > 0)
{
if (Serial.parseInt() == 1)
{
while (true)
{
Serial.println("MCU hang event!!!");
}
}
}
// Проверяем все флаги
if (flag[0] == 1 && flag[1] == 1 && flag[2] == 1)
{
// Так как все флаги установлены, значит все деятельности завершены
// успешно. Поэтому сбрасываем все флаги для нового контроля и
// "пинаем собаку" - сбрасываем счетчик сторожевого таймера (то есть
// запускаем отсчет счётчика заново)
flag[0] = 0;
flag[1] = 0;
flag[2] = 0;
esp_task_wdt_reset();
}
}
// ************************************ ex3-0-6-3-SanityCheckSingleLoop.ino ***
В программном коде, требующем многозадачности, как показано на рис.4 (в частности, работающем под управлением операционной системы реального времени - RTOS) для сторожевого таймера используется отдельная задача. Эта задача просыпается через регулярные промежутки времени и проверяет работоспособность всех остальных задач в системе. Если все задачи проходят проверку, запускается сторожевой таймер. Задача сторожевого таймера работает с более высоким приоритетом, чем задачи, которые она отслеживает.

Тайм-аут сторожевого таймера выбирается таким образом, чтобы он был максимальным временем, в течение которого все контролируемые задачи успевали бы выполнить полный цикл от начальной точки до конечной. Каждой задаче ставится в соответствие флаг, который может принимать два значения: TRUE или FALSE.
Позже флаг считывается и записывается монитором. Задача монитора — проснуться до истечения тайм-аута сторожевого таймера и проверить состояние каждого флага. Если все флаги содержат значения TRUE, это значит, что каждая задача получила возможность выполнить свою часть работы, и сторожевой таймер может быть запущен. Причём некоторые задачи могли выполнить несколько циклов и несколько раз установить свой флаг в TRUE, что допустимо. После запуска сторожевого таймера монитор устанавливает все флаги в FALSE. К моменту повторного запуска задачи монитора все флаги FALSE должны быть перезаписаны на TRUE.
/** Arduino-Esp32-CAM *** ex3-0-6-4-SanityCheckRTOST.ino ***
*
* Проверка работоспособности нескольких задач
* для плат ESP32 в версии библиотеки от Espressif Systems версии 3.0.6
* (на контроллере AI-Thinker ESP32-CAM)
**/
#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif
#include <esp_task_wdt.h>
int WDT_TIMEOUT = 5; // WDT Timeout in seconds
void vTask1(void *pvParameters);
void vTask2(void *pvParameters);
void vTask3(void *pvParameters);
void vTask4(void *pvParameters);
void vTaskIdle(void *pvParameters);
int flag[] = {0, 0, 0, 0};
void setup()
{
Serial.begin(115200);
xTaskCreatePinnedToCore(
vTask1, // Task function
"Task1", // Task name
1024, // Stack size
NULL, // Parameters passed to the task function
1, // Priority
NULL, // Task handle
ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(
vTask2, // Task function
"Task2", // Task name
1024, // Stack size
NULL, // Parameters passed to the task function
1, // Priority
NULL, // Task handle
ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(
vTask3, // Task function
"Task3", // Task name
1024, // Stack size
NULL, // Parameters passed to the task function
2, // Priority
NULL, // Task handle
ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(
vTask4, // Task function
"Task4", // Task name
1024, // Stack size
NULL, // Parameters passed to the task function
3, // Priority
NULL, // Task handle
ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(
vCheckFlagTask, // Task function
"CheckFlags", // Task name
1024, // Stack size
NULL, // Parameters passed to the task function
3, // Priority
NULL, // Task handle
ARDUINO_RUNNING_CORE);
}
void loop() {}
void vTask1(void* pvParameters)
{
for (;;)
{
Serial.println("Task1");
// Mimic the MCU hang event using Serial port
if (Serial.available() > 0)
{
if (Serial.parseInt() == 1)
{
while (true)
{
Serial.println("MCU hang event!!!");
}
}
}
flag[0] = 1;
vTaskDelay(100/portTICK_PERIOD_MS);
}
}
void vTask2(void* pvParameters)
{
for ( ;; )
{
Serial.println("Task2");
// Mimic the MCU hang event using Serial port
if (Serial.available() > 0)
{
if (Serial.parseInt() == 1)
{
while (true)
{
Serial.println("MCU hang event!!!");
}
}
}
flag[1] = 1;
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
void vTask3(void* pvParameters)
{
for ( ;; )
{
Serial.println("Task3");
// Mimic the MCU hang event using Serial port
if (Serial.available() > 0)
{
if (Serial.parseInt() == 1)
{
while (true)
{
Serial.println("MCU hang event!!!");
}
}
}
flag[2] = 1;
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
void vTask4(void* pvParameters)
{
for ( ;; )
{
Serial.println("Task4");
// Mimic the MCU hang event using Serial port
if (Serial.available() > 0)
{
if (Serial.parseInt() == 1)
{
while (true)
{
Serial.println("MCU hang event!!!");
}
}
}
flag[3] = 1;
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
void vCheckFlagTask(void* pvParameters)
{
for ( ;; )
{
// Check all flags if True then kick the dog
if (flag[0] == 1 && flag[1] == 1 && flag[2] == 1 && flag[3] == 1)
{
// Reset all flags
flag[0] = 0;
flag[1] = 0;
flag[2] = 0;
flag[3] = 0;
// Kick the dog
WDT_TIMEOUT = 5;
}
else
{
WDT_TIMEOUT --;
if (WDT_TIMEOUT == 0)
{
ESP.restart();
}
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
// ****************************************** ex3-0-6-4-SanityCheckRTOS.ino ***
WDT является последней линией защиты, его конструкция должна предусматривать любой возможный сбой.
Основные характеристики надёжного сторожевого устройства:
WDT должен быть независимым от центрального процессора;
WDT должен всегда, при любых условиях, за исключением, возможно, аппаратного сбоя, возвращать систему к жизни.
-------------------------------------------------------
Как вариант, некоторые WDT выдают не маскируемое прерывание (NMI) вместо сброса.
В следующем примере, сторожевой таймер - “всеобъемлющий сторожевой пес” обслуживает четыре задачи и вызывает перезагрузку контроллера, когда зацикливаются любая из задач. Сторожевой таймер реализован, как обработчик прерывания по таймеру.
Две задачи загружены на нулевое ядро контроллера, две другие на первое. Все контролируемые задачи после успешного завершения цикла выставляют свой флаг в 1. Обработчик сторожевого таймера содержит критическую секцию, в которой проверяет состояния флагов всех задач. Если все флаги говорят об успешном выполнении задач, то обработчик сбрасывает их в 0 и перезапускает сторожевой счётчик.
Искусственно любую задачу можно зациклить. Для этого в основном цикле считываются целые числа из последовательного порта, что позволяет ввести и числа 1, 2, 3, 4. Введенные числа проверяются задачами на соответствие своим номерам и, следовательно, запуску бесконечного цикла.
При зацикливании, флаг задачи сброшенный сторожевым обработчиком не устанавливается в 1, что затем вызывает перезагрузку контроллера.
/** Arduino-Esp32-CAM *** ex3-0-6-5-GreatWDT.ino ***
*
* Всеобъемлющий сторожевой пес
*
* В данном скетче сторожевой таймер, реализованный как обработчик прерывания
* по времени, обслуживает четыре задачи и вызывает перезагрузку контроллера,
* когда зацикливаются любая из задач.
*
* Две задачи загружены на нулевое ядро контроллера, две другие на первое. Все
* контролируемые задачи после успешного завершения своего цикла выставляют
* свой флаг в 1. Обработчик сторожевого таймера содержит критическую секцию,
* в которой проверяет состояния флагов всех задач. Если все флаги говорят об
* успешном выполнении задач, то обработчик сбрасывает их в 0 и перезапускает
* сторожевой счётчик.
*
* Искусственно любую задачу можно зациклить. Для этого в основном цикле
* считываются целые числа из последовательного порта, что позволяет ввести и
* числа 1, 2, 3, 4. Введенные числа проверяются задачами на соответствие своим
* номерам и, следовательно, запуску бесконечного цикла.
*
* При зацикливании, флаг задачи сброшенный сторожевым обработчиком не
* устанавливается в 1, что затем вызывает перезагрузку контроллера.
*
* v1.1, 04.11.2024 Автор: Труфанов В.Е.
* Copyright © 2024 tve Дата создания: 03.11.2024
**/
#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif
// Определяем заголовок для объекта таймера
hw_timer_t *timer = NULL;
// Инициируем спинлок критической секции в обработчике таймерного прерывания
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
// Определяем число, которое будет считываться в основном цикле
// с последовательного порта
volatile int inumber;
// Определяем задачи и их флаги
void vTask1(void *pvParameters);
void vTask2(void *pvParameters);
void vTask3(void *pvParameters);
void vTask4(void *pvParameters);
int flag[] = {0, 0, 0, 0};
// Обработка прерывания от таймера
void IRAM_ATTR onTimer()
{
portENTER_CRITICAL_ISR(&timerMux);
// Если флаги всех задач установлены в 1,
// то сбрасываем флаги задач и счетчик сторожевого таймера
if (flag[0] == 1 && flag[1] == 1 && flag[2] == 1 && flag[3] == 1)
{
// Сбрасываем флаги задач
flag[0] = 0;
flag[1] = 0;
flag[2] = 0;
flag[3] = 0;
// "Пинаем собаку" - сбрасываем счетчик сторожевого таймера
timerWrite(timer, 0);
}
// Иначе перезагружаем контроллер
else
{
ESP.restart();
}
portEXIT_CRITICAL_ISR(&timerMux);
}
// Начальная настройка: выделяем четыре задачи (две на 0 процессоре, две на 1)
// и обеспечиваем запуск прерывания от таймера периодически через 3 секунды
void setup()
{
Serial.begin(115200);
xTaskCreatePinnedToCore(
vTask1, // Task function
"Task1", // Task name
1024, // Stack size
NULL, // Parameters passed to the task function
1, // Priority
NULL, // Task handle
0); //ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(
vTask2, // Task function
"Task2", // Task name
1024, // Stack size
NULL, // Parameters passed to the task function
1, // Priority
NULL, // Task handle
0); //ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(
vTask3, // Task function
"Task3", // Task name
1024, // Stack size
NULL, // Parameters passed to the task function
2, // Priority
NULL, // Task handle
1); //ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(
vTask4, // Task function
"Task4", // Task name
1024, // Stack size
NULL, // Parameters passed to the task function
3, // Priority
NULL, // Task handle
1); //ARDUINO_RUNNING_CORE);
// Создаём объект таймера, устанавливаем его частоту отсчёта (1Mhz)
timer = timerBegin(1000000);
// Подключаем функцию обработчика прерывания от таймера - onTimer
timerAttachInterrupt(timer, &onTimer);
// Настраиваем таймер: интервал перезапуска - 3 секунды (3000000 микросекунд),
// всегда повторяем перезапуск (третий параметр = true), неограниченное число
// раз (четвертый параметр = 0)
timerAlarm(timer, 3000000, true, 0);
}
// Основной цикл: считываем с последовательного порта целое число
// (так как в зависимости от окружения за целым числом может следовать нулевое значение,
// то отсекаем 0)
void loop()
{
if (Serial.available() > 0)
{
int ii=Serial.parseInt();
if (ii>0) inumber=ii;
delay(100);
}
}
// Имитируем событие зависания процессора
void MimicMCUhangEvent(String NameTask)
{
while (true)
{
Serial.print(NameTask);
Serial.println(": зависание процессора!!!");
}
}
void vTask1(void* pvParameters)
{
for (;;)
{
Serial.print("Task1 ");
// В обычном режиме делаем паузу 500 мсек и выставляем бит задачи в 1
vTaskDelay(500/portTICK_PERIOD_MS);
flag[0] = 1;
// Имитируем зависание микроконтроллера с помощью опознанного числа,
// принятого в последовательном порту
if (inumber == 1) MimicMCUhangEvent("Task1");
}
}
void vTask2(void* pvParameters)
{
for ( ;; )
{
Serial.print("Task2 ");
vTaskDelay(500/portTICK_PERIOD_MS);
flag[1] = 1;
if (inumber == 2) MimicMCUhangEvent("Task2");
}
}
void vTask3(void* pvParameters)
{
for ( ;; )
{
Serial.print("Task3 ");
vTaskDelay(500/portTICK_PERIOD_MS);
flag[2] = 1;
if (inumber == 3) MimicMCUhangEvent("Task3");
}
}
void vTask4(void* pvParameters)
{
for ( ;; )
{
Serial.print("Task4 ");
vTaskDelay(500/portTICK_PERIOD_MS);
flag[3] = 1;
if (inumber == 4) MimicMCUhangEvent("Task4");
}
}
// ************************************************* ex3-0-6-5-GreatWDT.ino ***