Датчик температуры на даче проработал ровно два дня и семнадцать часов, после чего замолчал до субботы. Ехал туда специально: потыкал Reset — ожил. Через два дня — снова тишина. Код был «простой»: читать датчик, слать в Serial, повторять. Виновником оказался бесконечный while с ожиданием ответа: датчик один раз не ответил вовремя, а мой цикл вежливо ждал ответа вечно. Плата была включена, светодиод питания горел, скетч формально жил — но ни одна строка полезной работы не выполнялась. Это и есть зависание: не краш, а вечное ожидание.
Вторая попытка была тупиком поучительнее первой: я добавил таймаутов, логов и проверок — и получил код, который теперь зависал в другом месте. Ошибка была не в строках, а в подходе: я пытался предусмотреть все условия вручную. Надёжные прошивки устроены противоположно — они исходят из того, что зависнут обязательно, и заранее держат наготове механизм самоспасения, который не зависит от вашего кода вообще.
В этом уроке — четыре аппаратных счётчика ATmega328P: три рабочих и один сторожевой. Научимся перезагружать зависший скетч watchdog-ом с пределом восемь секунд, соберём софтверного сторожа на millis() прямо в симуляторе и посчитаем, как сон в режиме power-down превращает 45 мА платы в микроамперы — разницу между двумя днями на батарейках и полутора годами.
Откуда берутся зависания
Краши легко опознать: плата перезапустилась, setup() отработал, Serial печатает баннер заново. Зависание коварнее — снаружи всё выглядит нормально, но loop() застрял в одной точке. Три типовых источника: бесконечное ожидание внешнего события (ответ датчика, готовность шины, открытый Serial), выход за границы массива, после которого память ломается непредсказуемо, и блокирующая библиотека, у которой внутри собственного while — вы её не видите, но она есть.
Мой дачный датчик завис внутри функции чтения шины: библиотека отправляла запрос и ждала подтверждения в цикле. Один ослабший контакт на клеммнике — и цикл становился вечным. Хуже того: я добавил проверку связи в начало loop(), но проверка стояла до проблемного места, а не внутри него, и потому была бесполезна. Вывод, который я вынес с балкона: зависание нельзя вылечить проверками снаружи, если само место зависания спрятано внутри чужой библиотеки.
И заметьте — задержки тут ни при чём. Скетч с delay() и скетч на millis() зависают одинаково, потому что зависание — это не про ожидание по расписанию, а про условие, которое не наступает. Единственное, что в ATmega328P продолжает считать при любом поведении вашего кода, — аппаратные счётчики. С них и начнём.
// НЕ запускайте на настоящем железе с живым датчиком:// если датчик не ответит, loop() застрянет навсегда. void setup() { Serial.begin(9600); while (!Serial) { // на Uno Serial готов всегда, на Leonardo этот цикл // честно ждёт открытия монитора порта }} void loop() { requestSensor(); // спросили датчик while (!sensorReady()) { // а если датчик умер? // ...ждём вечно } int t = readSensor(); Serial.println(t);}Четыре счётчика внутри чипа
Внутри ATmega328P три классических аппаратных таймера: Timer0 (8-битный), Timer1 (16-битный) и Timer2 (8-битный). Каждый тикает от тактовой частоты 16 МГц через делитель, независимо от вашего кода — даже в длинном delay(). На них стоит вся невидимая кухня Arduino: millis() и delay() собирают время из Timer0, библиотека Servo генерирует импульсы на Timer1, tone() свистит через Timer2. Общая идея таймера одна: счётчик тикает сам, а при переполнении или совпадении с заданным числом поднимает прерывание.
Четвёртый счётчик — особенный: watchdog, сторожевой таймер. У него собственный крошечный источник тактирования, отдельный от основной логики чипа. Задача у него не считывать время, а выживать: watchdog считает от нуля до заданного предела — максимум восемь секунд — и, если его вовремя не сбросили, жёстко перезагружает весь чип. Точно так же, как кнопка Reset, только без вашего пальца.
Логика сторожевой собаки простая и жёсткая. В нормальном скетче loop() пробегает полный круг гораздо быстрее восьми секунд, и в каждом круге вы «гладите собаку» — вызываете wdt_reset(), сбрасывающий счётчик. Зависло? Круг не доходит до гладильной строки, счётчик досчитывает до предела — и плата перезапускается. Через несколько секунд устройство снова живое, без вашей поездки на дачу и без участия основного кода, который как раз и сломался.
На настоящей плате watchdog включается двумя строками через библиотеку avr/wdt.h. В браузерном симуляторе команды wdt нет — там мы соберём софтверного сторожа на millis(), который устроен ровно так же, только перезапускает не весь чип, а конкретно зависшую задачу. Сначала железный вариант — он вам понадобится в каждом батарейном и дачном проекте.
#include <avr/wdt.h> void setup() { wdt_enable(WDTO_8S); // предел 8 секунд — максимум для watchdog Serial.begin(9600); Serial.println("Старт, сторож включён");} void loop() { wdt_reset(); // «гладим собаку»: сброс счётчика в ноль readSensors(); // весь полезный круг обязан успеть за 8 с sendData(); // если что-то зависло — до wdt_reset() мы не дойдём, // и через 8 секунд чип перезагрузится сам}Режимы сна: микроамперы вместо миллиампер
Watchdog спасает от зависаний, а сон — от разряженных батареек. Сон — это официальные режимы чипа, в которых часть железа выключается. У ATmega328P их шесть: от лёгкого idle — спит только ядро, таймеры тикают, просыпается за такты, — до глубокого power-down: ядро, тактовая частота и почти вся периферия остановлены, разбудить может только внешнее прерывание (тот самый INT0 на пине 2), смена пина или watchdog.
Разница в потреблении драматическая. Плата Uno в активной работе берёт примерно 45 мА — и почти ничего из этого не тратится на вычисления: жрут светодиод питания, USB-чип и стабилизатор. А сам чип ATmega328P в power-down потребляет единицы микроампер — порядка 5-7 мкА при включённом watchdog — в тысячи раз меньше. Отсюда честный вывод: настоящий батарейный узел — это голый чип или мини-плата вроде Pro Mini со снятым светодиодом и стабилизатором, а не Uno в сборе. Uno уснёт, а батарейку всё равно съест обвязка платы.
Главный приём батарейных узлов — скучная жизнь. Узел просыпается по watchdog каждые восемь секунд, за 100 мс читает сенсор и передаёт данные, снова засыпает. Считаем средний ток для голого чипа на 16 МГц: (0,1 с × 15 мА + 8 с × 0,006 мА) / 8,1 с — выходит примерно 0,19 мА. На трёх батарейках АА ёмкостью 2500 мА·ч это около 13 000 часов — полтора года работы. При постоянной активности те же батарейки сгорели бы за 2500/15 — примерно неделю. Если верить даташитным микроамперам до конца; на практике смело закладывайте вдвое меньше — саморазряд, передатчик и холод съедят свою долю.
Код сна на настоящей плате — пара строк из avr/sleep.h: set_sleep_mode(SLEEP_MODE_PWR_DOWN); затем sleep_mode(); — и чип замолкает до прерывания. В симуляторе сон не отыграть, зато дисциплина батарейного узла уже перед вами: проснулся, сделал дело, оставил след жизни, уснул. Точно тот же паттерн «след плюс лимит тишины», что в стороже выше.
Эксперименты для закрепления
- Понизьте лимит тишины со 3000 до 1000 мс и поднимите шанс зависания до 20: перезапуски посыпятся чаще. Найдите баланс, при котором сторож не пугает живую, но медленную задачу.
- Добавьте вторую задачу — «радиомодуль» со своей меткой lastRadioOK и лимитом 5 секунд. Двухзадачный сторож — уже почти промышленный код.
- Заведите счётчик перезапусков в EEPROM: замените recoveries на EEPROM.read(0) + 1 и сохраняйте через EEPROM.update(0, ...). В симуляторе EEPROM поддержан — счётчик переживёт «перезапуск».
- Пересчитайте батарейный бюджет под себя: сон 8 с, активность 200 мс, ток 20 мА, ёмкость 2000 мА·ч. Подставьте числа в формулу из текста и сравните с неделями работы без сна.
- У Uno три рабочих таймера: Timer0 — millis(), Timer1 — Servo, Timer2 — tone
- Watchdog — четвёртый счётчик: перезагружает чип, если его не сбрасывали wdt_reset() в пределах 8 секунд максимум
- Зависание — не краш: плата жива, но loop() застрял в вечном ожидании
- Софтверный сторож: у каждой задачи «след жизни» и лимит тишины по millis()
- Power-down — самый глубокий сон: единицы мкА у чипа, но Uno в сборе всё равно берёт около 45 мА из-за обвязки
- Батарейный узел = сон 8 с + работа 100 мс: на 2500 мА·ч выходит около полутора лет
- 1. Какой максимальный предел у watchdog на ATmega328P?
- 2. Что делает wdt_reset()?
- 3. Чем зависание отличается от краша?
- 4. Какой режим сна у ATmega328P самый глубокий?
- 5. Почему Uno не годится для батарейного узла, даже если скетч спит?
- 6. Что обязан успеть сделать loop(), чтобы watchdog не перезагрузил чип?
Четыре урока раздела сложились в цельный инструментарий: неблокирующие таймеры на millis(), аппаратный дозор на прерываниях, генерация ШИМ и самоспасение со сном. Это тот слой, который отличает прошивку из туториала от устройства, живущего на балконе без присмотра годами.
Дальше вынесите за пределы урока три упражнения: переведите свой старый delay()-скетч на millis(), добавьте в него счётчик по прерыванию и прикрутите ШИМ-яркость. А когда устройство переедет на батарейки — вы уже знаете, кто в чипе отвечает и за самоспасение, и за будильник каждые восемь секунд.
Частые вопросы по теме урока
- Какой максимальный предел у watchdog на ATmega328P?
- WDTO_8S — восемь секунд, максимум для сторожевого таймера. Для более длинных циклов сон комбинируют: watchdog будит чип каждые 8 с, а счёт ведёт сам скетч. 8 секунд
- Что делает wdt_reset()?
- wdt_reset() — «гладить собаку»: сброс сторожевого счётчика. Если вызов не случится в пределах лимита, чип перезагрузится. Сбрасывает счётчик watchdog в ноль
- Чем зависание отличается от краша?
- Краш перезапускает плату, зависание — нет: скетч жив, светодиоды горят, но полезная работа не выполняется. Именно поэтому зависание так дорого обходится. При зависании плата работает, но loop() застрял в вечном ожидании и не перезапускается
- Какой режим сна у ATmega328P самый глубокий?
- Power-down останавливает ядро и почти всю периферию: проснуться могут только внешнее прерывание, смена пина или watchdog. Потребление — единицы микроампер. power-down
- Почему Uno не годится для батарейного узла, даже если скетч спит?
- Чип в power-down берёт микроамперы, но плата в сборе потребляет около 45 мА: светится power-LED, питается USB-чип, греется стабилизатор. Для батареек берут голый чип или Pro Mini без лишней обвязки. Обвязка платы — USB-чип, стабилизатор, светодиоды — продолжает есть десятки миллиампер
- Что обязан успеть сделать loop(), чтобы watchdog не перезагрузил чип?
- Сторож сбрасывается вызовом wdt_reset(). Полный круг loop() должен успевать до лимита; всё долгое — разбивайте на шаги и гладьте собаку между ними. Дойти до wdt_reset() раньше предела — максимум за 8 секунд