Тепличный логгер месяц жил по схеме «пробудился — подтянул время по NTP — поработал — уснул на час». Работало, пока я не присмотрелся к журналу: после каждого пробуждения плата тратила три секунды на WiFi и сессию к серверу времени — а время-то и не сбрасывалось. RTC-таймер чипа тикает сквозь глубокий сон, и time() после timer-пробуждения продолжается с секунды засыпания. Три тысячи лишних синхронизаций на месяце — тоже бюджет: каждая с радио на полную мощность. Исправил на одну пересинхронизацию в сутки — и лог стал честнее, и батарейка дышит легче.
В этом уроке — вторая ступень работы со временем после урока про configTime и epoch: там была математика деления, здесь — зоны строкой через configTzTime, структура tm по полям и strftime для даты в человеческом виде. Главная развилка, на которой спотыкаются: когда time() после пробуждения живой, а когда честный 1970-й, и как порогом на год отлавливать несвежие часы. Плюс дрейф: время во сне ведёт медленный генератор, и его надо знать в лицо.
RTC-таймер тикает во сне
Время в ESP32 ведут двое. Системные часы, которые отдаёт time(), во сне обесточиваются вместе с ядрами — но базу времени хранит RTC-таймер из RTC-домена: низкопотребляющий счётчик, который после пробуждения возвращает своё значение системным часам. Итог по причинам старта: пробуждение по таймеру или по пину из глубокого сна — time() продолжается с секунды засыпания плюс длительность сна; ресет кнопкой EN, снятие питания, сбой по сторожу — 1970-й, база потеряна. При этом millis() после любого сна стартует с нуля — планировщики на millis после пробуждения перезапускаются заново.
Точность этой базы упирается в генератор. Внутренний RC около 150 кГц гуляет на единицы процентов — за сутки сна время уплывает на минуты; припаянный кварц 32,768 кГц даёт десятки частей на миллион — секунды за сутки. Отсюда практическое правило: время после сна «живое, но кривоватое» — метку в журнал ставить можно, а «включить насос ровно в 18:00» — нельзя: пересинхронизируйтесь по NTP на каждом пробуждении или раз в сутки, это секунды радио против минут дрейфа. Там, где сеть ненадёжна, время держит внешний RTC с батарейкой — DS3231 с его минутой дрейфа за год.
#include <WiFi.h>#include <time.h>#include <esp_sleep.h> void setup() { Serial.begin(115200); delay(500); WiFi.begin("DachaNet", "superParol123"); while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); } Serial.println(); // зона строкой POSIX: знак перевёрнут! MSK-3 = UTC+3 configTzTime("MSK-3", "pool.ntp.org", "time.google.com"); struct tm t; while (!getLocalTime(&t, 2000)) { // ждём первую синхронизацию Serial.println("ntp: zhdyom pervyj sync..."); } esp_sleep_enable_timer_wakeup(60ULL * 60ULL * 1000000ULL); // час Serial.println("chasy zavedeny: cikl raboty, potom son na chas");} void loop() { struct tm t; if (getLocalTime(&t, 100)) { if (t.tm_year + 1900 < 2016) { // порог против 1970-го Serial.println("vremya nesusvezhee: nuzhna sync"); } else { char buf[24]; // dd.mm.yyyy hh:mm:ss + запас strftime(buf, sizeof(buf), "%d.%m.%Y %H:%M:%S", &t); Serial.println(buf); const char* dni[7] = {"Vsk","Pon","Vtr","Sre","Cht","Ptn","Sub"}; Serial.print("den nedeli: "); Serial.println(dni[t.tm_wday]); // tm_wday: 0 = voskresenje } } delay(5000); Serial.println("son na chas..."); Serial.flush(); esp_deep_sleep_start(); // после пробуждения setup() пойдёт заново, // но time() продолжится - sync подтвердит за секунду}configTzTime("MSK-3", ...) задаёт зону строкой POSIX — и вот ловушка, о которую я ударился дважды: знак перевёрнут относительно привычной записи. MSK-3 означает UTC+3, потому что POSIX считает, сколько часов надо прибавить к локальному времени, чтобы получить UTC. Строка зоны умеет и переходы на летнее время там, где оно ещё живёт, — но для России после отмены переводов хватает простого «MSK-3». getLocalTime(&t, 100) заполняет struct tm и ждёт до ста миллисекунд: tm_hour, tm_min, tm_mday — как ждёте; tm_mon — месяцы с НУЛЯ, январь это 0; tm_year — годы от 1900, поэтому прибавка 1900 в проверке свежести; tm_wday — день недели, 0 = воскресенье.
strftime собирает дату в строку по шаблону: %d.%m.%Y %H:%M:%S — буфер 24 байта с запасом под нулевой терминатор. Русские названия дней из коробки он не даст: %A печатает английское имя, поэтому в скетче свой массив из трёх букв по tm_wday. Порог t.tm_year + 1900 < 2016 — тот самый щит от 1970-го: время после ресета без сети пройдёт через getLocalTime, но будет несвежим. И не путайте два хранилища: RTC_DATA_ATTR — это восемь килобайт под ваши переменные, а время ведёт сам RTC-таймер — его заводить и читать отдельно не нужно.
| Старт платы | time() после старта | millis() | Что делать |
|---|---|---|---|
| Пробуждение по таймеру из deep sleep | продолжается: RTC-таймер тикал во сне, но уплыл на единицы процентов | с нуля | NTP-пересинхронизация, планировщик millis перезапустить |
| Пробуждение по пину (ext0/ext1) | то же продолжение: RTC-домен не обесточивался | с нуля | та же схема проверки года |
| Ресет кнопкой EN, снятие питания | 1970-й: база потеряна | с нуля | ждать sync, порог на год перед использованием |
| Сбой по сторожу | 1970-й или сбитый | с нуля | журнал причин рестарта и та же проверка |
- 1. Что будет с time() после пробуждения по таймеру из глубокого сна?
- 2. Что в строке POSIX «MSK-3» означает минус?
- 3. Где в struct tm прячется номер месяца января?
- RTC-таймер ведёт время сквозь глубокий сон: после timer-пробуждения time() продолжается, после снятия питания — 1970-й
- configTzTime("MSK-3"): знак зоны в POSIX перевёрнут, MSK-3 = UTC+3
- struct tm: tm_mon с нуля, tm_year от 1900, tm_wday 0 = воскресенье; strftime собирает дату
- дрейф RC-генератора — минуты за сутки сна: пересинхронизация NTP на каждом пробуждении
- millis() после любого сна стартует с нуля — планировщики millis перезапускать
Частые вопросы по теме урока
- Что будет с time() после пробуждения по таймеру из глубокого сна?
- Базу времени во сне держит RTC-таймер: после timer-пробуждения системные часы продолжаются с момента засыпания. В 1970-й время проваливается при снятии питания, ресете и сбое — там база теряется. продолжится с секунды засыпания плюс длительность сна: RTC-таймер тикал во сне
- Что в строке POSIX «MSK-3» означает минус?
- Знак в POSIX-зоне перевёрнут относительно привычной записи: MSK-3 — это UTC+3. Перепутанный знак уводит часы ровно на удвоенное смещение мимо — шесть часов. UTC+3: POSIX считает, сколько часов прибавить к локальному времени, чтобы получить UTC
- Где в struct tm прячется номер месяца января?
- tm_mon идёт с нуля (январь = 0, декабрь = 11), tm_year — годы от 1900. Обе прибавки нужны при печати и при проверке года против 1970-го. tm_mon = 0: месяцы считаются с нуля