Поливалка на балконе молчала трое суток. Плата была жива: светодиод на борту мигал, помпа щёлкала по расписанию — а в журнале, который я собирал через Serial, после среды не появилось ни одной строки. В четверг скетч упёрся в датчик уровня, который перестал отвечать: цикл ожидания без выхода завис, watchdog честно перезагрузил плату — и вместе с зависанием стёр весь Serial-лог и все переменные. Плата поднялась чистой и невинной, будто ничего не случилось, а я остался с вопросом «что это было» без единой улики.
Теперь перед каждым зависанием у меня остаются две улики. Первая — последняя печать перед wdt_enable: шаг, значение датчика, время; журнал ведётся по уровням из урока про уровни логов. Вторая — байт-флаг в EEPROM: энергонезависимая память из урока про EEPROM без питания переживает и зависание, и перезагрузку. После старта setup() первым делом читает флаг и печатает: упал после шага X. Здесь соберём этот паттерн целиком и разберём граблю, из-за которой на старых Nano он вообще не работает.
План такой: сначала механика сторожа — генератор, ступени таймаутов, кто и как кормит пса; потом паттерн двойной улики — печать шага в Serial плюс флаг в EEPROM, и что делает setup() с флагом после старта; в финале — таблица причин падений с тем, что печатать в каждом случае, и грабля старых загрузчиков, которая на некоторых Nano обнуляет весь смысл затеи. Если вы уже видели сторожа в деле на итоговом термостате, здесь тот же персонаж получит собственную систему чёрного ящика.
Как работает сторожевой таймер
Watchdog — отдельный счётчик с собственным генератором около 128 килогерц, который тикает независимо от вашего скетча; место в системе таймеров ATmega328 мы рассматривали в обзоре таймеров. Вызов wdt_enable(WDTO_2S) заводит отсчёт на две секунды, wdt_reset() обнуляет его — «кормит пса». Пока loop() успевает кормить, счётчик не доходит до конца. Завис скетч — хотя бы один вызов wdt_reset пропущен, отсчёт дошёл до нуля, и микроконтроллер получает сброс: тот же, что от кнопки Reset, только без вашего участия. Генератор у сторожа грубый, плюс-минус десять процентов, поэтому таймаут в две секунды на практике плавает в обе стороны — выбирать время в упор, впритык к реальному циклу, нельзя.
Таймауты на ATmega328 идут ступенями от 15 миллисекунд до 8 секунд: WDTO_15MS, WDTO_500MS, WDTO_2S и далее. Выбор я делаю по простому правилу: таймаут втрое длиннее худшего нормального прохода loop(). Если цикл при тяжёлом датчике занимает 300 миллисекунд — ставлю WDTO_1S; а WDTO_15MS рядом с delay(500) — это подписка на перезагрузки каждые 15 миллисекунд, потому что delay пса не кормит. И ещё: wdt_reset() должен звать сам loop(), а не обработчик прерывания таймера — иначе зависший цикл будет исправно кормить пса, который по идее должен был его укусить.
У сторожа есть и мягкий режим: перед сбросом он может позвать прерывание — так плату будят из сна по расписанию, и это отдельный разговор про энергосбережение; режим задаётся битами WDIE и WDE в регистре WDTCSR, а wdt_enable управляет ими за вас. Для страховки от зависаний нужен именно сбросный режим, который даёт wdt_enable. Таймаут подбирается под характер скетча: для термостата, спящего между замерами, я беру WDTO_4S и кормлю пса раз в секунду; для скетча с активным обменом по I2C хватает WDTO_500MS — зависание на шине ловится полусекундой, а не восемью секундами молчания.
| Причина падения | Что печатать перед wdt_enable | Что назовёт флаг после старта |
|---|---|---|
| Цикл ожидания датчика без выхода | шаг и имя ожидания: shag=3 zhdem datchik | флаг 3 — добавляйте таймаут чтения |
| Выход за границы массива | индекс записи перед каждым обращением | последний индекс — видно, куда полезли |
| Просадка питания, помеха | миллисекунды с старта и последнюю команду | малое время жизни — падение раннее |
| Зависание на I2C-шине | какой адрес шины ждём | флаг этапа шины — проверять подтяжку |
Последний вздох: Serial плюс EEPROM
Паттерн выглядит так. Там, где вы подозреваете зависание, скетч печатает состояние в Serial: шаг, значение, время. Тут же он пишет номер шага в EEPROM командой EEPROM.update(0, shag) — update, а не write, чтобы не жечь ячейку при каждом проходе: ресурс EEPROM около ста тысяч перезаписей, мы считали их в уроке про износ ячеек. Рядом по соседнему адресу хорошо живёт счётчик срабатываний — сколько раз сторож уже спасал плату; тот же приём разборан в уроке про счётчик перезагрузок.
После сброса setup() начинается с чтения: flag = EEPROM.read(0). Флаг не ноль — предыдущая жизнь кончилась падением на шаге flag: печатаем причину, показываем состояние светодиодом и обнуляем ячейку, чтобы следующая штатная перезагрузка не пугала ложной тревогой. Флаг пишите ДО включения сторожа, а не после: EEPROM обновляется за миллисекунды, но зависание может случиться между записью и wdt_enable — и тогда улика всё равно останется от предыдущего прохода. А печать в Serial дублирует флаг для человека: память держит номер шага, журнал — контекст с числами.
Флаг умеет говорить и без монитора: если плата стоит в ящике без USB, Serial печатать некуда — кодируйте причину миганиями светодиода. Три вспышки с паузой — упал на шаге 3, пять вспышек — на шаге 5; шифр вы придумываете сами и записываете в комментарий первой строкой скетча, потому что через полгода он забудется. И деталь порядка: Serial.begin обязан стоять до чтения флага — иначе первую улику печатать некуда, и вы увидите только её хвост.
#include <EEPROM.h>#include <avr/wdt.h> // Шаги: 1 — чтение датчика, 2 — расчёт, 3 — ожидание клапана.// Печать на каждом проходе — для наглядности; в боевом скетче печатайте// смену состояния, чтобы журнал не рос в весе.int shag = 0; void zapisShaga(int n, String chto) { shag = n; Serial.print("LOG shag="); Serial.print(n); Serial.print(" "); Serial.println(chto); EEPROM.update(0, n); // улика переживает любое падение} void setup() { Serial.begin(9600); int flag = EEPROM.read(0); if (flag > 0) { Serial.print("POSLE SBROSA: upal posle shaga "); Serial.println(flag); if (flag == 1) Serial.println("prichina: cikl chteniya datchika bez taimauta"); if (flag == 2) Serial.println("prichina: raschet, proverit indeksy massiva"); if (flag == 3) Serial.println("prichina: ozhidanie klapana, proverit pitanie"); EEPROM.update(0, 0); // тревога отработана — гасим } else { Serial.println("CHISTYI START"); } wdt_disable(); // на время setup сторож спит} void loop() { wdt_enable(WDTO_2S); // пса выпустили на 2 секунды zapisShaga(1, "chitayu datchik"); // ... чтение датчика ... wdt_reset(); zapisShaga(2, "schitayu obiem"); // ... расчёт ... wdt_reset(); zapisShaga(3, "zhdem klapan"); // ... ожидание ... wdt_reset(); delay(1000); // Демонстрация: раскомментируйте — плата будет уходить в сброс каждые 2 секунды, // а после старта печатать шаг, на котором зависла: // while (true) { }}Движок симулятора не эмулирует ни wdt_enable, ни настоящую перезагрузку с потерей питания, а EEPROM в нём живёт лишь в памяти прогона — честная замена: скетч моделирует сам паттерн. Командой step вы двигаете плату по шагам, crash имитирует зависание: скетч печатает последний лог, пишет номер шага в EEPROM и гасит светодиод — как плата, у которой сторож вот-вот сработает. Команда restart играет роль сброса: запускается логика setup(), флаг читается, причина падения печатается, тревога гасится.
На реальной плате эти же строки работают сами: wdt_enable показан в основном скетче выше. Светодиод на пине 12 — индикатор жизни: горит после чистого старта и после restart, гаснет при crash. Побудьте в роли сторожа сами: спровоцируйте падение на шаге 1 и на шаге 3 и сравните, какую причину назовёт стартовый лог, — это ровно тот разбор, который вы будете делать на плате после каждого необъяснённого сбоя.
Отдельный вопрос — отличить сторожа от других сбросов: плату мог дёрнуть человек, могла пропасть питание, мог отвалиться USB. Помогает счётчик стартов в соседней ячейке EEPROM: инкремент при каждом setup, а рядом — байт с аптаймом перед падением. Сторожа выдаёт характерная пара: счётчик вырос на единицу, а аптайм мал и каждый раз упирается примерно в один и тот же таймаут. Человек и питание дают большие и разные аптаймы — такая статистика за неделю отвечает на вопрос «зависает ли плата вообще», прежде чем вы начнёте искать причину.
- Watchdog — отдельный счётчик на своём генераторе: wdt_enable заводит отсчёт, wdt_reset() кормит пса, зависание без кормления даёт сброс
- Таймаут — втрое длиннее худшего нормального прохода loop; ступени на ATmega328 от 15 мс до 8 с
- Последний вздох — двойная улика: печать состояния в Serial и EEPROM.update(0, shag) перед wdt_enable
- setup() после сброса начинается с EEPROM.read(0): флаг не ноль — печатаем шаг и причину, потом гасим тревогу
- Nano с загрузчиком 2009 года после watchdog-сброса уходит в бесконечный цикл: лечение — прошивка Optiboot
- 1. Что делает wdt_reset()?
- 2. Какой максимальный таймаут у watchdog на ATmega328?
- 3. Nano 2009 года после первого срабатывания watchdog перезагружается по кругу и не шьётся. В чём дело?
- 4. Где хранить номер шага, чтобы улика пережила сброс?
- 5. С чего обязан начинаться setup() на плате с watchdog-логом?
Частые вопросы по теме урока
- Что делает wdt_reset()?
- wdt_reset() перезапускает отсчёт с нуля. Пока loop() вызывает его регулярно, счётчик не доходит до конца и сброса не происходит. Обнуляет отсчёт: пса покормили, сброс отложен
- Какой максимальный таймаут у watchdog на ATmega328?
- Ступени идут от WDTO_15MS до WDTO_8S. Практика: таймаут втрое длиннее худшего нормального прохода loop(). 8 секунд
- Nano 2009 года после первого срабатывания watchdog перезагружается по кругу и не шьётся. В чём дело?
- Загрузчик обязан гасить сторожа и чистить флаг WDRF до старта скетча. Старый этого не делает — счётчик тикает заново ещё до setup(). Optiboot решает. Старый bootloader не выключает сторожа после сброса; лечение — прошивка Optiboot
- Где хранить номер шага, чтобы улика пережила сброс?
- RAM обнуляется при сбросе, буфер порта тоже. EEPROM держит байт без питания; update перезаписывает ячейку только при смене значения. В EEPROM одним байтом через update
- С чего обязан начинаться setup() на плате с watchdog-логом?
- Первым делом читаем флаг: если он не ноль, предыдущая жизнь кончилась падением — печатаем шаг и причину, затем гасим тревогу. С чтения флага EEPROM и печати причины падения