В конце марта форумы по Arduino традиционно покрываются темами одного сорта: полив включился на час раньше, свет в курятнике погас не вовремя, будильник сработал посреди ночи. Причина всегда одна — стрелки перевелись, а скетч об этом не узнал. Вопрос тут не в коде, а в архитектуре: кто в системе отвечает за сезонный сдвиг — модуль часов, скетч или человек с кнопкой? Пока ответ не выбран осознанно, он выбирается сам, обычно в худшей версии: сдвигом в голове хозяина. Каждая такая тема стоит кому-то ночи перепрошивки и ведра воды — и почти всегда лечится одной переменной в скетче.
Сам механизм записи времени и подвод часов через rtc.adjust разобраны в уроке про установку времени — там про то, как и когда писать в чип. Здесь политика: что именно лежит в микросхеме, чего она принципиально не знает и как устроить слой локального времени так, чтобы сезонный перевод стоил одной константы. Две зоны рядом я уже раскладывал в уроке про две зоны времени; повторять его не буду — решаю узкий вопрос именно сезонного сдвига.
Что DS3231 знает про летнее время: ничего
Внутри DS3231 лежат семь полей — секунды, минуты, часы, день недели, число, месяц, год — и батарейное питание, чтобы поля не сбрасывались. Ни таймзоны, ни флага «сейчас летнее», ни встроенного правила перевода в чипе нет: он хранит ровно то, что записали через rtc.adjust, и тикает вперёд с точностью ±2 ppm в тепличном диапазоне температур — уход мы считали в уроке про дрейф DS1307 и DS3231. Записали 06:00 — чип покажет 06:00 хоть в Калининграде, хоть во Владивостоке и никогда не спросит, чьё это время. Это не недоработка, а честность: сдвиг — вопрос к человеку, микросхема знать его не может. Само устройство чипа и его регистры разобраны в уроке про DS3231.
Отсюда три стратегии, и выбирать надо до первого запуска, потому что переделывать больно. Первая: в RTC лежит локальное время, про сдвиг не думаем вовсе — норма для России, где сезонный перевод отменён с 26 октября 2014 года. Вторая: в RTC лежит зона-нейтральное время, а скетч прибавляет сдвиг одной константой или флагом — так делают проекты, которые синхронизируются по NTP или переезжают между зонами. Третья: локальное время в RTC плюс ручной rtc.adjust дважды в год — худший вариант, потому что упирается в память человека, а человек в конце марта на даче занят рассадой, не прошивкой. Синхронизацию по сети со всеми её граблями я считал в уроке про NTP и RTC.
Отдельно про соседей по плате. RTC и millis живут параллельно и не обязаны совпадать: программный счёт плывёт вместе с кварцем самой Arduino, а DS3231 держит время термокомпенсацией — расхождение в минуты за сутки между двумя источниками норма, и в этом смысл урока про millis и RTC. Правило разделения труда простое: календарь и расписания спрашивают только у rtc.now(), короткие паузы и период опроса держат на millis. Склейка «минуту расписания посчитаю из millis» ломается после первого же перезапуска: у millis нет ни даты, ни памяти о вчерашнем дне, а расписание без даты — просто будильник.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc; // Правило ЕС: +1 час в 01:00 UTC последнего воскресенья марта,// обратно - в 01:00 UTC последнего воскресенья октября.bool letneeVremyaEU(const DateTime& d) { byte m = d.month(); if (m > 3 && m < 10) return true; // апрель-сентябрь: точно лето if (m < 3 || m > 10) return false; // ноябрь-февраль: точно зима DateTime kraj = DateTime(d.year(), m, 31); // 31-е число есть и в марте, и в октябре byte posledneeVs = 31 - kraj.dayOfTheWeek(); // в RTClib воскресенье = 0 if (m == 3) return d.day() >= posledneeVs; // после последнего вс - лето return d.day() < posledneeVs; // до последнего вс октября - ещё лето} void setup() { Serial.begin(9600); if (!rtc.begin()) { Serial.println("DS3231 молчит: проверьте шину"); while (true) delay(100); }} void loop() { DateTime now = rtc.now(); // то, что физически лежит в чипе int sdvig = letneeVremyaEU(now) ? 1 : 0; // часы сдвига DateTime lokal = now + TimeSpan(0, sdvig, 0, 0); Serial.print("В чипе: "); Serial.print(now.hour()); Serial.print(":"); if (now.minute() < 10) Serial.print("0"); Serial.print(now.minute()); Serial.print(" Локальное: "); Serial.print(lokal.hour()); Serial.print(":"); if (lokal.minute() < 10) Serial.print("0"); Serial.println(lokal.minute()); // все расписания сравниваем с lokal, а не с now: // сдвиг правится в одной функции, а не в десяти условиях delay(1000);}Формула последнего воскресенья — самое хитрое место: берём 31-е число месяца, спрашиваем у библиотеки день недели и вычитаем из 31. Если 31 марта — среда, последнее воскресенье было 28-го; если само воскресенье — 31-го. Одно предупреждение: в RTClib воскресенье — ноль, а в части других библиотек нумерация начинается с понедельника, и тогда перевод уезжает на неделю. Сверьте нумерацию с документацией своей версии, прежде чем верить формуле на слово.
Слой локального живёт в одном месте: TimeSpan из RTClib прибавляет сдвиг к DateTime, и все сравнения расписаний ведутся с lokal, а не с now. То же правило «сравнивай метки целиком, а не поля по отдельности» разобрано в уроке про сравнение DateTime — здесь оно приобретает вторую причину: после перевода половина прямых сравнений с полями чипа считает по-зимнему, половина по-летнему, и расписание распадается незаметно. Сдвиг, который правится в одной функции, — единственная версия, которую вы вспомните без стыда в следующем марте.
Кто и когда переводит стрелки
Календарь переводов короткий. Россия не переводит часы с 26 октября 2014 года — флаг сезона дачному скетчу не нужен, пока плата не уехала за границу. Европейский союз переводит дважды: последнее воскресенье марта в 01:00 UTC стрелки идут на час вперёд, последнее воскресенье октября в 01:00 UTC — на час назад. По местному времени весной 03:00 становится 04:00, осенью 03:00 возвращается к 02:00. США живут по своему календарю — второй воскресенье марта и первое ноября, — поэтому правило из скетча выше переносится в Америку только с заменой дат. Для самопроверки формулы: последнее воскресенье октября 2026 года — 25-е число, подставьте и сверьте. Слухи о полной отмене перевода в ЕС ходят с 2019 года, но к 2026 стрелки по-прежнему едут: не стройте скетч на слухах, стройте на проверяемой дате.
Практический вывод по стратегии. Плата стоит на даче и ни с чем не синхронизируется — храните локальное время, DST-код выбрасывайте целиком. Плата переезжает, синхронизируется по сети или шлёт метки на сервер — держите в чипе зона-нейтральное время и один сдвиг в скетче. Проверка здравым смыслом: сезонный сдвиг должен правиться в одном месте. Если для этого нужно ехать на дачу с ноутбуком — вы выбрали третью стратегию, ту, что живёт в голове хозяина, и она съест вас в первый же март.
Как проверить свой скетч за пять минут: сдвиньте часы на час — кнопками виджета в лаборатории или rtc.adjust на живой плате — и смотрите, что поедет. Поедет всё: скетч смотрит в чип напрямую, слой локального отсутствует. Поедет только расписание: слой есть, и править нужно именно его. Не поедет ничего: либо вы в России и сдвига нет, либо сдвиг уже сидит в переменной, и этот урок был для вас повторением пройденного. После проверки верните время на место: DS3231 сам не забудет перепутанное время, а расписания на чипе с чужим временем живут до ближайшего перезапуска — то есть вечно.
И про личную дисциплину, раз уж выбрана стратегия с ручным переводом: правило «прошивка проверяется в последнее воскресенье марта и октября» дешевле любой ночной поездки. Повесьте напоминалку туда, где её увидите, — а лучше переведите проект на слой локального времени и вычеркните человека из цепочки отказа.
| Стратегия | Где живёт сдвиг | Когда выбирать |
|---|---|---|
| Локальное в RTC, про сезон не думаем | нигде: сдвига нет | Россия с 26.10.2014, статичная дача |
| Зона-нейтральное время в RTC, сдвиг в скетче | одна переменная или функция | NTP-синхронизация, переезды, метки на сервер |
| Локальное в RTC + adjust дважды в год | в памяти хозяина | никогда: пропущенный перевод сдвигает всё на час |
| Время решает сервер, плате шлют команды | вне платы | связные проекты с базой и логами |
- 1. Что DS3231 знает про таймзоны и сезонный сдвиг?
- 2. Как обстоят дела с сезонным переводом в России?
- 3. Как вычислить дату последнего воскресенья марта?
- 4. Сравнения расписаний идут напрямую с now.hour(). Что будет после перевода часов?
- 5. Почему разницы unixtime не боятся перевода часов?
- DS3231 хранит ровно то, что записали: ни таймзоны, ни флага сезона в чипе нет
- Три стратегии: локальное без сдвига (Россия с 2014), зона-нейтральное время плюс сдвиг в скетче, ручной adjust дважды в год — худший вариант
- Последнее воскресенье месяца = 31 минус день недели 31-го числа; в RTClib воскресенье — ноль
- Все сравнения расписаний — через слой локального времени, не с полями чипа
- Разницы unixtime сдвига не боятся: обе метки одной шкалы, сдвиг сокращается
Частые вопросы по теме урока
- Что DS3231 знает про таймзоны и сезонный сдвиг?
- DS3231 хранит семь полей и тикает их вперёд. Таймзоны и правила перевода — уровень скетча или человека: чип покажет ровно то, что записали. Семь полей — и всё. Ничего: в чипе лежат поля даты-времени, как их записали
- Как обстоят дела с сезонным переводом в России?
- С 26 октября 2014 года стрелки в России стоят на месте. Пока плата не уезжает за границу, вся логика летнего времени ей не нужна. Отменён с 26 октября 2014 года: флаг сезона не нужен
- Как вычислить дату последнего воскресенья марта?
- 31-е число минус номер дня недели 31-го даёт дату последнего воскресенья. В RTClib воскресенье — ноль; в других библиотеках нумерация может начинаться с понедельника, сверьтесь. Для октября логика зеркальна: до 25-го числа 2026 года — ещё лето. 31 минус день недели 31-го числа
- Сравнения расписаний идут напрямую с now.hour(). Что будет после перевода часов?
- Чип хранит записанное время и не знает о переводе. Пока сдвиг не учтён слоем локального времени, каждое сравнение честно работает по старому часу. Слой локального времени возвращает всё на место без правки расписаний. Всё уедет ровно на час: условия продолжают считать по-зимнему
- Почему разницы unixtime не боятся перевода часов?
- Шкала секунд одна для всех меток: если обе взяты до и после перевода, сдвиг входит в обе одинаково и при вычитании исчезает. Перевод правит отображение времени, а не саму шкалу секунд. Обе метки одной шкалы, сдвиг сокращается при вычитании