Логгер температуры для гаража был моим любимым проектом того месяца: DS18B20 на пине 2, раз в минуту строка в Serial — uptime и градусы. Две недели идеального журнала, полторы тысячи строк. Потом электрик подъезда выключал свет на выходные, и в понедельник я увидел пикантную картину: строка 20160 — «uptime=20161 мин, минус 8 C», а следующая через минуту — «uptime=1 мин, минус 11 C». Плата перезагрузилась без питания, millis() стартовал с нуля, и весь мой журнал превратился в кашу: сортировать такие записи по времени невозможно, потому что время в них — не время, а счётчик работы с последнего включения. Про календарную дату и говорить нечего: её не было ни до аварии, ни после.
Этот урок — про разницу между двумя инструментами, которые новички путают чаще всего. millis() отвечает на вопрос «сколько прошло с запуска», RTC-чип — на вопрос «который час и какое сегодня число». Посчитаем, на что способен счётчик и где он ломается, разберу собственный тупик с EEPROM-костылём, и покажу, как выглядел тот же логгер после переезда на DS3231 — когда мигание света перестало быть катастрофой.
Два разных вопроса: интервал и календарь
millis() возвращает беззнаковое 32-битное число миллисекунд с момента старта платы. Для интервальной логики это идеальный инструмент: мигать светодиодом каждые 800 мс, опрашивать кнопку раз в 50 мс, шевелить сервой раз в 20 мс — всё это живёт на millis() и не блокируется delay(). Мы собирали такой код в уроке про millis вместо delay, и там он был безупречен. Тонкость, о которой стоит знать заранее: 32 бита переполняются на 4 294 967 295 мс, то есть через 49,7 суток непрерывной работы. Пока вы сравниваете разницы через вычитание беззнаковых, переполнение проходит незаметно — но если вы где-то прибегли к знаковому сравнению или длинным циклам, в конце седьмой недели проект защёлкает.
Но у millis() нет даже попытки ответить на календарные вопросы. Спросите у него «сейчас 07:00 или 07:15?» — тишина. Какое число? Какой день недели? Февраль этого года високосный или нет? Счётчик не знает ничего, что случилось до включения платы, и не знает ничего, что не выражается миллисекундами. Это не баг, а конструкция: интервал и момент календаря — разные сущности, и мерить их одним числом не выйдет.
Единственная опора millis() — генератор платы, и его честность ограничена. На большинстве Uno и Nano стоит керамический резонатор с допуском порядка ±0,5% — это до семи минут ухода в сутки. Практическая проверка: я оставил китайскую Uno на сутки рядом с часами телефона; реальный уход составил 3 минуты 40 секунд — 0,25%, в пределах допуска, но для расписания полива это плюс-минус двадцать минут в неделю. Вторая плата в той же партии ушла на 5 минут за те же сутки: разброс между экземплярами больше, чем заявленная погрешность одного из них. Для замеров покороче есть micros() — о его ловушках я писал в уроке про точные замеры, но и он про интервалы, а не про дату.
Ситуация, в которой оказался мой логгер, стандартна для любого чисто-миллисового проекта: выключение питания стирает всё. Причём перезагрузка — не только кнопка Reset и не только пропавший свет.
| Вопрос проекта | Правильный инструмент | Почему |
|---|---|---|
| Мигать светодиодом каждые 800 мс | millis() | интервал внутри одной сессии, календарь не нужен |
| Полив ровно в 07:00 по расписанию | RTC-чип | нужен момент календаря, переживший ночь без питания |
| Замер длительности импульса сенсора | micros() | микросекунды одного события |
| Журнал с датой и временем строки | RTC-чип | после перезагрузки таймштампы должны продолжиться |
| Сколько плате лет без выключения | RTC + EEPROM-счётчик | аптайм и календарь вместе |
Тупик, который я прошёл: EEPROM и смещения
Прежде чем купить RTC-модуль, я попробовал победить проблему программно — и эта история сэкономит вам вечер. Схема выглядела умно: раз в час писать в EEPROM последний millis(), после перезагрузки читать его, вычитать из нового значения и получать «глобальное время». Механику записи я собрал по уроку про счётчик включений в EEPROM, где циклы перезаписи бережно растянуты. Работало — ровно до трёх мест, где костыль кончился. Первое: EEPROM переживает 100 000 записей на ячейку, поэтому часовая отметка одной ячейки — это 11 лет, но я по глупости писал раз в минуту, и через два месяца ячейка начала подгнивать. Второе: смещение давало «uptime плюс дельта», а не дату — про день недели и число по-прежнему никто не знал. Третье: переход на летнее время и вообще любая правка системного времени не попадали в схему никак.
Вывод из того вечера сформулирован навсегда: наращивание счётчика не превращает его в часы. Календарь — это кварц 32768 Гц, регистры с компенсацией високосных лет и питание от батарейки; всё это физически живёт не в прошивке, а в отдельном чипе. Сколько ни пиши поверх millis() слоёв, после пропадания питания программе нужно откуда-то взять честное время — а взять его неоткуда, кроме как от железа, которое это время хранило.
Что добавляет RTC: батарейка и календарные регистры
Часы реального времени решают обе проблемы разом. Внутри — собственный кварц на 32768 Гц (та же частота, что в наручных часах: делить 32768 пополам пятнадцать раз — удобный способ получить ровно один герц), регистры календаря до 2100 года и вывод на резервную батарейку. Основное питание пропало — чип тикает дальше от CR2032, потребляя доли микроампера: одна таблетка держит счёт годами. Точность зависит от чипа: у DS3231 с термокомпенсацией это ±2 ppm — около минуты за год, я разобрал эту цифру по косточкам в первом уроке про DS3231. У чтения есть бонус: метод lostPower() сообщает, что питание пропадало и время могло потеряться — можно не верить часам слепо, а проверить и перезавести.
Отдельный вопрос, который мне задают в комментариях: а ESP32 с Wi-Fi? Он же тянет время с NTP-сервера, как показано в уроке про первый Wi-Fi. Тянет — пока есть сеть и электричество. Первое же отключение света на даче без роутера — и плата просыпается слепой: NTP недоступен, внутренний счётчик пуст. Поэтому в автономных устройствах NTP и RTC не соперники, а пара: интернет подводит время раз в сутки, чип хранит его между синхронизациями. А если проекту нужны не только показания, но и срабатывания по часу — аппаратный будильник из урока про часы с дисплеем тикает даже тогда, когда логика спит.
Проверить разницу руками можно в симуляторе ниже: он печатает одновременно миллисекундный счётчик и показания RTC. Перезапустите симуляцию — и увидите оба поведения в одном эксперименте.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;unsigned long lastPrint = 0; void setup() { Serial.begin(9600); if (rtc.begin() == false) { Serial.println("RTC ne najden, prover'te SDA/SCL"); while (1) { } } if (rtc.lostPower()) { // питание пропадало: время под вопросом, заводим вручную rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); } Serial.println("Start: millis=0, chasy prodolzhayut");} void loop() { if (millis() - lastPrint < 1000) return; // раз в секунду lastPrint += 1000; DateTime now = rtc.now(); Serial.print("ms="); Serial.print(millis() / 1000); // аптайм в секундах Serial.print(" h="); Serial.print(now.hour()); Serial.print(" m="); Serial.print(now.minute()); Serial.print(" s="); Serial.println(now.second());}- millis() — счётчик аптайма: обнуляется при любом сбросе, включая открытие Serial-монитора на Uno (DTR дёргает Reset)
- 32 бита millis() переполняются через 49,7 суток; сравнивайте разницами беззнаковых — тогда переполнение не страшно
- Генератор Uno — керамический резонатор с допуском до ±0,5%: до семи минут ухода в сутки, у моих плат вышло 3,5 и 5 минут
- EEPROM-костыль со смещениями не даёт календаря и съедает ресурс ячеек — я прошёл этот тупик, вы можете пропустить
- RTC хранит календарь на батарейке годами; lostPower() честно сообщает, что питание пропадало и время надо проверить
- NTP и RTC — пара, а не альтернатива: сеть подводит время, чип хранит его между синхронизациями
- 1. Что произойдёт с millis() при открытии Serial-монитора на Arduino Uno?
- 2. Через сколько времени переполняется 32-битный счётчик millis()?
- 3. Почему EEPROM-смещения не заменяют RTC?
- 4. Какую роль играет батарейка в модуле RTC?
- 5. Проекту на ESP32 нужен RTC, если время тянется по NTP?
Итог прост: millis() для интервалов, RTC для календаря, и они не конкуренты, а соседи по плате. Остался второй вопрос — какой именно чип часов покупать, потому что на прилавке рядом лежат DS1307, DS3231 и DS1302, а разброс в точности между ними — в сотни раз. В следующем уроке сравню их по кварцу, питанию, будильникам и цене — с цифрами ухода, которые я намерил на своих трёх платах.
Частые вопросы по теме урока
- Что произойдёт с millis() при открытии Serial-монитора на Arduino Uno?
- DTR с USB-чипа дёргает Reset через конденсатор — плата стартует заново, millis() начинается с нуля. Плата перезагрузится по DTR, счётчик обнулится
- Через сколько времени переполняется 32-битный счётчик millis()?
- 4 294 967 295 мс — это примерно 49,7 суток. При сравнении разниц беззнаковых типов переполнение не ломает логику. Около 49,7 суток непрерывной работы
- Почему EEPROM-смещения не заменяют RTC?
- Из смещения не восстановить дату, день недели и летнее время, а частые записи выработают лимит 100 000 циклов на ячейку. Смещение даёт дельту аптайма, но не календарную дату, и жжёт ресурс ячеек
- Какую роль играет батарейка в модуле RTC?
- В резерве чип берёт доли микроампера — CR2032 хватает на годы отсчёта без основной платы. Питает счёт часов, когда пропало основное питание
- Проекту на ESP32 нужен RTC, если время тянется по NTP?
- NTP требует сети. После отключения света и роутера плата просыпается слепой — RTC продолжает счёт от батарейки. Да: без сети и электричества NTP недоступен, а чип сохранит ход