Весной я получил урок на тридцать рублей: птичник с автодверцей на DS3231 открылся в 04:45 вместо 05:45 — перевод стрелок съехал ровно на час, и куры были в замешательстве не меньше моего. Россия с 2011 года стрелки не переводит, но приборы переезжают: мой читатель из Финляндии прислал ту же жалобу с обратным знаком — его полив включался посреди ночи. Форумы в конце марта покрываются темами одного сорта, и причина всегда одна: про сдвиг стрелок не знает ни чип, ни скетч.
Архитектурный вопрос «кто в системе отвечает за сезонный сдвиг» — с флагом летнего времени и разбором, почему в России перевода нет, — я разбирал в уроке про летнее время и RTC; он полезен, если вы ещё не выбрали схему. Здесь я добираю арифметику: как вычислить последнее воскресенье месяца из day(), как сдвинуть часы ровно один раз и почему будильник на час перехода молчит весной и звонит дважды осенью.
Правило, на котором тренировался мой скетч: последнее воскресенье марта в 03:00 стрелки идут на 04:00, последнее воскресенье октября в 04:00 — обратно на 03:00. Правила по странам различаются — где-то окно 02:00-03:00, как когда-то в России, где-то 03:00-04:00, как в Прибалтике и Финляндии; суть везде одна: весной один час не существует, осенью повторяется. Подставьте свои даты в те же окна — механика не меняется.
Последнее воскресенье из day()
Готовой функции «последнее воскресенье месяца» в RTClib нет, и она не нужна: ключ — dayOfTheWeek(), который библиотека вычисляет из даты. На плате нумерация с воскресенья: если 31 марта — понедельник (1), то последнее воскресенье было 30-го, и формула «31 минус день-недели-31-го» даёт ровно 30. Одна строка вместо циклов, таблиц дней и библиотек календаря.
Проверьте формулу на дате, которую помните: 30 марта 2025 года было воскресенье, и мой скетч это подтвердил с первого прогона. В симуляторе нюанс: движок нумерует дни с понедельника, и формула сдвигается на «31 минус (w плюс 1) по модулю 7»; обе версии я держу в скетчах с явным комментарием о нумерации, а калибровка по известной дате из урока про дни недели и месяцы закрывает вопрос навсегда.
Сезон собирается из границ: с апреля по сентябрь — летнее время, в марте после последнего воскресенья — летнее, в октябре до последнего воскресенья — ещё летнее, с ноября по февраль — зимнее. У марта и октября по 31 дню — приятное совпадение: одна константа 31 обслуживает оба перехода. Високосный февраль на эту арифметику не влияет — там своя история с 29 числом, разобранная в уроке про высокосный год.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;const int LED = 12; // мигает в ночь перехода - видно с порога bool sdvinuto = false; // корректировка этой даты уже сделанаint denMarki = -1; int poslVoskr(int god, int mes) { // нумерация платы RTClib: Вс = 0 DateTime t = DateTime(god, mes, 31, 12, 0, 0); return 31 - t.dayOfTheWeek();} void setup() { Serial.begin(9600); pinMode(LED, OUTPUT); if (rtc.begin() == false) { Serial.println("DS3231 ne najden: SDA A4, SCL A5"); while (1) {} } Serial.println("Pravila: mart 03:00->04:00, oktyabr 04:00->03:00");} void loop() { DateTime now = rtc.now(); if (now.day() != denMarki) { // новый день - флаг корректировки чист denMarki = now.day(); sdvinuto = false; } bool vesna = (now.month() == 3 && now.day() == poslVoskr(now.year(), 3)); bool osen = (now.month() == 10 && now.day() == poslVoskr(now.year(), 10)); if (!sdvinuto && vesna && now.hour() == 3) { // окно 03:00-03:59 rtc.adjust(DateTime(now.unixtime() + 3600L)); // +1 час, один раз sdvinuto = true; digitalWrite(LED, HIGH); Serial.println("Perevod na letnee: +1 chas"); } if (!sdvinuto && osen && now.hour() == 4) { // окно 04:00-04:59 rtc.adjust(DateTime(now.unixtime() - 3600L)); sdvinuto = true; digitalWrite(LED, LOW); Serial.println("Perevod na zimnee: -1 chas"); } delay(500);}Прогоните T26.10.2025: последнее воскресенье октября 2025 — 26-е, дата попадает в день перехода, и скетч печатает окно 04:00-03:00 с пометкой про повторяющийся час. Команда N выдаёт вердикт для сегодняшней даты — с мая по сентябрь он стабильно отвечает «летнее», и это бесплатный тест калибровки формулы. Команда W честно отказывается работать вне окна: так же ведёт себя и скетч на плате, окна которого защищены сами собой.
Ловушка ночи перехода стоит отдельного абзаца. При правиле 03:00-04:00 час с 03:00 до 03:59 весной не существует: сразу после 02:59:59 наступает 04:00:00. Осенью тот же час отыгрывается дважды: 03:59:59 снова превращается в 03:00:00. Будильник на 03:15 весной молчит без всякого сообщения, осенью срабатывает дважды, если нет флага «уже звонил». Если ваш регион переводит в окне 02:00-03:00, те же ловушки относятся к часу 02 — проверьте свои правила, прежде чем ставить окно.
Моё мнение по хранению, и я готов его защищать: в RTC держите UTC, а локальное время вычисляйте при выводе. Лог с метками в UTC не лжёт ни в ночь перехода, ни после: полночь не наступает дважды, и сортировка журнала всегда совпадает с хронологией. Условие, при котором я отказываюсь от этой схемы: прибор без сети и без логов, где пользователь читает время с экрана и правит кнопками — тогда локальное время в чипе проще, а сдвиг раз в год стоит тридцать секунд. Связку «две шкалы на одном приборе» я собирал в уроке про две зоны времени.
Чужие окна: правило 02:00-03:00 и суффиксы секунд
Каркас выше написан под окно 03:00-04:00, но старое российское правило жило в 02:00-03:00, и под него скетч подстраивается заменой одной цифры: весной после 01:59:59 наступает 03:00:00, час 02 вычеркнут из ночи целиком, осенью 03:00 возвращается на 02:00. Окно слушается now.hour() == 2, сдвиг — те же 3600 секунд. Даты у большинства европейских правил общие — последнее воскресенье марта и октября, — а вот час и даже направление различаются: прежде чем копировать скетч, сверьтесь с действующим правилом своего региона на свежих датах, а не на форуме десятилетней давности. Ловушка везде одна: весной один час не существует, осенью идёт дважды, и будильники платят по-разному.
Секунды в этих окнах живут с суффиксами, и это не педантизм. Сутки — 86 400 секунд, «сутки минус час» — 82 800, а int на Uno держит максимум 32 767; без суффикса L или UL такая константа переполняется прямо в выражении, иногда с предупреждением компилятора, иногда молча. Я пишу время всегда длинным: 3600L на час сдвига, 86400UL на сутки, 60UL * 60UL * 24UL — когда хочется, чтобы глаз сам считал. Проверка на плате занимает минуту: печатаете unixtime до и после сдвига, разность обязана быть ровно 3600 — если вышло 3600 плюс мусор старших битов, где-то остался int.
Моё мнение о том, кому вообще нужен автоперевод, я сужу условием. Ставлю его приборам, которых не вижу месяцами: полив на даче, теплица у родственников, птичник — всё, что живёт без присмотра и помнит о расписании. Всё, что перед глазами ежедневно, перевожу руками: два adjust в год по тридцать секунд, зато в скетче нет ни одного окна, которое может сработать не вовремя. Компромисс середины — автоматика с подтверждением: прибор сам заметил ночь перехода, вывел на экран вопрос, вы нажали кнопку. Отдельный случай — платы с сетью: там сезонный сдвиг не нужен вовсе, NTP раз в сутки приносит честное время и все окна исчезают вместе со скетчем, разбор в уроке про NTP и RTC.
Проверить чужое правило можно датой из прошлого: возьмите последнее воскресенье октября прошлого года, подставьте в формулу и сверьте вердикт с тем, что помните. Я прогоняю три даты: день перед переходом, сам переход и день после — на них формулу не обманешь, и заодно видно, не поехали ли границы «до» и «после». Тот же тест в симуляторе закрывается командой T с датой прошлого перехода: календарь движка считает дни недели честно, и если вердикт совпал трижды, арифметику можно не трогать до следующей реформы календаря.
UTC или локальное: где что ломается
Ключевой аргумент за UTC — повторяющиеся и несуществующие метки. Журнал на локальном времени в осеннюю ночь содержит 02:30 дважды, и разборщик не отличит первый проход от второго; в UTC обе записи различаются на час по построению. Цена схемы — сдвиг в каждом месте вывода: одна функция вывода в проекте решает это, а для разбора команд установки времени пригождается парсинг строк из урока про Serial и числа.
Защита от двойного сдвига строится на узком окне, и это стоит проговорить. После корректировки показания уходят из окна: часы показывают 04:xx, когда окно слушает 03:xx, и второй adjust в ту же ночь не случится даже после перезагрузки — а перезагрузка внутри окна до сдвига просто откроет его заново, что и требуется. Флаг с дневным сбросом в моём скетче — страховка для широких окон вида «позже 03:00», где без флага сдвиг повторялся бы до конца суток. Установка времени руками — adjust из урока про установку времени — остаётся запасным входом, если автоматике вы пока не доверяете.
| Стратегия | Что платит | Где ломается |
|---|---|---|
| Локальное время + флаг сдвига | правила в коде, флаг на сутки | широкое окно без флага — сдвиг повторяется |
| UTC в чипе + сдвиг при выводе | сдвиг в каждом месте вывода | будильник обязан сравнивать с локальным, а не с чипом |
| Ручной adjust дважды в год | ноль кода | забывается ровно в год, когда прибор стал важным |
- 1. Когда по правилу урока случается весенний переход?
- 2. Как вычислить последнее воскресенье марта, если dayOfTheWeek() нумерует с воскресенья?
- 3. Почему будильник на 03:15 молчит в ночь весеннего перехода?
- 4. Что защищает скетч от двойного сдвига в ту же ночь?
- 5. Когда хранить в чипе UTC выгоднее локального времени?
- Последнее воскресенье = 31 минус день недели 31-го (на плате Вс=0; в симе Пн=0 и формула со сдвигом)
- Окна перехода: март 03:00-03:59 (+1 час), октябрь 04:00-04:59 (-1 час); правила по странам различаются
- Весной час перехода не существует, осенью повторяется: будильник молчит или звонит дважды
- Защита от двойного сдвига — узкое окно; флаг с дневным сбросом страхует широкие окна
- Для приборов с логами храните UTC и сдвигайте при выводе; для автономного будильника проще локальное
Частые вопросы по теме урока
- Когда по правилу урока случается весенний переход?
- Правило урока: последнее воскресенье марта в 03:00 стрелки идут на 04:00. Правила по странам различаются, но механика окон одна. Последнее воскресенье марта в 03:00 — стрелки на 04:00
- Как вычислить последнее воскресенье марта, если dayOfTheWeek() нумерует с воскресенья?
- Если 31-е — понедельник (1), воскресенье было 30-м: 31-1=30. Формула работает для любого года без таблиц и циклов. 31 минус день недели 31 марта
- Почему будильник на 03:15 молчит в ночь весеннего перехода?
- Весной час перехода выпадает из хода времени, поэтому момент не наступает физически. Осенью тот же час повторяется, и будильник без флага сработает дважды. Час 03:15 при правиле 03:00-04:00 не существует: после 02:59:59 наступает 04:00:00
- Что защищает скетч от двойного сдвига в ту же ночь?
- После +1 часа часы показывают 04:xx, а окно слушает 03:xx — условие ложно. Флаг с дневным сбросом страхует широкие окна вида «позже 03:00». Узкое окно: после adjust показания уходят из окна, второй сдвиг не случится
- Когда хранить в чипе UTC выгоднее локального времени?
- UTC снимает несуществующие и повторяющиеся метки: полночь не наступает дважды, журнал сортируется как хронология. Цена — сдвиг при каждом выводе. Когда есть логи и связь: метки не лгут в ночь перехода