Узел полива в щитке построен на паре ESP32 плюс DS3231, и до недавнего времени эти двое жили врозь. ESP32 брала время из сети: соединение есть — часы точные, моргнул свет — плата перезагрузилась, системное время обнулилось, и журнал родил события 1970 года; ловушку нулевых часов и сам протокол NTP я разбирал в уроке про NTP-клиент на ESP32. DS3231 от батарейки время держала железно, но кто и когда заводил его руками — история умалчивает, и верить показаниям было страшно.
Помирились они на простой иерархии: истина — у сети, кэш — у таблетки. При старте скетч получает точную метку из сети и записывает её в DS3231; дальше часы идут от батарейки с уходом в минуту за год, а калибровка повторяется раз в сутки. Пропала сеть — устройство живёт по RTC. Моргнуло питание — при загрузке всё восстановилось из сети за секунды. В этом уроке собираем схему целиком: скетч для платы с настоящим NTP и симулятор, где роль пакета NTP играет HTTP-запрос.
Кто главный: сервер или таблетка
Роли раскладываем честно. NTP даёт абсолют: десятки миллисекунд точности на обычном домашнем канале, но требует сети и повторяется каждый раз. DS3231 даёт автономность: батарейка переживает и перезагрузку, и месяцы без розетки, а её уход — та самая минута в год, посчитанная в уроке про точность и компенсацию. Задача пары — взять у каждого сильную сторону: сеть калибрует, таблетка хранит. Разнобой сюда закрадывается там, где оба источника начинают правиться независимо: одно время на экране, другое в журнале, третье в брокере — и расследование инцидента превращается в археологию.
Почему бы не синхронизироваться каждый час? Можно, но три причины говорят «реже». Первая — этикет NTP-пула: серверы точного времени кормят миллионы устройств, стек ESP32 после configTime сам повторяет синхронизацию раз в час, и лезть чаще незачем. Вторая — журнал: каждый rtc.adjust() сдвигает время, и в логе появляются дырки или дубли секунд — для отчётов это боль. Третья — бессмысленность: поправлять минуту за год ежечасно смешно. Моё правило: adjust при старте, затем раз в сутки, либо немедленно, если разница с сетевой меткой превысила пять секунд.
Механика записи метки на плате укладывается в пару строк. configTime поднимает фоновую синхронизацию с NTP-сервером; когда time() возвращает число больше миллиарда — метка пришла. Дальше конструктор DateTime принимает unix-время числом: rtc.adjust(DateTime(unix)) — и таблетка говорит тем же временем, что и сервер; полный разбор полей снимка я делал в уроке про объект DateTime. Часовой пояс решите один раз: я держу в RTC локальное время — для часов на LCD и расписаний так проще, а смещение зоны задаю в configTime, как в уроке про NTP.
Практика требует чисел, поэтому посчитаем окно между калибровками. Уход DS3231 при двух промилле — до 0,2 секунды за сутки, до 5 секунд за месяц, минута за год. Суточная калибровка держит расхождение в пределах долей секунды — для расписания полива и меток журнала это с огромным запасом. Даже если сеть уехала на неделю, часы от таблетки уйдут на полторы секунды, и дождеватель этого не заметит. Отсюда спокойствие про порог в пять секунд: он срабатывает не от дрейфа таблетки, а от событий — перезагрузки без сети, ручной подводки, севшего резерва.
Зачем вообще держать время на устройстве, которое само сидит в сети? Для меток. Каждая строка телеметрии выигрывает от честного таймштампа — связка с уроком про публикацию показаний по MQTT даёт готовый конвейер: датчики, метка из RTC, публикация в брокер. Серверу всё равно, откуда взялась метка, а вам через месяц разбора инцидента всё равно, была ли сеть в момент события: журнал, помеченный батарейными часами, внятнее журнала, замолчавшего на время моргания света.
| Источник | Точность | Перезагрузка | Свет погас |
|---|---|---|---|
| millis() | тики, не время суток | обнуляется | теряется |
| time() на ESP32 | десятки мс после NTP | до синхронизации — 1970-й | теряется до NTP |
| NTP | десятки мс | восстанавливается при сети | недоступен |
| DS3231 + CR2032 | около 1 мин в год | продолжает | продолжает |
Схема вечных часов
Скетч для платы читается сверху вниз. WiFi подключаем по классике — механика begin, статусов и ожидания разобрана в уроке про подключение к роутеру. Дальше configTime с двумя серверами и порогом готовности по time(): второй сервер — страховка на случай, когда первый пул молчит. Метка уезжает в RTC одним вызовом: конструктор DateTime от unix-числа RTClib принимает нативно; до и после печатаем показания, чтобы в журнале остался след калибровки. Ветка lostPower() из урока про первый скетч здесь отдельно не нужна: суточная калибровка перезаписывает время и без неё, а когда сети нет, функция честно оставляет часы как есть — батарейка за это время не подведёт.
В loop живут две вещи: суточный таймер на millis и печать времени из DS3231 — ровно тот снимок, который читают будильник на RTC и журнал на карте памяти. Обратите внимание на слово единственная: печать и логика не пишут в часы, только syncRTC() имеет на это право — иначе вернётся спор двух хозяев из заметки выше. Таймер суток я собираю на сравнении millis с константой 86400000 — миллисекунд в сутках; перезагрузка сбрасывает его, но это не страшно: калибровка при старте и так происходит.
А если сети нет в принципе — дача без интернета, гараж за бетонными стенами? Пара продолжает жить: DS3231 тикает от батарейки, и единственная потеря — истинность. Метки журнала согласованы между собой, порядок событий сохранён, а абсолют сдвинут на неизвестную константу — ровно то, что было у всех механических часов до эпохи NTP. Подводится такая система один раз: выставить время в момент, когда вы точно знаете его по наручным часам, и дальше держать режим «сеть есть — калибруй, сети нет — молчи». Тот же скетч работает и с DS1307: меняется только класс объекта, а связка adjust с DateTime никуда не денется.
#include <WiFi.h>#include <time.h>#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;const int MSK = 3 * 3600; // смещение зоны: Москва = UTC + 3 чunsigned long lastSync = 0; void syncRTC() { time_t utc = time(nullptr); // метка от NTP-фона if (utc < 1000000000) { // синхронизации ещё не было Serial.println("NTP molchit - ostavlyaem chasy kak est"); return; } rtc.adjust(DateTime((uint32_t)utc)); Serial.print("RTC podvedeno po NTP: "); Serial.println(rtc.now().text());} void setup() { Serial.begin(115200); WiFi.begin("DachaNet", "superParol123"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); if (rtc.begin() == false) { Serial.println("DS3231 ne vidno: proverte provodku"); while (1) { } } configTime(MSK, 0, "pool.ntp.org", "time.google.com"); while (time(nullptr) < 1000000000) { delay(200); Serial.print("."); } Serial.println(); syncRTC(); // калибровка при старте} void loop() { if (millis() - lastSync > 86400000UL) { // раз в сутки lastSync = millis(); syncRTC(); } DateTime now = rtc.now(); Serial.print(now.year()); Serial.print("-"); Serial.print(now.month()); Serial.print("-"); Serial.print(now.day()); Serial.print(" "); Serial.print(now.hour()); Serial.print(":"); Serial.print(now.minute()); Serial.print(":"); Serial.println(now.second()); delay(1000);}В симуляторе сеть настоящая: подключение к WiFi, адрес, GET-запрос с кодом 200 идут как на плате. Настоящего NTP в движке нет — это UDP-пакет на порту 123, и эмулировать его движок не умеет, поэтому роль метки сервера играет вычисление внутри скетча: стартовая отметка 9 утра 18 июня 2025 года плюс секунды, отмеренные millis(). Эта отметка и уезжает в RTC вызовом adjust — ровно то место скетча, где на плате стоит конструктор DateTime от числа из time(). Ответ HTTP в симуляторе — демо-заглушка с погодой, времени в ней нет: печатаем её в лог как есть и не делаем вид, что разобрали метку из сети. Такой контракт — сеть-транспорт отдельно, метка отдельно — к слову, спасает и на живой плате: если API времени переедет на другой адрес, каркас скетча не заметит подмены.
Что здесь переносится на плату один в один: порядок соединения, ожидание статуса, проверка кода ответа, единственная точка записи, тикающие часы и периодическая перекличка. Что заменяется: источник метки. На плате его даёт configTime с фоновым повтором раз в час, в симуляторе — арифметика от millis(). Смысловая часть — куда и когда писать метку — не меняется вовсе, поэтому скетч из симулятора можно собирать на макетке, глядя на него как на схему движения данных, и только потом подставлять настоящий источник времени.
Эксперименты
- Следите за порядком в логе: сначала подключение WiFi, потом код 200, потом метка в RTC — ровно так же уедет на плату.
- Перезапустите симуляцию: часы снова выставятся по метке, потому что при старте скетч калибрует RTC — потеря питания для этой схемы не событие.
- Сравните время до и после строки с меткой: показания браузера сменятся серверной отметкой.
- Убедитесь, что часы тикают между перекличками — это работа батарейки, сеть в эти секунды не нужна.
- Иерархия пары: истина — у NTP, кэш — у DS3231; калибровка при старте и раз в сутки, порог срочности — 5 секунд
- На плате: configTime, порог time() больше миллиарда, затем rtc.adjust(DateTime(unix))
- Единственная точка записи времени в скетче — функция syncRTC(); каждый лишний adjust рвёт журнал дырками и дублями
- Пропажа сети или питания не страшна: между калибровками время ведёт батарейка с уходом в минуту за год
- В симуляторе NTP изображают GET-запрос и метка, вычисленная скетчем, — движок не умеет UDP
- 1. Какая роль отдаётся DS3231 в паре ESP32 плюс NTP?
- 2. Почему не стоит звать adjust каждый час без нужды?
- 3. Что на плате должно случиться перед вызовом rtc.adjust(DateTime(unix))?
- 4. Моргнул свет, ESP32 перезагрузилась, сети нет. Что покажет пара?
- 5. Что в симуляторе изображает настоящий NTP?
Частые вопросы по теме урока
- Какая роль отдаётся DS3231 в паре ESP32 плюс NTP?
- Сеть даёт абсолют, но требует связи. Таблетка хранит время автономно: батарейка переживает перезагрузки и месяцы без розетки, поэтому из пары выкидывать нельзя ни одну половину. Хранение времени между калибровками и при пропадании сети
- Почему не стоит звать adjust каждый час без нужды?
- Каждый adjust сдвигает время, и лог страдает. Уход DS3231 — минута в год, такой разрыв часовая поправка не окупает. В журнале появляются дырки и дубли секунд, плюс лишний трафик к пулу
- Что на плате должно случиться перед вызовом rtc.adjust(DateTime(unix))?
- Число больше миллиарда — признак того, что NTP-метка пришла. Без порога вы запишете в таблетки 1970 год. time() должно вернуть число больше миллиарда
- Моргнул свет, ESP32 перезагрузилась, сети нет. Что покажет пара?
- DS3231 продолжал счёт всё это время. При появлении сети суточная калибровка снова подведёт часы по NTP. Время от DS3231 — батарейка хранила
- Что в симуляторе изображает настоящий NTP?
- Движок не эмулирует UDP: транспорт изображает HTTP-запрос, а метку скетч считает от millis() — на плате это место занимает time(). GET-запрос и метка времени, вычисленная самим скетчем