Таймер полива в теплице я собрал за вечер и гордился им ровно до первой грозы. Просадка питания на пару секунд — плата перезагрузилась, millis() обнулился, расписание поехало: в три часа ночи клапан открылся и честно отработал свои минуты, полив рассаду по полной. Утром я сформулировал простую вещь: у Arduino нет часов. Вообще. Плата умеет считать миллисекунды с момента включения, но не имеет представления о том, который час в человеческом мире — ни даты, ни дня недели, ни часового пояса.
Решение — отдельный прибор, который помнит время всегда, даже без питания: модуль часов реального времени DS3231 размером с ноготь. В этом уроке подключим его по I2C, установим время одной строкой, прочитаем обратно и выведем в Serial дату с временем. И разберём, что означает обещанная точность ±2 ppm — и почему она радикально лучше всего, что умеет сама Arduino.
Почему millis() — не часы
Функция millis() возвращает миллисекунды с момента старта платы. Для интервалов и мигалок — идеальна. Но спросите у неё «сейчас 14:05 или 14:06?» — она не знает. Ей неизвестно, сколько дней плата работает, был ли февраль високосным и что такое часовой пояс. После каждой перезагрузки счётчик начинается с нуля, и вся временная картина мира стирается.
Хуже того, сам счётчик врёт на длинной дистанции. Тактовый генератор Arduino Uno — керамический резонатор с допуском порядка ±0,5 %. За сутки это до семи минут ухода — представьте настенные часы, которые вы подводите каждое утро. Строить расписание полива на таком генераторе — значит сверять время по прибору, который врёт на проценты, а не на доли секунды.
Вывод, к которому я пришёл на той теплице: миллисекундный таймер и часы — разные инструменты. millis() живёт внутри скетча и умирает вместе с ним. Часы обязаны жить своей жизнью: тикать при выключенной плате, переживать перезагрузки, знать календарь. Для этого нужен свой кварц, своя батарейка и своя микросхема.
Что внутри модуля DS3231
DS3231 — законченные часы реального времени (RTC, real time clock) в одном чипе. Главная фишка — термокомпенсированный кварцевый генератор: микросхема меряет собственную температуру и постоянно подстраивает частоту кварца. Отсюда заявленная точность ±2 ppm в диапазоне 0…+40 °C. Переведём на человеческий: 2 промилле — это около 63 секунд ошибки за год, меньше минуты. Старый собрат DS1307 с обычным кварцем гуляет на минуты в месяц, и его уход зависит от температуры в комнате.
Часы ведут полный календарь: секунды, минуты, часы, день недели, число, месяц, год — с учётом разной длины месяцев и високосных лет, вплоть до 2100 года. Питается всё от шины 5 В, а при её пропадании микросхема мгновенно переключается на резервную батарейку — на модулях стоит литиевая «таблетка» CR2032 на 3 В. Собственное потребление в резервном режиме — единицы микроампер, поэтому одной батарейки время живёт годами.
Связь с Arduino — по шине I2C, всего два провода. На Uno линия данных SDA сидит на пине A4, тактирование SCL — на A5. Адрес DS3231 на шине — 0x68, он зашит в кремнии жёстко и не настраивается. На той же шине потом спокойно уживутся дисплей, память и датчики — у каждого свой адрес.
| Вывод модуля | Пин Arduino Uno | Назначение |
|---|---|---|
| VCC | 5V | питание модуля |
| GND | GND | общий провод |
| SDA | A4 | линия данных I2C |
| SCL | A5 | линия тактирования I2C |
| SQW, 32K | не подключаем | дополнительные выходы, пока не нужны |
Установка и чтение времени
Ставим библиотеку RTClib от Adafruit через менеджер библиотек: Sketch → Include Library → Manage Libraries, в поиске — RTClib. Она работает и с DS1307, и с DS3231, разница только в классе: RTC_DS1307 или RTC_DS3231.
Время устанавливается одной строкой: rtc.adjust(DateTime(F(__DATE__), F(__TIME__))). Читается так: взять дату и время, когда компилировался этот скетч, и записать их в часы. __DATE__ и __TIME__ — служебные макросы компилятора, а F() убирает строки во флеш-память, чтобы не расходовать ОЗУ. После записи микросхема тикает сама — от шины или от батарейки.
Чтение — DateTime now = rtc.now(): объект-снимок текущего момента с методами year(), month(), day(), hour(), minute(), second(). Все возвращают числа, поэтому форматирование на вашей стороне: чтобы 9:05 не выглядело как 9:5, я вручную дописываю ведущие нули.
Две ловушки строки с adjust. Первая: она перезаписывает время при каждой загрузке — батарейка честно хранила точное время, а вы его стёрли, залив скетч ещё раз. Вторая: __DATE__ — это момент компиляции, а не включения платы, поэтому перезаливка старого бинарника запишет вчерашнее время. Правильный цикл: залить с adjust один раз, убедиться, что время встало, убрать строку и залить снова.
Кстати, почему все методы возвращают числа, а не готовые строки: числа удобны для логики. Сравнить now.hour() == 7 проще, чем парсить строку, а ведущие нули нужны только человеку на экране. Для хитрых форматов у объекта DateTime есть свои помощники, но в девяти бытовых задачах из десяти хватает шести геттеров и функции printTwoDigits — её код вы уже видели выше и увидите ещё раз в симуляторе.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc; void setup() { Serial.begin(9600); if (rtc.begin() == false) { Serial.println("DS3231 ne najden! Prover'te provodku"); while (1) { } // дальше без часов смысла нет } // время компиляции -> в часы (один раз, потом строку убрать) rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); Serial.println("Vremya ustanovleno");} void loop() { DateTime now = rtc.now(); Serial.print(now.year()); Serial.print("-"); if (now.month() < 10) Serial.print("0"); Serial.print(now.month()); Serial.print("-"); if (now.day() < 10) Serial.print("0"); Serial.print(now.day()); Serial.print(" "); if (now.hour() < 10) Serial.print("0"); Serial.print(now.hour()); Serial.print(":"); if (now.minute() < 10) Serial.print("0"); Serial.print(now.minute()); Serial.print(":"); if (now.second() < 10) Serial.print("0"); Serial.println(now.second()); delay(1000);}- Выведите дату в формате ГГГГ-ММ-ДД: сначала год, потом месяц и день с ведущими нулями. Такой порядок сортируется в логах даже как строка.
- Напечатайте день недели по-русски через массив из семи строк — по индексу
now.dayOfTheWeek(). - Соберите таймштамп в одну переменную String вида 2025-01-15 09:07:33 и выводите одним вызовом println(). Подсказка: склейка через оператор плюс из урока про String.
- У Arduino нет часов: millis() считает миллисекунды с запуска и уходит на ±0,5% — до 7 минут в сутки
- DS3231 — RTC с термокомпенсированным кварцем: ±2 ppm, около минуты ошибки за год
- При пропадании питания часы живут на батарейке CR2032 годами — потребление в единицах микроампер
- Подключение: SDA→A4, SCL→A5, адрес 0x68; библиотека RTClib, класс RTC_DS3231
- rtc.adjust(DateTime(F(__DATE__), F(__TIME__))) ставит время компиляции — один раз, затем строку убираем
- rtc.now() возвращает DateTime: year(), month(), day(), hour(), minute(), second(), dayOfTheWeek() (0 — воскресенье)
- 1. Что вернёт millis() сразу после перезагрузки платы?
- 2. Насколько точен DS3231?
- 3. Что произойдёт с часами DS3231 при отключении основного питания?
- 4. Куда подключается линия SDA на Arduino Uno?
- 5. Почему строку с rtc.adjust(F(__DATE__)...) убирают после первой прошивки?
- 6. Что вернёт now.dayOfTheWeek() в понедельник?
Время на плате появилось — можно строить расписания, которые переживают грозы. В следующем уроке соберём настоящий прибор: настольные часы с LCD 16x2 и будильником, который срабатывает в заданный час и минуту ровно один раз в сутки. Заодно поймаем классическую ловушку сравнения времени — и увидим, почему будильник, написанный в лоб, звенит целую минуту.
Частые вопросы по теме урока
- Что вернёт millis() сразу после перезагрузки платы?
- millis() — счётчик с момента запуска. Ни даты, ни времени суток он не знает и после сброса обнуляется. Ноль — счёт с момента старта
- Насколько точен DS3231?
- Термокомпенсированный кварц держит ±2 ppm в диапазоне 0…+40 °C, это примерно 63 секунды за год. ±2 ppm — около минуты ошибки за год
- Что произойдёт с часами DS3231 при отключении основного питания?
- Резервная батарейка поддерживает счёт и память: потребление в единицах мкА, поэтому годы без питания — норма. Модуль переключится на CR2032 и продолжит отсчёт
- Куда подключается линия SDA на Arduino Uno?
- SDA — A4, SCL — A5. Адрес DS3231 на шине — 0x68. A4
- Почему строку с rtc.adjust(F(__DATE__)...) убирают после первой прошивки?
- adjust нужен один раз. Иначе каждый запуск сбрасывает часы на дату сборки скетча, даже если батарейка хранила точное время. При каждой загрузке она перезаписывает точное время на момент компиляции
- Что вернёт now.dayOfTheWeek() в понедельник?
- Метод возвращает число 0-6, где 0 — воскресенье, 1 — понедельник. Строки выводите сами через массив. 1