Сторожевой таймер Arduino


В данном уроке рассматривается устройство и работа со сторожевым таймером, для управления которым предлагается использовать библиотеку GyverWDT (by N1gra), предоставляющую полный контроль за WDT и доступ ко всем его настройкам и режимам работы. Библиотека очень лёгкая и подходит для большинства МК AVR, т.е. в частности для всех плат Arduino на их базе.

Сторожевой таймер в МК AVR представляет собой независимый асинхронный блок, тактирующийся от отдельного внутреннего RC генератора, частота которого составляет около 128 кГц (несколько зависит от температуры и напряжения питания). Следует учесть, что изначально данный генератор спроектирован как энергоэффективный источник тактирования, и не предполагает высокой точности, отклонение частоты при производстве составляет около 10%. При желании этот генератор может тактировать и ядро AVR, в случае, если внешний кварцевый резонатор не предусмотрен, а внутренний RC генератор на 8МГц не удовлетворяет требованиям энергопотребления.

Сторожевой таймер представляет из себя простой 10-битный счетный регистр, он инкрементируется при каждом фронте тактового сигнала, приходящего из делителя. Как только значение в этом счетном регистре достигнет 1023 – произойдет тайм-аут, а сторожевой таймер, в зависимости от установленного режима, инициирует сброс МК или вызывает прерывание. Чтобы предотвратить тайм-аут, можно сбросить счет сторожевого таймера в 0, воспользовавшись короткой функцией Watchdog.reset() библиотеки GyverWDT, или ассемблерной инструкцией “WDR”.

Зачем нужен сторожевой таймер? Он позволяет автоматически перезагрузить контроллер в случае зависания программы, что может быть просто необходимо в автономных проектах. Помимо этого, библиотека GyverWDT предоставляет возможность использовать сторожевой таймер и в других целях, например для генерации прерываний с высоким приоритетом без использования основных аппаратных таймеров, что может очень пригодиться в сложных проектах, например для регулярного опроса датчиков. Не стоит забывать, что сторожевой таймер остается активным во всех, даже самых глубоких режимах сна, и может разбудить контроллер прерыванием. Полезным может оказаться и комбинированный режим сторожевого таймера, в котором он может одновременно опрашивать датчики, выступать сторожевым таймером для внешнего устройства, и следить за работой собственного ядра, чтобы в случае зависания сбросить его.

Делители частоты сторожевого таймера

Константа делителя GyverWDT Приблизительный тайм-аут Приблизительная частота тайм-аутов
WDT_PRESCALER_2 ~ 16ms ~ 62Hz
WDT_PRESCALER_4 ~ 32ms ~ 32Hz
WDT_PRESCALER_8 ~ 64ms ~ 16Hz
WDT_PRESCALER_16 ~ 0.125s ~ 8Hz
WDT_PRESCALER_32 ~ 0.25s ~ 4Hz
WDT_PRESCALER_64 ~ 0.5s ~ 2Hz
WDT_PRESCALER_128 ~ 1s ~ 1Hz
WDT_PRESCALER_256 ~ 2s ~ 0.5Hz
WDT_PRESCALER_512 ~ 4s ~ 0.25Hz
WDT_PRESCALER_1024 ~ 8s ~ 0.125Hz

Почему приблизительная? Потому что частота генератора сторожевого таймера уже при производстве имеет приличный разброс, и если на малых значениях делителя это практически не проявляется (16 мс +/- 1.6 мс), то на больших это может стать серьезной проблемой (8 с +/- 1 с).

Не стоит забывать, что RC генератор, в отличии от кварцевого резонатора, имеет небольшую зависимость частоты от температуры и напряжения питания.

Библиотека GyverWDT


Для работы с Watchdog можно использовать встроенную avr библиотеку, но у нас есть своя библиотека с более широким набором настроек: GyverWDT. Отдельная страница с документацией и ссылка на загрузку здесь.

