Гаражный узел на 8266 должен был писать температуру раз в минуту и молчать. Молчал он не всегда: раз в сутки-двое в журнале открывалась дыра, а после неё шла строка boot с нулевым аптаймом. Плата рестартовала сама, и я не знал ни когда, ни почему. Тогда я впервые отнёсся к рестарту как к событию, которое надо не лечить наугад, а документировать: сколько прожило, отчего умерло, сколько раз это уже случалось. Аптайм и причина последнего рестарта — первые два вопроса, которые я теперь задаю любой плате, пока она ещё в состоянии ответить.
Здесь: как считать аптайм на millis и форматировать его в дни и часы, где у счётчика потолок в 49,7 суток, как ESP.getResetReason() выдаёт шильдик последней смерти и что значит каждый шильдик, зачем плате сторож и отчего зацикленный код рестартует по кругу. Счётчик перезагрузок заводим в RTC-памяти пользователя — ближайшей родственнице той, что выживает во сне ESP32.
Аптайм: миллисекунды в дни и часы
Аптайм — это millis() с момента старта, весь вопрос в форматировании. Деление на 1000 даёт секунды, целочисленное деление на 86400 — полные сутки, из остатка вычитаются часы, минуты и секунды; в скетче ниже строка up=0d 00:04:37 идёт в монитор раз в секунду. Ценность не в красоте: отчёт замолчал — значит, скетч умер раньше платы; строка растёт — значит, loop дышит. Для журнала я дополнительно печатаю сырые секунды: их удобно складывать и сравнивать, не расшифровывая двоеточия.
Потолок у millis() тот же, что на Uno: 32 бита без знака, 4 294 967 295 миллисекунд — 49,7 суток, после чего счётчик переполнится и начнёт с нуля, а мой аптайм соврёт. Гаражный узел я перезагружаю раз в месяц — хватает с запасом; устройству на год одного millis мало: считайте переливы или берите время из сети — урок про NTP и configTime даёт плате настоящие часы без переполнений и вранья. Аптайм на millis — инструмент диагностики, а не биллинга.
В журнал аптайм у меня идёт в каждой строке события, а не отдельным столбом: температура, показания датчика и up= в одной строке — и дыра в журнале мгновенно отвечает на вопрос «до какого момента дожила прошлая жизнь». Дельта между соседними строками — это пауза между событиями; её скачок с 60 секунд до нуля и есть рестарт, без всяких догадок. Такой формат я выстрадал на второй итерации: первый журнал с аптаймом отдельным столбцом листать оказалось втрое дольше.
// ESP-объект (getResetReason, rtcUserMemory) объявлен ядром сам.struct BootInfo { uint32_t magic; // сторож: 0xC0FFEE — память уже наша uint32_t boots; // счётчик перезагрузок uint32_t lastUp; // аптайм прошлой жизни, в секундах};BootInfo bi; void setup() { Serial.begin(115200); delay(100); ESP.rtcUserMemoryRead(0, (uint32_t*)&bi, sizeof(bi)); if (bi.magic != 0xC0FFEE) { // питание сняли: память пустая bi.magic = 0xC0FFEE; bi.boots = 0; bi.lastUp = 0; } bi.boots = bi.boots + 1; Serial.print("reason="); Serial.println(ESP.getResetReason()); // шильдик прошлой смерти Serial.print("boots="); Serial.println(bi.boots); ESP.rtcUserMemoryWrite(0, (uint32_t*)&bi, sizeof(bi));} void loop() { static unsigned long t0 = 0; if (millis() - t0 >= 1000) { t0 = millis(); unsigned long sec = millis() / 1000; bi.lastUp = sec; Serial.print("up="); Serial.print(sec / 86400); Serial.print("d "); Serial.print((sec % 86400) / 3600); Serial.print(":"); Serial.print((sec % 3600) / 60); Serial.print(":"); Serial.println(sec % 60); }}ESP.getResetReason() возвращает строку — шильдик последней перезагрузки. Словарь ядра короткий: Power on — подали питание; External System — дёрнули ножку reset; Software/System restart — рестарт по команде: ESP.restart(), заливка прошивки, кнопка сброса в мониторе; Exception — падение кода с трассировкой в мониторе, чаще всего чужой указатель; Hardware Watchdog и Software Watchdog — сторож усыпил плату, потому что код перестал отдавать такты; Deep-Sleep Wake — плановый выход из глубокого сна. Расшифровку падений даёт ESP.getResetInfo(): там код исключения и адрес, по которому оно случилось.
Шильдик живёт один раз за включение: следующий рестарт перепишет регистр, и улика сгорит вместе с ним. Мой порядок — первой строкой setup() причина уходит в переменную и в журнал, дальше скетч работает с копией. Для сна на ESP32 причины считываются иначе — esp_sleep_get_wakeup_cause, я разбирал его в уроке про кнопку и причины пробуждения; шильдики getResetReason — про бодрствующую жизнь и её смерти.
После Exception в мониторе остаётся богатый улов: число в скобках — код исключения, ниже — стек с адресами. Адреса сами по себе молчат, но их декодирует утилита EspExceptionDecoder для Arduino IDE: она превращает стену шестнадцатеричных чисел в имена функций вашего скетча, и виновник находится за минуты. Ставить декодер стоит заранее: в ночь, когда узел падает третий раз подряд, ставить его уже некогда — журнал пишется в общий файл, а искать дешифратор в три часа ночи — сомнительное удовольствие.
| Шильдик | Что случилось | Порядок расследования |
|---|---|---|
| Power on | включили питание или оно проседало | блок питания, кабель, контакт USB |
| Software/System restart | команда ESP.restart или заливка прошивки | сверить, кто зовёт restart в коде |
| Hardware/Software Watchdog | код не отдавал такты сторожу | искать бесконечное ожидание без yield |
| Exception | падение по адресу: чужой указатель, куча | читать трассировку и getResetInfo |
| Deep-Sleep Wake | выход из глубокого сна | норма для батарейного узла |
Сторож: рестарт как услуга
Сторож — контрагент, который рестартует плату, когда loop перестал дышать. На 8266 их два: софт-сторож ждёт около трёх секунд, аппаратный добивает к восьмой. Кормятся они сами — через yield(), который ядро вставляет в delay и в каждый оборот loop. Проблема начинается там, где код не выходит из цикла: while (!client.connected()) {} без delay — самый короткий способ увидеть Soft WDT reset в мониторе. Мой гаражный случай был ровно таким: WiFi-клиент зависал в переподключении, узел рестартовал, подключался и висел снова; аптайм не переваливал за пять минут, и сама эта кривая была диагнозом раньше всякого шильдика.
Лечение: в любое длинное ожидание — delay(1) или yield(), они кормят сторожа и заодно дают сети доработать; зацикленное ожидание вообще переписывать по расписанию, как в уроке про планировщик задач. ESP.wdtDisable() выключает софт-сторожа — на столе при отладке я этим пользуюсь, но держать сторожа выключенным на устройстве, которое должно жить месяцами, значит снять последнюю страховку. Родственник со стороны Uno разобран в уроке про watchdog; механика та же, кормить приходится чаще.
Счётчик перезагрузок в обычной переменной бессмыслен: она умирает вместе с тем рестартом, который считает. Носитель на 8266 — полкилобайта RTC-памяти пользователя: ESP.rtcUserMemoryRead и ESP.rtcUserMemoryWrite кладут туда структуру, и байты переживают рестарт, глубокий сон и нажатие reset — умирают только при снятии питания. Схема со сторожем-магическим числом переехала из урока про счётчик снов ESP32: magic не совпал — память свежая, счётчики обнуляются; у ESP32 та же идея живёт на восьми килобайтах с RTC_DATA_ATTR.
Статистике, которая обязана переживать и снятие питания, дорога в EEPROM — begin и commit из урока про счётчик включений, износ ячейки при записи раз в сутки — копейки. Итоговый журнал гаражного узла у меня из трёх строк: reason= с шильдиком, boots= из RTC, up= раз в секунду. Они превратили загадочные дыры в конкретную работу: рестарты совпали по времени с включением компрессора в соседнем боксе, и шильдик Power on после каждой такой смерти указал на просадки питания, а не на мой код.
Журнал ночного узла я читаю по трём паттернам, и каждый указывает на своего виновника. Утренняя строка reason=Power on при чистом питании — кто-то выключал вилку: ночью мыла коридор уборщица, и вопрос закрылся без осциллографа. Software/System restart среди дня — мой же код зовёт ESP.restart по расписанию переподключения: сверяю время с настройками. А вот boots, выросший за ночь на шесть при аптаймах до пяти минут, — это круговой рестарт: сторож гонит плату по кругу, и я иду искать зацикленное ожидание. Три паттерна закрывают девять случаев из десяти — остальные попадают в таблицу выше и разбираются по трассировке.
Стенд: аптайм в симуляторе
В стенде формат виден без суточного ожидания: нажмите кнопку D2 и следите, как 0d 00:00 за минуту превращается в 0d 01:00 — ускоренные часы складывают секунды в минуты ровно по той же арифметике делений, что и в скетче для платы. Сердцебиение при этом продолжает дышать в реальном ритме: светодиод — единственное, что живёт в настоящем времени, всё остальное в мониторе — виртуальные часы. На плате строки пойдут раз в настоящую секунду, и к утру журнал сам покажет и 0d 08:41:22, и момент, когда после рестарта вместо неё появится reason=.
- Аптайм — millis()/1000, разложенное на дни, часы и минуты; строка up= раз в секунду показывает, что loop жив
- Потолок millis() — 32 бита без знака, 49,7 суток: после переполнения наивный аптайм начинает врать с нуля
- ESP.getResetReason() даёт шильдик: Power on, External System, Software/System restart, Exception, Hardware/Software Watchdog, Deep-Sleep Wake — читается первой строкой setup()
- Сторож двухэшелонный: софт ждёт около трёх секунд, аппаратный — до восьми; кормится через yield и delay внутри loop
- Счётчик рестартов живёт в RTC-памяти пользователя (ESP.rtcUserMemoryRead/Write, 512 байт) со сторожем-магическим числом
- 1. Какой шильдик вернёт getResetReason после того, как вы сами вызвали ESP.restart()?
- 2. Аптайм на millis() врёт после какого срока?
- 3. Код висит в while (!client.connected()) {} без delay. Что будет на 8266?
- 4. Где счётчик перезагрузок переживёт и рестарт, и снятие питания?
- 5. Симптом: скетч стартует, живёт пару секунд и рестартует по кругу; мелькает Soft WDT reset. Первое действие?
Частые вопросы по теме урока
- Какой шильдик вернёт getResetReason после того, как вы сами вызвали ESP.restart()?
- Рестарт по команде ядра помечается как Software/System restart. Power on означает подачу питания, сторожевые шильдики — зависание кода, Exception — падение по адресу. Software/System restart
- Аптайм на millis() врёт после какого срока?
- 32 бита без знака дают 4 294 967 295 миллисекунд — 49,7 суток. Дальше счётчик стартует с нуля; для долгих устройств время берут из сети по NTP. через 49,7 суток — переполнение uint32
- Код висит в while (!client.connected()) {} без delay. Что будет на 8266?
- Сторож кормится через yield и delay, которые в пустом цикле не вызываются; около трёх секунд молчания — рестарт, и так по кругу, пока цикл не исправлен. софт-сторож рестартует плату примерно через три секунды
- Где счётчик перезагрузок переживёт и рестарт, и снятие питания?
- RTC-память переживает рестарт и сон, но умирает при снятии питания; EEPROM во флеше держит всё. Обычные переменные не переживают и рестарта. в EEPROM со begin и commit
- Симптом: скетч стартует, живёт пару секунд и рестартует по кругу; мелькает Soft WDT reset. Первое действие?
- Круговой рестарт с Soft WDT reset — классика зацикленного кода: пустой while не кормит сторожа. yield() или delay(1) в каждой итерации ожидания закрывают вопрос; отключать сторожа насовсем я не советую. искать бесконечное ожидание без yield или delay