Экран над воротами гаража я собрал ещё в уроке про часы на LCD: время по центру, дата и температура снизу. Зимой выяснилось, что над воротами востребован другой прибор: соседи спрашивали, сколько осталось до полуночи, а я честно показывал текущее время и заставлял их вычитать в голове. Календарный отсчёт «сколько дней до праздника» у меня тоже был — таблица накопленных дней из урока про календарь праздников считает с точностью до суток. Для новогодней ночи этого мало: хочется видеть часы и минуты, а за десяток секунд до нуля — услышать зуммер.
Здесь собираю именно такой прибор: цель задана константой DateTime(2026, 1, 1, 0, 0, 0), остаток считается вычитанием двух unix-чисел — без TimeSpan и без календарных таблиц; на LCD 16x2 уходят две строки, зуммер на D8 отбивает последние десять секунд, кнопка на D2 включает тихий режим. Это третий взгляд на арифметику времени в разделе: TimeSpan считает интервалы от текущего момента, календарь — по дням года, а тут момент пришёл извне, готовой датой, и против него работает одно вычитание.
Цель — константа, остаток — одно вычитание
Целевая дата в скетче — обычный объект DateTime из шести чисел: DateTime(2026, 1, 1, 0, 0, 0). У него, как у любого момента, есть вторая форма — unixtime, количество секунд с 1970 года, которое библиотека считает из полей на лету; почему форма вторая и где у неё границы — в уроке про unixtime. Дальше арифметика второго класса: из unix-числа цели вычитаю unix-число текущего момента — остаток получается одним числом в секундах. Никаких календарных правил в скетче нет: високосный февраль, разная длина месяцев и переводы часов уже зашиты в пересчёт библиотеки.
Остаток раскладываю на дни, часы, минуты и секунды четырьмя делениями с остатком: дни — left / 86400, из остатка — часы, из него — минуты, последнее деление отдаёт секунды. Три константы стоят того, чтобы их помнить: сутки 86400 секунд, час 3600, минута 60. А рядом с ними полезно держать пары из таблицы ниже: любой интервал отсчёта записывается и библиотечным конструктором, и числом секунд, с которым он равен. Порядок аргументов TimeSpan — дни, часы, минуты, секунды; перепутать легко, поэтому в таблице каждая строка показана в обеих формах.
| Интервал | Аргументы TimeSpan | Секунд | Где в отсчёте |
|---|---|---|---|
| 10 секунд | TimeSpan(0, 0, 0, 10) | 10 | финальная фаза: порог зуммера |
| полчаса | TimeSpan(0, 0, 30, 0) | 1800 | порог «цель близко» для смены вида экрана |
| час | TimeSpan(0, 1, 0, 0) | 3600 | разложение остатка: делю на 3600 |
| сутки | TimeSpan(1, 0, 0, 0) | 86400 | старший разряд остатка: left / 86400 |
#include <Wire.h>#include <RTClib.h>#include <LiquidCrystal_I2C.h> RTC_DS3231 rtc;LiquidCrystal_I2C lcd(0x27, 16, 2);const int BUZZER = 8;const int BTN = 2; int prevS = -1; // секунда прошлого кадраint finale = 0; // фанфара уже сыграла void z2(int v) { if (v < 10) { lcd.print("0"); } lcd.print(v);} void setup() { Serial.begin(9600); pinMode(BUZZER, OUTPUT); pinMode(BTN, INPUT_PULLUP); lcd.init(); lcd.backlight(); rtc.begin(); // время в чипе живёт от батарейки: adjust при каждой загрузке не нужен} void loop() { DateTime now = rtc.now(); DateTime target = DateTime(2026, 1, 1, 0, 0, 0); unsigned long tS = target.unixtime(); unsigned long nS = now.unixtime(); int mute = digitalRead(BTN) == LOW; // держат кнопку — тихий режим if (nS >= tS) { if (finale == 0) { finale = 1; lcd.setCursor(0, 0); lcd.print("S NOVYM GODOM! "); lcd.setCursor(0, 1); lcd.print("2026! "); if (mute == 0) { tone(BUZZER, 1046, 200); // до-ми-соль вверх: фанфара delay(250); tone(BUZZER, 1318, 200); delay(250); tone(BUZZER, 1568, 400); } Serial.println("S NOVYM GODOM 2026!"); } delay(100); } else { unsigned long left = tS - nS; int s = now.second(); if (s != prevS) { // раз в секунду: экран и звук prevS = s; int d = left / 86400; int h = (left % 86400) / 3600; int m = (left % 3600) / 60; int sec = left % 60; lcd.setCursor(0, 0); lcd.print("Novyi god cherez"); lcd.setCursor(0, 1); lcd.print(d); lcd.print("d "); z2(h); lcd.print(":"); z2(m); lcd.print(":"); z2(sec); lcd.print(" "); Serial.print("ostalos "); Serial.print(left); Serial.println(" s"); if (left <= 10 && mute == 0) { tone(BUZZER, 2000, 250); } } delay(100); }}Экранная дисциплина тут та же, что в часах на LCD: перерисовка поверх, без clear(), и одна перерисовка в секунду, а не в каждый проход loop. Строка 0 в отсчёте — ровно шестнадцать клеток: Novyi god cherez. Строка 1 собрана из переменных по ширине кусков — дни занимают одну-две клетки, — поэтому хвост из пяти пробелов держит строку постоянной длины и не пускает призраков прошлых кадров. Кириллицы на HD44780 без перепрошивки знакогенератора нет, потому поздравление латиницей: S NOVYM GODOM. Почему после короткой строки надо дочищать клетки — отдельная история про мерцание и хвосты.
Финальная фаза прибита к разности: пока left больше нуля, но не больше десяти, каждый тик секунды даёт писк 250 мс на 2000 Гц — резонансная зона пьезо, самый громкий регистр. В сам момент нуля играется фанфара из трёх нот — 1046, 1318, 1568 герц, до-ми-соль, — и она честно блокирует loop на секунду: три tone с выдержками подряд. Для табло это неважно, после полуночи на экране всё равно ничего не меняется; но если прикрутите сюда ещё дела — переносите мелодию в millis-планировщик задач. Кнопка на D2 — тихий режим: держите её, и бипы с фанфарой замолчат, а экран продолжит считать.
Симулятор: подведите часы к полуночи
В сим-скетче время подведено строкой rtc.adjust(DateTime(2025, 12, 31, 23, 59, 45)) — за пятнадцать секунд до цели. Часы в симуляторе идут по-настоящему, секунды тикают, поэтому отсчёт доживает до нуля на глазах: пятнадцать секунд тишины, десять пиков и фанфара. Кнопка на макетке — тот самый тихий режим. Хочется сразу к финалу — подвесьте время в виджете часов ближе к полуночи или поправьте adjust и перезапустите. Макетка скромная: часы на I2C, зуммер на D8, кнопка на D2 — LCD поднимается строкой LiquidCrystal_I2C прямо в скетче, отдельного провода ему не нужно.
- Цель — готовый DateTime константой; остаток — разность двух unixtime: одно вычитание вместо календарных таблиц
- Разложение остатка — четыре деления с остатком: 86400 на сутки, 3600 на час, 60 на минуту
- Беззнаковое вычитание после цели оборачивается через ноль в 4,29 млрд секунд — страж if (nS >= tS) стоит до вычитания
- LCD перерисовывается поверх, раз в секунду; строки добиваются пробелами до постоянной ширины
- Финал: писк 250 мс на каждый тик последних десяти секунд, фанфара из трёх нот в ноль, кнопка глушит звук
- 1. Как из двух объектов DateTime получить остаток до цели в секундах?
- 2. Сколько секунд в TimeSpan(0, 0, 10, 0)?
- 3. Что окажется в left после полуночи, если стража if (nS >= tS) нет?
- 4. Почему фанфара в момент нуля блокирует loop на секунду и когда это допустимо?
- 5. Кнопка на D2 в новогоднем табло делает...
Частые вопросы по теме урока
- Как из двух объектов DateTime получить остаток до цели в секундах?
- Оба момента переводятся в секунды с 1970 методом unixtime(), и остаток — одно вычитание. Календарные правила внутри пересчёта уже учитывает библиотека. вычесть unixtime текущего момента из unixtime цели
- Сколько секунд в TimeSpan(0, 0, 10, 0)?
- Конструктор принимает дни, часы, минуты, секунды. Тройка (0, 0, 10, 0) — десять минут, то есть 600 секунд; порядок легко перепутать, поэтому спорные места пишут с нулями во всех четырёх позициях. 600 секунд: порядок аргументов дни, часы, минуты, секунды
- Что окажется в left после полуночи, если стража if (nS >= tS) нет?
- unsigned long не умеет минуса: nS больше tS даёт оборот через ноль и гигантский остаток. Страж переключает скетч на финал ещё до вычитания. число около 4 294 966 000 секунд — беззнаковая разность оборачивается через ноль
- Почему фанфара в момент нуля блокирует loop на секунду и когда это допустимо?
- Фанфара — три вызова tone с delay между ними. Когда после финала на экране ничего не меняется, секундная блокировка безвредна; для живых дел мелодию переносят в millis-планировщик. три tone с выдержками подряд занимают секунду; для табло после полуночи это безразлично
- Кнопка на D2 в новогоднем табло делает...
- digitalRead в каждом кадре даёт флаг mute: при зажатой кнопке звук не вызывается, а логика отсчёта продолжается. Ночной город скажет спасибо. включает тихий режим: глушит бипы и фанфару, экран считает дальше