Функции библиотеки GyverWDT:

  • Watchdog.reset(); короткая и быстрая команда сброса сторожевого таймера в 0, вызывается внутри программы , для подтверждения нормальной работы.
  • Watchdog.disable(); короткая команда остановки работы сторожевого таймера. Вызывается для полного отключения watchdog.
  • Watchdog.enable(mode, prescaler); функция для конфигурации и запуска таймера (см. пример)
Режим (константа) Действие при тайм-ауте
INTERRUPT_MODE Прерывание, вызывающее функцию, указанную в watchdog_enable ()
INTERRUPT_RESET_MODE Прерывание, вызывающее функцию, указанную в watchdog_enable () , и автоматическая установка сторожевого таймера в режим reset.
(комбинированный режим) Соответственно первый тайм-аут инициирует прерывание, второй тайм-аут – инициирует сброс.

Зачем нужен комбинированный режим? Он позволяет одновременно использовать как прерывания, так и сброс, что может оказаться очень мощным инструментом. Как это работает:

  • Если причина зависания кроется в программной ошибке, например, в бесконечном ожидании поступления данных. Первый тайм-аут вызовет прерывание с высоким приоритетом, если мы смогли в него войти и понять, что это случилось – зависание явно носит программный, а не аппаратный характер. В таком случае мы можем попытать исправить программную ошибку прямо внутри прерывания, или предупредить пользователя, что написанный код имеет проблемные участки. Если удалось оперативно решить проблему до наступления следующего тайм-аута, мы можем не отходя от кассы перенастроить сторожевой таймер функцией Watchdog.enable(…) , чтобы следующий тайм- аут отправил нас в прерывание-сигнализатор, вместо hard-reset. Если же не удалось оперативно исправить причину зависания – следующий тайм-аут автоматически сбросит контроллер.
  • Если необходимо использовать одновременно прерывания (например, для опроса датчиков), и защиту от зависания. В пользовательской функции прерывания, помимо необходимых нам действий, опроса датчиков и т.д. просто дописывается функция перенастройки Watchdog.enable (…), если перенастройка не произошла, значит контроллер не вошел в процедуру прерывания, попросту зависнув. Следующий тайм-аут сбросит контроллер.
  • Если необходимо аппаратно следить следить как за работоспособностью внешнего устройства, так и самого МК. Например, при совместной работе МК и Bluetooth/Wi-Fi /GSM/GPS модуля. Как это реализовать? Watchdog настраивается на необходимый период. Внешнее устройство обязано периодически посылать на МК подтверждение своей работоспособности, это может быть как импульс, периодически создаваемый с помощью GPIO внешнего устройства, инициирующий внешнее прерывание МК, так и посылка, отправляемая внешним устройством по одному из последовательных интерфейсов, и аналогично инициируя прерывания МК. Внутри процедуры прерывания, вызывается короткая функция Watchdog.reset (); , сбрасывающая сторожевой таймер. Если внешнее устройство зависло и перестало посылать данные/импульсы, сбросов watchdog не происходит и происходит прерывание, внутри которого мы можем без труда сбросить внешнее устройство, подав на него низкий уровень (необходимо наличии внешней подтяжки к VCC). В этом же прерывании производится перенастройка watchdog. Если МК зависнет – он не перенастроит watchdog вовремя, и следующий тайм-аут инициирует сброс МК.

Примеры из библиотеки


#include "GyverWDT.h"

/*
   Пример использования watchdog в качестве генератора прерываний
   Зависимость таймаутов от делителей см. в GyverWDT.h
*/

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
  Watchdog.enable(INTERRUPT_MODE, WDT_PRESCALER_128); // Режим генерации прерываний , таймаут ~1с
}

void loop() {
  /* Загруженный цикл */
  digitalWrite(13, HIGH);
  delay(5000);
  digitalWrite(13, LOW);
  delay(5000);
}

/* Прерывание watchdog */
ISR(WATCHDOG) {
  Serial.println("Hello from interrupt");
}

#include "GyverWDT.h"

/*
  Пример классического применения watchdog - сброс устройства при зависании
  Если не вызвать Watchdog.reset() вовремя - произойдет сброс
  Зависимость таймаутов от делителей см. в GyverWDT.h
*/

