Табло в витрине — «до Нового года осталось 84 дня 11 часов» — выглядит как игрушка, а собирается из одной операции вычитания. Никаких таблиц на двенадцать месяцев и проверок високосного февраля: RTC-модуль выдаёт время одним числом секунд, и разность двух таких чисел — уже готовый отсчёт. Здесь считаю дни и часы до события — Новый год, день рождения, дедлайн проекта — и вывожу их в Serial раз в секунду. Базовый приём с расписанием на дату уже был в уроке про отсчёт до события, а это место — чистая арифметика unixtime со всеми углами, куда она бьёт. На выходе — строка в мониторе, которую не стыдно показать заказчику, и LED, который загорается за порог до события.
Почему не millis(): отсчёт до даты живёт днями и месяцами, а millis() не переживёт ни перезагрузку, ни сбой питания — механику счётчиков и их пределы я раскладывал в уроке про millis вместо delay. DS3231 с его ±2 ppm уходит на месяц примерно на пять секунд — для табло с точностью до минуты несущественно; даже DS1307 с его минутами в месяц сгодится. Цифры дрейфа обоих чипов за 30 дней я мерил в уроке про дрейф DS1307 против DS3231, здесь вывод один: на дистанции месяца часы реального времени нужны, счётчик от включения — нет.
Разность unixtime: вся математика в одном минусе
unixtime — число секунд, прошедших с полуночи 1 января 1970 года. У любого объекта DateTime в RTClib есть метод unixtime(), и с этого момента календарные вопросы исчезают: дата — просто большое число. Разность celya.unixtime() - now.unixtime() даёт секунды до события, дальше — деление: в сутках 86400 секунд, в часе 3600, в минуте 60. Целочисленное деление на 86400 — дни; остаток от деления, поделённый на 3600, — часы; и так до секунд. Четыре строки вместо календаря.
Целевая дата — тоже DateTime: DateTime celya(2026, 1, 1, 0, 0, 0) — полночь 1 января 2026-го. Год не обязательно вписывать руками: now.year() + 1 даёт следующий Новый год автоматически, и скетч переживёт смену года без правок. Для дня рождения год вообще вторичен, важны месяц и число — как вычислить «следующий» день рождения, когда текущий уже прошёл, показано в уроке про календарь праздников.
Альтернатива без ручных делений — TimeSpan из той же библиотеки: TimeSpan span = celya - now; и у объекта готовы days(), hours(), minutes(), seconds(). Та же математика, спрятанная в библиотеку; механику и цену каждого поля я раскладывал в уроке про TimeSpan. Мой выбор простой: для одной строки берите unixtime, для кода, который будут читать люди, — TimeSpan. На плате разница видна сразу: строка с делениями требует комментария, объект span читается сам без него.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;DateTime celya(2026, 1, 1, 0, 0, 0); // полночь 1 января 2026 void setup() { Serial.begin(9600); if (rtc.begin() == false) { Serial.println("DS3231 ne najden: proverite SDA(A4) i SCL(A5)"); while (1) {} }} void loop() { DateTime now = rtc.now(); long ostalos = celya.unixtime() - now.unixtime(); if (ostalos > 0) { long dney = ostalos / 86400; long chasov = (ostalos % 86400) / 3600; long minut = (ostalos % 3600) / 60; long sekund = ostalos % 60; Serial.print("Do Novogo goda: "); Serial.print(dney); Serial.print(" d "); Serial.print(chasov); Serial.print(" h "); Serial.print(minut); Serial.print(" m "); Serial.print(sekund); Serial.println(" s"); } else { long nazad = 0 - ostalos; Serial.print("Novyi god nastupil "); Serial.print(nazad / 86400); Serial.println(" dnej nazad"); } delay(1000);}Первый подводный камень — тип. На Uno int — 16 бит с потолком 32767: даже одни сутки в 86400 секунды в нём не помещаются, а до Нового года там десятки миллионов секунд. Держите разность в long: 32 бита дают запас до пары миллиардов секунд, то есть 68 лет в обе стороны. С int скетч соберётся молча, а числа уползут в отрицательный бред — компилятор такие вещи не ловит, ловит их только взгляд на монитор. Потолок long — 19 января 2038 года: знаковые 32 бита секунд кончаются в эту дату, но DS3231 и RTClib живут своим календарём до 2100-го, так что бытовым табло запаса хватит на всё.
Второй — прошедшая дата. Наступит 1 января, разность станет отрицательной, и без проверки знака табло покажет «минус один день». Дешёвое правило: if (ostalos > 0) — отсчёт, иначе событие состоялось, и показываете, сколько дней назад. Для дня рождения это не сбой, а ровно нужное поведение: исполнилось три дня назад.
Третий — часовые пояса, и тут честная оговорка. DS3231 хранит то время, которое вы в него записали, без понятия о зонах, и unixtime у RTClib — «наивное»: оно считает локальные секунды так, будто зона всегда UTC. Для отсчёта до локального события это неважно, обе даты в одной системе. Но как только рядом встаёт интернет-время, сдвиг зон всплывает: как подводить RTC к сети без ручного перевода — в уроке про NTP-синхронизацию.
Пороги: табло хочет мигать в последние сутки
Чистые «дни и часы» — ещё не вся задача. Табло обычно подсвечивается за неделю до события и мигает в последние сутки: это пороговые сравнения на той же разности, например if (ostalos < 86400). В симуляторе ниже порог стоит на тридцати сутках, чтобы его можно было потрогать, не дожидаясь декабря; на плате его двигает одна константа. Второй порог ставится на неделю тем же способом: ostalos < 604800 — и у табло два состояния вместо одного.
И про частоту вывода. Печатать отсчёт в каждый круг loop — заспамить монитор сотнями строк в секунду. Раз в секунду достаточно даже для табло с секундами; если секунды на экране не нужны, печатайте при смене минуты — строк становится в шестьдесят раз меньше, а история в мониторе наконец читается.
Порядок в симуляторе. Сразу после старта в мониторе идёт отсчёт до следующего Нового года — движок считает от реальной даты, поэтому числа настоящие. Отправьте CEL с датой из ближайших пары недель: отсчёт перескочит, а когда до цели останется меньше тридцати суток, загорится LED — порог это одна константа, меняется в редакторе за секунду. Поставьте дату из прошлого — получите ветку «событие прошло» с числом дней назад.
На плате ту же роль играет правка константы в DateTime(...) или Serial-команда по всем правилам парсинга — она и есть тема следующего урока про установку времени из монитора. Печатать каждые 250 мс, как в симе, не обязательно: для табло хватает раза в секунду, для тихих проектов — при смене минуты. Заодно решите, что показывать в последний час: моё табло переключалось с дней и часов на часы-минуты-секунды, когда дней оставалось меньше единицы — зрители это чувствуют как приближение.
| Способ посчитать | Код | Когда беру |
|---|---|---|
| Разность unixtime | (celya.unixtime() - now.unixtime()) / 86400 | одна строка, точность до секунды |
| TimeSpan из RTClib | TimeSpan span = celya - now; span.days() | читаемый код: деления спрятаны в библиотеку |
| Полевое вычитание | дни, месяцы, високосные — руками | почти никогда: календарь уже написан |
| Счётчик на millis() | без RTC, с нуля после перезагрузки | не для дат: переживает только минуты |
Что дальше. Табло — самый честный способ проверить часы: повесьте вывод на смену минуты и оставьте плату на сутки. Если утренняя строка совпала с телефоном, часы стоят; если отстали на минуту, проверьте батарейку и вспомните, DS1307 ли у вас на борту — он и уходит быстрее. А когда появится второй отсчёт, день рождения рядом с дедлайном, — сделайте массив целей: цикл из одной строки печатает столько строк, сколько событий, и у каждого свой порог.
- 1. Сколько секунд в сутках и какой тип нужен для их хранения на Uno?
- 2. Что вернёт celya.unixtime() - now.unixtime() после того, как дата события прошла?
- 3. Как автоматически посчитать следующий Новый год, не вписывая год руками?
- 4. Что делает TimeSpan в RTClib для отсчёта?
- 5. Почему для отсчёта до даты берут RTC, а не millis()?
- Разность unixtime — весь календарь в одном минусе: секунды делите на 86400, 3600, 60
- На Uno int — 16 бит: разность секунд живёт в long, иначе числа уползут в минус молча
- Отрицательная разность — не сбой, а сигнал «событие прошло»: покажите дни назад
- Цель, посчитанная в setup, устаревает: пересчитывайте, когда разность ушла в минус
- TimeSpan делает те же деления читаемо — days(), hours(), minutes(), seconds()
Частые вопросы по теме урока
- Сколько секунд в сутках и какой тип нужен для их хранения на Uno?
- В сутках 86400 секунды, а int на Uno — 16 бит с потолком 32767. Разность секунд до события храните в long: 32 бита покрывают десятки миллионов секунд с запасом. 86400, нужен long — int держит максимум 32767
- Что вернёт celya.unixtime() - now.unixtime() после того, как дата события прошла?
- Разность честно уходит в минус. Правильная реакция скетча — ветка для прошедшей даты: показать, сколько дней назад состоялось событие. Отрицательное число секунд
- Как автоматически посчитать следующий Новый год, не вписывая год руками?
- Год берётся из текущего времени и увеличивается на единицу: цель сама переезжает на следующий год после полуночи первого января. Константу в коде пришлось бы править руками. DateTime(now.year() + 1, 1, 1, 0, 0, 0)
- Что делает TimeSpan в RTClib для отсчёта?
- TimeSpan — разность двух DateTime, из которой библиотека достаёт дни, часы, минуты и секунды. Математика та же, что у ручного деления unixtime, только читаемее. Прячет те же деления в days(), hours(), minutes(), seconds()
- Почему для отсчёта до даты берут RTC, а не millis()?
- Отсчёт живёт днями и месяцами, а счётчик millis() стартует с нуля после каждой перезагрузки и ничего не знает о датах. RTC с батарейкой переживает и то и другое. millis() обнуляется при перезагрузке и не знает календаря