Теплица за городом, узел полива на голом чипе, батарея от щитка солнечной панели. Мне нужно было, чтобы чип просыпался строго по расписанию, делал замер и снова засыпал — на месяцы, без единого лишнего миллиампера. Все глаза на прерывания по кнопке не натянешь: событий нет, время идёт. И тут раскрылась вторая профессия watchdog, о которой в уроке про сторож-молоток сказано одной строкой: у него собственный крошечный генератор, который тикает даже во сне.
Получается интересная развилка. В базовом уроке watchdog работал молотком: считал до предела и перезагружал зависший чип. Но тот же счётчик можно настроить мягче — не перезагружать чип, а будить его прерыванием. Тогда watchdog превращается в будильник: восемь секунд спим, секунду работаем, снова спим — ровно тот цикл, на котором живут дачные узлы и счётчики.
Здесь разберу оба режима: классический wdt_enable() для сторожа и битовый конфиг WDIE для будильника без сброса. И обязательно — тёмную сторону: интервал сторожа короче времени загрузки, и скетч попадает в вечную перезагрузку, из которой не видно выхода. Этот гроб я собрал сам, случайно, и покажу, как он выглядит снаружи и как из него выходить.
Выбор между режимами — это выбор, что делать с состоянием. Будильник через полный сброс проще всего: скетч честно начинает жизнь с нуля, и если всё ценное живёт в EEPROM, ничего не теряется — так устроены простые счётчики моточасов и накипи. Режим прерывания дороже по вниманию: регистры конфигурируются вручную, в обработчике нельзя задерживаться, зато RAM переживает пробуждения и статистика ведётся без походов в EEPROM. Мои первые узлы шли через полный сброс; прерывание появилось, когда понадобилась живая статистика между будильниками.
Свой генератор не спит никогда
Watchdog тактируется от отдельного RC-генератора примерно на 128 кГц, независимого от основного кварца. Пока кварц стоит в power-down, сторож продолжает отсчёт — потому он и способен будить чип. Интервал выбирается ступенями от 15 миллисекунд до восьми секунд: WDTO_15MS, WDTO_30MS, WDTO_60MS, WDTO_120MS, WDTO_250MS, WDTO_500MS, WDTO_1S, WDTO_2S, WDTO_4S, WDTO_8S. Восемь секунд — потолок; больше не даст ни одна настройка, длинные паузы собираются кругами сна, как в уроке про LowPower и сон по кругу.
Первый путь — библиотечный: wdt_enable(WDTO_8S) включает сторож в режиме сброса. Чип уснул, через восемь секунд сторож сработал — и это был не будильник, а полная перезагрузка: скетч начался с setup заново, вся RAM обнулилась. Для глупого узла, который после пробуждения всё равно читает датчик с нуля, это годится и почти бесплатно. Но счётчики, расписания и статистика между пробуждениями не переживут: их место в EEPROM.
Второй путь — режим прерывания. В регистре WDTCR кроме бита разрешения есть отдельный бит WDIE: сторож не перезагружает чип, а вызывает прерывание WDT_vect, после чего скетч продолжает жить с того же места. Удобно и то, что эта конфигурация — чистая работа с битами: вписать нужные биты в регистр через маски, как в уроке про побитовые операции.
#include <avr/sleep.h> const int PIN_LED = 12;volatile byte budilnik = 0; // будильник прозвенелunsigned int probuzhdenij = 0; ISR(WDT_vect) { // сторож в режиме прерывания, не сброса budilnik = 1;} void wdtBudilnik8s() { // 8 с и только прерывание: WDIE без WDE WDTCR = _BV(WDCE) | _BV(WDE); // разблокировка смены конфига WDTCR = _BV(WDIE) | _BV(WDP3) | _BV(WDP0); // 8 s, режим будильника} void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(9600); Serial.println("uzel: son po 8 s, probuzhdenie po WDT"); Serial.flush(); wdtBudilnik8s();} void loop() { set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_enable(); sleep_mode(); // спим, пока сторож не прозвенит sleep_disable(); if (budilnik) { budilnik = 0; probuzhdenij = probuzhdenij + 1; // живёт в RAM: сброса не было digitalWrite(PIN_LED, HIGH); delay(100); // деловая часть: короткая и дешёвая digitalWrite(PIN_LED, LOW); Serial.print("probuzhdenie "); Serial.println(probuzhdenij); Serial.flush(); }}Разбор по строкам. WDTCR защищён от случайной перезаписи: сначала в него пишут связку WDCE+WDE, открывающую окно на четыре такта, и следом — рабочую конфигурацию. WDP3 и WDP0 вместе задают восемь секунд, WDIE включает прерывание, а отсутствие WDE означает «никаких перезагрузок». В ISR как всегда только флаг; Serial и мигания — в loop.
Счётчик probuzhdenij живёт в RAM и растёт от пробуждения к пробуждению — лучшая проверка, что чип не перезагружается: после настоящего сброса он начал бы с нуля. Если же нужен именно режим полного сброса, оставьте WDIE выключенным, включите WDE через wdt_enable() — и храните счётчики в EEPROM, как это делается в итоговом проекте термостата со сторожем.
Про честность расчётов: во сне со включённым сторожем чип ест порядка пяти-семи микроампер — сторож тоже потребляет, его генератор не бесплатен. Средний ток узла с восемью секундами сна и сотней миллисекунд работы выходит около двух десятых миллиампера — цифры и формула есть в уроке про таймеры, watchdog и сон. Отдельно про генераторы: кварц основного такта после пробуждения ещё должен раскрутиться, это миллисекунды стабилизации; сторожу же на его грубом RC-генераторе стабилизация не нужна — зато и точность у него плюс-минус десять процентов, секунды плавают. Часы на нём вести не стоит — для этого есть RTC.
| Интервал | Куда годится | Плата за сон | Чем грозит |
|---|---|---|---|
| WDTO_15MS - 250MS | быстрый сторож в плотном цикле | почти ноль во сне короткими рывками | любой задержистый вызов ловит сброс |
| WDTO_1S - 2S | сторож вокруг loop с датчиками | мкА во сне | setup дольше 2 с попадает в ребут-цикл |
| WDTO_4S | медленные узлы, инициализация внешних модулей | мкА во сне | редкий wdt_reset: зависание живёт до 4 с |
| WDTO_8S | будильник батарейного узла, максимум | порядка 5-7 мкА во сне | секунды плавают на 128 кГц RC-генераторе |
Вечная перезагрузка: как выглядит и как выйти
Теперь грабли, на которые я наступил с ходу: wdt_enable(WDTO_1S) в начале setup и инициализация карты памяти, которая честно занимает две секунды. Счёт: скетч стартовал, сторож пошёл, карта инициализируется… на второй секунде сторож сгорает — чип перезагружается, скетч стартует, карта снова инициализируется — и всё по кругу. Вечный ребут. Снаружи это выглядит жутковато: плата ритмично мигает стартом, монитор порта сыплет приветственными строками с частотой интервала, до loop() дело ни разу не доходит.
Ситуацию раньше усугублял bootloader: старые версии не выключали сторож при старте, и флаг сброса переживал перезагрузку — цикл шёл ещё до первой строки скетча, платы считались «кирпичами», спасала только прошивка через ISP. Современный optiboot на Uno чистит конфигурацию сторожа на входе, поэтому после срабатывания скетч стартует с выключенным watchdog и успевает дожить до своего wdt_enable. Но запас прочности никто не отменял: первой строкой setup ставьте wdt_disable(), как этого требует и журналирование последнего слова перед ребутом — без него причина аварии в EEPROM может не долететь.
Правила, которые я вывел из той истории. Первое: интервал сторожа — минимум вдвое больше худшего времени setup плюс худшего прохода loop; не меряли худший путь — ставьте WDTO_8S и спите спокойно. Второе: перед заведомо долгой библиотечной операцией — init SD-карты, долгий обмен по шине — можно подбодрить сторожа wdt_reset(); это костыль, но лучше костыля только интервал с запасом. Третье: никогда не ставить wdt_reset() внутри обработчика прерывания, если основной код заблокирован — сторож перестанет ловить главное зависание, ради которого его звали. И четвёртое: delay() длиннее интервала — та же мина, что и долгий setup; круги пауз на millis() из урока про millis вместо delay снимают вопрос целиком.
Что докрутить
Задания на закрепление. Первое: соберите узел из урока, добавьте к нему счётчик пробуждений в EEPROM и посмотрите, что происходит с ним в обоих режимах сторожа — так вы своими глазами увидите разницу между будильником и полным сбросом. Второе: сделайте гибрид — сторож включён в режиме WDE на восемь секунд, а перед каждым засыпанием wdt_reset() и сон; убедитесь, что зависание в loop всё ещё ловится. Третье: посчитайте, сколько раз в сутки прозвенит восьмисекундный будильник, и сколько это стоит в микроампер-часах для вашего сценария.
И четвёртое, на понимание: специально соберите вечный ребут — wdt_enable(WDTO_1S) и delay(2000) в setup, — запишите, как ведёт себя монитор порта, а потом почините первой строкой wdt_disable(). Пять минут работы экономят потом часы диагностики на реальном железе.
- 1. Почему watchdog умеет будить чип из power-down?
- 2. Максимальный интервал watchdog у ATmega328P?
- 3. Чем режим с битом WDIE отличается от wdt_enable()?
- 4. Скетч попадает в вечную перезагрузку. Самая частая причина?
- 5. Где место wdt_reset()?
- Watchdog тактируется своим 128 кГц RC-генератором и тикает во сне — это делает его будильником для power-down
- Интервалы от 15 мс до 8 с; восемь секунд — потолок, длинные паузы собираются кругами сна
- Режим WDIE будит чип прерыванием без перезагрузки: RAM живёт, счётчики растут; wdt_enable() — это полный сброс
- Вечный ребут — интервал короче setup или loop; wdt_disable() первой строкой setup и запас ×2 от худшего замеренного пути
- Во сне со сторожем чип ест порядка 5-7 мкА; средний ток узла с 8 с сна и 100 мс работы — около 0,2 мА
Частые вопросы по теме урока
- Почему watchdog умеет будить чип из power-down?
- Сторож тактируется отдельным RC-генератором, который работает и когда кварц остановлен. Именно поэтому он единственный «часовщик», доступный в глухом сне. у него свой RC-генератор около 128 кГц, не зависящий от основного кварца
- Максимальный интервал watchdog у ATmega328P?
- Ступени идут от 15 мс до WDTO_8S. Длинные паузы собираются кругами сна, а не одной настройкой. произвольный, задаётся в секундах
- Чем режим с битом WDIE отличается от wdt_enable()?
- wdt_enable() включает режим сброса: счётчик сгорел — чип перезагрузился. Бит WDIE вызывает прерывание WDT_vect, скетч продолжает жить, RAM и переменные сохраняются. ничем, это синонимы
- Скетч попадает в вечную перезагрузку. Самая частая причина?
- Если setup или loop не укладываются в интервал, сторож сгорает раньше wdt_reset — и цикл повторяется. Лечение: wdt_disable() первой строкой setup и интервал с двойным запасом. выключенный Serial
- Где место wdt_reset()?
- Сброс сторожа из ISR держит счётчик живым даже при зависшем главном цикле — сторож слепнет. Поэтому wdt_reset() ставят в setup/loop и, в крайнем случае, перед долгой библиотечной операцией. внутри delay()