void setup() {
  pinMode(13, OUTPUT);
  Watchdog.enable(RESET_MODE, WDT_PRESCALER_512); // Режим сторжевого сброса , таймаут ~4с
}

void loop() {
  /* Ваш полезный код */
  digitalWrite(13, HIGH);
  delay(1000);
  digitalWrite(13, LOW);
  delay(1000);
   
  Watchdog.reset(); // Переодический сброс watchdog, означающий, что устройство не зависло
}

#include "GyverWDT.h"
#include <avr/sleep.h>

/*
   Пример использования watchdog в "будильника"
   Зависимость таймаутов от делителей см. в GyverWDT.h
*/

void setup() {
  pinMode(13, OUTPUT);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Выбираем нужный режим сна
}

void loop() {

  /* Мигаем светодиодом , а в паузах спим */
  digitalWrite(13, HIGH);

  Watchdog.enable(INTERRUPT_MODE, WDT_PRESCALER_128); // Режим прерываний , таймаут ~1c
  sleep_enable();                                     // Разрешаем сон
  sleep_cpu();                                        // Уходим в сон

  digitalWrite(13, LOW);

  Watchdog.enable(INTERRUPT_MODE, WDT_PRESCALER_128); // Режим прерываний , таймаут ~1c
  sleep_enable();                                     // Разрешаем сон
  sleep_cpu();                                        // Уходим в сон

}

/* Прерывание watchdog , в нем мы просыпаемся */
ISR(WATCHDOG) {
  sleep_disable();     // Запрещаем сон
  Watchdog.disable();  // Выключаем watchdog
}

#include "GyverWDT.h"

/*
   Пример работы watchdog для контроля сразу за двумя устройствами
   В данном случае - arduino + esp8266 модуль
   Зависимость таймаутов от делителей см. в GyverWDT.h
*/

void setup() {
  Serial.begin(9600);                                         // На этом последовательном порту у нас wifi-модуль
  pinMode(8, INPUT);                                          // Этот пин сбрасывает wifi-модуль при переходе в режим OUTPUT
  Watchdog.enable(INTERRUPT_RESET_MODE, WDT_PRESCALER_1024);  // Комбинированный сторжевой режим , таймаут ~8с
}


void loop() {
  /* Ваш полезный код */
}

/* Если wifi-модуль нам что то отправил , а мы его поняли - никто из нас не завис */
void serialEvent() {
  Watchdog.reset();  // Сбросить watchdog
}

/* Если wifi-модуль не отправлял нам что то более 8с - он завис */
ISR(WATCHDOG) {
  pinMode(8, OUTPUT);                                         // Тянем за reset модуля
  Watchdog.enable(INTERRUPT_RESET_MODE, WDT_PRESCALER_1024);  // Если мы не смогли перенастроить watchdog - значит мы зависли
  Watchdog.reset();                                           // Если не зависли - сбросить watchdog и продолжить
  pinMode(8, INPUT);                                          // Отпускаем reset модуля
}

#include "GyverWDT.h"

/*
   Пример использования watchdog в комбинированном режиме
   Может использоваться для оповещения в случае зависания
   Первый тайм-аут вызывает прерывание, второе - сброс
   Перенастройте watchdog чтобы избежать сброса
   Зависимость таймаутов от делителей см. в GyverWDT.h
*/

void setup() {
  Serial.begin(9600);
  Serial.println("Program started");
  Watchdog.enable(INTERRUPT_RESET_MODE, WDT_PRESCALER_128);   // Комбинированный режим , таймаут ~1c
  Serial.println("watchdog enabled");
  while (1);                                                  // Причина зависания
  Serial.println("loop started");                             // Этого мы не увидим из-за зависания
}

void loop() {
  /* этого кода программа не достигнет - зависание в setup */
  Serial.println("hello");
  delay(500);
}

