Поливалка должна была включаться раз в четырнадцать суток, и я по-программистски честно хранил дату последнего полива: число, месяц, год — три поля в EEPROM. Арифметика «прибавить 14 дней» рассыпалась на глазах: 20 января плюс 14 — это 34 января, которого не существует; я завёл таблицу длин месяцев, потом поправил февраль високосного года, а потом в журнале нашёл полив 31 декабря, который спокойно перешёл в 45-е декабря. Три вложенных исправления ради одного интервала. Спасло одно число: секунды с 1970 года, в которых «через четырнадцать суток» — это просто плюс 1 209 600. Заодно сэкономил память: три поля по два байта — шесть байтов на момент, а unsigned long — четыре, и одно число вместо трёх означает один блок сравнения вместо трёх вложенных if.
Считать от эпохи можно было уже тогда: часы к тому моменту умели узнавать который час — установка времени была пройдена, объект DateTime уже в руках, как разобрано в уроке про разбор DateTime. Оставалось увидеть, что у момента есть вторая форма — без месяцев и дней недели, чистая арифметика. В этом уроке разберём unixtime как систему координат: откуда отсчитывается эпоха и почему формат не знает про пояса и календари, как из двух чисел получить длительность и день недели, где у этой красоты границы — знаменитый 2038 год и тихая ловушка 86400 на Uno.
Отсчёт от полуночи 1 января 1970
unixtime — количество секунд, прошедших с 00:00:00 1 января 1970 года по универсальному координированному времени. Этот стартовый момент называют эпохой, и выбран он не по календарной логике, а по инженерной удобности: круглый десятилетний масштаб, ничего до него не нужно. Внутри формата мир устроен просто: каждые сутки — ровно 86400 секунд, час — 3600, минута — 60. Никаких часовых поясов, переводов стрелок и разной длины месяцев; секунды координации формат тоже пропускает, поэтому раз в несколько лет unix-время и официальное UTC расходятся на секунду — для Arduino-задач это ничто.
Ориентиры, которые стоит держать в голове: миллиард секунд — утро 9 сентября 2001 года, два миллиарда — 18 мая 2033 года, раннее утро; сейчас число перевалило за 1,7 миллиарда и всегда выглядит как десять цифр. Часовой пояс в число не зашит: в Москве, Лондоне и на орбите момент один и тот же — пояс появляется только когда вы печатаете дату человеку, и вы это делаете сами, как договорились в уроке про объект DateTime. Вдобавок у метода unixtime() важное свойство: он не хранится в DS3231 — библиотека считает его из полей даты на лету при каждом вызове. Чип хранит календарь, число — заслуга библиотеки, поэтому печать в журнал я веду в обеих формах: строку для глаз и число для скрипта анализа — они соседствуют в одной строке журнала и не мешают друг другу.
Обратное направление тоже существует: на плате конструктор DateTime(1735689600UL) собирает объект даты из числа — так из unix-секунд получаются год, месяц и день без всяких таблиц. Честная оговорка про симулятор: движок этот одинарный конструктор не разбирает — он понимает DateTime только из двух строк или шести чисел, поэтому обратную сборку показываем в скетче для платы, а в симуляторе считаем разностями и остатками. В диапазоне дат от 1970 до 2106 года арифметика формата честно учитывает високосность: 2024 и 2028 — високосные, а 2100 — нет, потому что делится на сто и не делится на четыреста; считать это вручную не придётся ни разу.
День недели из числа — бесплатное приложение. День эпохи, 1 января 1970 года, был четвергом, поэтому при нумерации с понедельника-нуля день недели равен (дней_с_эпохи + 3) % 7, а при воскресенье-нулевой нумерации, как в RTClib на плате, — (дней_с_эпохи + 4) % 7. Проверим на числах: 1 735 689 600 — полночь 1 января 2025 UTC; делим на 86 400, получаем ровно 20 089 полных суток; (20 089 + 3) % 7 = 2 — среда по понедельничной нумерации, (20 089 + 4) % 7 = 3 — та же среда по воскресеньичной. Одна проверка на известной дате — и формуле можно верить. Этим приёмом мы займёмся плотно в уроке про дни недели и месяцы, где калибровка станет главной темой.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc; void setup() { Serial.begin(9600); rtc.begin(); unsigned long moment = 1735689600UL; // полночь 1 января 2025 UTC DateTime ny = DateTime(moment); // число -> дата Serial.print("iz chisla: "); Serial.print(ny.year()); Serial.print("-"); Serial.print(ny.month()); Serial.print("-"); Serial.println(ny.day()); unsigned long u = rtc.now().unixtime(); unsigned long days = u / 86400UL; // полных суток с эпохи Serial.print("sutok s 1970: "); Serial.println(days); Serial.print("den nedeli (0=Vs): "); Serial.println((days + 4) % 7);} void loop() { Serial.print("u="); Serial.println(rtc.now().unixtime()); delay(5000);}Арифметика без календаря
Практика строится на трёх операциях. Разность двух unixtime — длительность между моментами: 1 767 225 540 минус 1 767 214 540 равно 11 000 секунд, три часа и чуть больше, и это одинаково верно для соседних минут и для записей годичной давности. Прибавление — сдвиг: плюс 86400UL — завтра в это же время, плюс 604800UL — через неделю, и граница месяца там не нужна в принципе. Разложение — деление с остатком: полных суток d / 86400UL, часов в неполном дне (d % 86400UL) / 3600UL, минут — (d % 3600UL) / 60UL. Три строки вместо таблицы длин месяцев. Планировщик срабатываний на этих числах строится так же, как таймеры без delay — приём из урока про millis вместо delay, только отсчёт идёт от эпохи, а не от включения платы.
Пример из симулятора: интервал до полуночи 2027 года вышел 9 163 766 секунд — командой разложения это 106 полных суток, 1 час и 29 минут; остаток 26 секунд ни на что в расписании не влияет. Границы формата видны в этой же арифметике: знаковое 32-битное число кончается значением 2 147 483 647, и это 03:14:07 UTC 19 января 2038 года — та самая проблема 2038 года; беззнаковое живёт до февраля 2106-го. Для прибора, который строится сегодня, обе границы далеки — опасность не в формате, а в ваших переменных, о чём ниже.
Масштабы для бытовых проектов удобно помнить наизусть: сутки 86 400, неделя 604 800, месяц в тридцать дней 2 592 000, год из 365 дней 31 536 000 секунд. Интервал полива из начала урока — 14 суток — это 1 209 600 секунд, и один раз посчитав, вы перестаёте бояться календаря. Для журналов связка идеальна: в EEPROM рядом с человеческой датой живёт unix-число — механику записи с бережным расходом циклов разобрали в уроке про счётчик включений; строку читает человек, разность — скрипт. Одна оговорка: unix-число меняется каждую секунду, поэтому логгер, пишущий метку в одни и те же четыре байта каждую минуту, истощит ячейку за 69 дней — сто тысяч циклов на байт делятся на 1440 записей в сутки. А печать в монитор парами имя=значение дружит с Serial Plotter, как в уроке про графики, — график unix-счётчика в живом приборе это прямая под 45 градусов, по изломам которой видно зависания.
| Интервал | Секунд | Что вписать в скетч |
|---|---|---|
| час / сутки | 3 600 / 86 400 | 3600UL / 86400UL |
| неделя | 604 800 | 604800UL — межполивные 7 суток |
| год из 365 дней | 31 536 000 | 31536000UL — юбилеи устройства |
| граница знакового 32 бит | 2 147 483 647 | 19.01.2038 03:14:07 UTC — предел long |
| граница беззнакового | 4 294 967 295 | февраль 2106 — предел unsigned long |
- Вычислите командой dow сегодняшнюю дату и сверьте с календарём — формула калибруется за одну проверку.
- Посчитайте diff до полуночи наступающего Нового года: найдите число момента и разложите интервал на сутки, часы и минуты.
- Прибавьте к текущему unixtime 604800UL и угадайте, какой это будет день недели, — потом проверьте командой dow.
- В скетче для платы соберите из DateTime(moment) дату своего рождения по числу эпохи — обратная сборка работает на любом историческом моменте после 1970 года.
- unixtime — секунды с 00:00:00 UTC 1 января 1970; сутки ровно 86400, пояса и календари в число не зашиты
- Разность двух чисел — длительность, прибавление — сдвиг через границы месяцев без всяких таблиц
- День недели из числа: (дней + 3) % 7 при Пн=0 и (дней + 4) % 7 при Вс=0; калибруйте по известной дате
- На плате DateTime(число) собирает дату обратно из unix-секунд; в симуляторе конструктор не разобран — считаем разностями
- Календарные константы — только с UL: 86400 не влезает в int на Uno, знаковый long кончится 19 января 2038
- unixtime не хранится в DS3231 — RTClib считает его из полей на лету при каждом вызове
- 1. От какой точки отсчитывается unixtime?
- 2. Сколько секунд в сутках по мнению unix-формата?
- 3. Как получить «через 14 суток» из unixtime?
- 4. Что случится с unixtime в переменной signed long 19 января 2038 года?
- 5. Почему выражение 14 * 86400 опасно на Uno?
Одно число заменило таблицу месяцев, високосные поправки и 45-е декабря: храните момент в unsigned long, считайте разности, разлагайте делением с остатком. Осталась последняя ступенька между машиной и человеком: превратить момент в строку, которую приятно читать, — со словом «среда» вместо тройки и «сентября» вместо девятки. Прямая линия счётчика на графике — заодно лучший тест живучести прибора: излом виден раньше, чем отказ. В следующем уроке собираем красивые даты из массивов имён и ловим главную граблю нумерации дней недели.
Частые вопросы по теме урока
- От какой точки отсчитывается unixtime?
- Эпоха — 00:00:00 UTC 1 января 1970. Число секунд от неё и есть unixtime, одинаковое для всей планеты. От полуночи 1 января 1970 UTC
- Сколько секунд в сутках по мнению unix-формата?
- Формат устроен просто: каждый день ровно 24x60x60 = 86400 секунд, без поясов, стрелок и секунд координации. 86 400 — всегда
- Как получить «через 14 суток» из unixtime?
- 14 суток = 14 x 86400 = 1 209 600 секунд. Прибавление сдвигает момент через любые границы месяцев без календарной логики. Прибавить 1209600UL
- Что случится с unixtime в переменной signed long 19 января 2038 года?
- Знаковый 32-битный счётчик кончается в 2 147 483 647 — 03:14:07 UTC 19.01.2038. Держите unixtime в unsigned long: тот тянет до 2106 года. Старший бит сделает значение отрицательным
- Почему выражение 14 * 86400 опасно на Uno?
- Максимум int на Uno — 32767. Пишите 86400UL: беззнаковые длинные константы разворачивают арифметику в 32 бита. int 16-битный: 86400 не помещается, результат — мусор