/* Первый тайм-аут вызывает прерывание */
ISR(WATCHDOG) {
  // Если причина зависания программная - тут можно попытаться исправить ее */
  Serial.println("warning!");
  // Если исправить причину не вышло - следующий таймаут вызывает сброс
  // Watchdog.enable(INTERRUPT_RESET_MODE, WDT_PRESCALER_128); // Если перенастроить watchdog здесь - сброса не будет
}

Watchdog и сон


  1. Настроить МК на один из режимов сна
  2. Разрешить сон
  3. Взвести watchdog на определенный тайм-аут в режиме прерываний
  4. Уйти в сон
  5. При пробуждении, оказавшись в процедуре прерывания watchdog, запретить сон
  6. Продолжить выполнение программы

Проблемы


Основная проблема заключается в том, что режим автоматического сброса не работает на некоторых платах Arduino. Вернее работает то он просто отлично, только приводит к бесконечной перезагрузке или boot loop. Почему так происходит? Дело в том, что контроллеры свежих партий, в случае перезагрузки по watchdog автоматически перенастраивают сторожевой таймер на минимальный период, т.е. 16 мс, не отключая его. Чтобы избежать бутлупа необходимо при старте программы выключить watchdog, но как известно, в большинстве плат мы имеем МК с зашитым загрузчиком, время выполнения которого выше, чем 16 мс. Это приводит к тому, что контроллер даже не успевает приступить к выполнению основной программы, как уже улетает в hard reset , происходит это циклически и бесконечно долго. Единственный способ остановить этот процесс – обесточить плату. Во многих платах выпуска 2018+ года зашит более оптимизированный и продуманный загрузчик optiboot, который автоматически выключает watchdog при старте, что предотвращает бутлуп и позволяет нам использовать сторожевой таймер на 100%.

Проверка работоспособности Watchdog


В примерах к библиотеке идёт скетч тестирования поддержки watchdog на вашей плате. Загрузите его в плату и откройте монитор порта. Спустя 10 секунд паузы, установленной для упрощения перепрошивки при boot loop, в монитор порта начнет выводиться время работы программы в секундах. Если при достижении таймаута программа сбросилась и отсчет начался заново – ваша плата (загрузчик) поддерживает watchdog на все 100%. Если после перезагрузки корректный вывод времени в монитор порта прекратился, а светодиод на плате начал быстро и безостановочно мигать – загрузчик на вашей плате не поддерживает перезагрузку watchdog. Загрузите в плату любой другой скетч, чтобы продолжить использование.

#include <GyverWDT.h>

/*
  Пример тестирующий поддержку всех функций watchdog на вашем устройстве
  > После 10 секунд отсчета программа стартует заного -> поддерживаются все функции
  > После таймаута устройство зависает, светодиод на D13 начинает мигать -> поддерживается только INTERRUPT_MODE
  В случае bootloop у вас будет 10 секунд на перепрошивку устройства после подачи питания
  Для добавления поддержки всего функционала watchdog загрузите optiboot или откажитесь от загрузчика
*/


void setup() {
  Serial.begin(9600);

  Serial.println("Program started , wait 10 seconds");
  delay(10000);                                             // 10 секунд на перепрошивку в случае bootloop

  Watchdog.enable(RESET_MODE, WDT_PRESCALER_1024);          // Режим сторожевого сброса , таймаут ~8с
  Serial.println("watchdog enabled");

  while (1) {                                               // Бесконечный цикл , эмуляция "зависания"
    if (!(millis() % 1000)) {                               // Каждую секунду
      Serial.println((uint16_t)((millis() / 1000) - 10));   // Вывести время после включения watchdog в секундах
      delay(10);
    }
  }

}

void loop() {}

 

Что делать, если загрузчик на моей плате не поддерживает watchdog?

Если вам действительно нужен полный функционал сторожевого таймера – необходимо перепрошить загрузчик, для этого необходим программатор, который поддерживает МК AVR, или другая плата Arduino + скетч Arduino ISP. Подробнее читайте в уроке про работу с программатором. Без перепрошивки загрузчика вам доступны обычные прерывания, не использующие таймеров и имеющие высокий приоритет, и соответственно возможность использования watchdog в режиме будильника МК.

Важные страницы