Тепличный будильник на millis, который я собирал по уроку про часы без RTC-модуля, вёл себя честно ровно две недели — потом начал отставать, и рассветное окно полива уехало к обеду. Кварц Uno — это керамический резонатор с допуском до полпроцента, до семи минут в сутки, и никакая компенсация в скетче не сделает из него хронометр. В этом уроке ставим на шину модуль DS3231: часы с собственным кварцем и батарейкой, которые тикают сами, питаются отдельно от платы и переживают её отключение месяцами. Экран при этом остаётся тем же 16x2 — переезжает только источник времени.
DS3231 — не просто «более точный кварц». Это отдельная микросхема с календарём и термокомпенсацией, заявленный уход — около двух частей на миллион, секунда за месяц; цифра из даташита, и мои два модуля за полгода не разошлись с телефонными часами заметнее пары секунд. Разбор пары DS1307 против DS3231 с ценами и батарейками я делал в уроке про сравнение двух чипов, повторять его здесь не буду. Фокус этого урока — экран: как вытащить время из модуля и положить его на 16x2 так, чтобы картинка не мигала, не ползла и читалась с метра.
План урока: три вызова на старте — begin, проверка lostPower и adjust только по делу — потом печать на экране с обновлением по смене секунды и кнопка, переключающая строку с датой. Из ловушек разберём две: разряженную батарейку, из-за которой часы «останавливаются» при выключенном питании, и лишний ответ сканера — за микросхемой часов на многих модулях прячется ещё и EEPROM со своим адресом. В симуляторе время идёт живьём: подвёдте его кнопками на панели часов и следите, как скетч это увидит.
Три вызова на старте
Скелет одинаковый для любого проекта с часами. rtc.begin() поднимает шину и возвращает false, если модуль не найден — проверяйте возвращаемое значение, «молча продолжить» здесь дорогой стиль: дальше скетч будет печатать нули и мусор, а вы будете искать граблю в экране вместо проводов. rtc.lostPower() возвращает true, когда питание пропадало, а батарейка время не удержала — впервые запущенный модуль почти всегда отвечает true. И только в этом случае зовите rtc.adjust(DateTime(F(__DATE__), F(__TIME__))) — установка по моменту компиляции скетча. Безусловный adjust — классическая грабля: каждый сброс платы откатывает часы на время сборки, и прибор, перезагрузившийся неделю спустя, врёт ровно на эту неделю.
С батарейкой разберитесь до корпуса. Норма для модуля — таблетка CR2032: держит время два-три года и не рассчитана на заряд, но на дешёвых платах стоит цепочка подзаряда под аккумуляторную LIR2032, и обычная таблетка в такой схеме живёт заметно хуже. Симптом разряженной батарейки особый: при включённом питании часы точны, при выключенном стоят, и прибор после перерыва показывает момент последнего отключения. Это не глюк модуля и не сбитый скетч — время держать нечем. Проверка занимает час: отключите плату от питания и посмотрите, тикают ли часы дальше.
Код у обеих микросхем общий: библиотека прячет различия за одним интерфейсом, и переход с DS1307 на DS3231 — это смена имени класса в конструкторе, rtc.begin() и rtc.now() остаются на местах. Этим удобно пользоваться при апгрейде: собрать сначала на дешёвом DS1307, а когда поймаете его уход, заменить модуль без правки остального скетча. Адрес на шине у обоих один — 0x68, и сканер не различает чипы: что именно у вас на плате, подскажет маркировка или сравнение показаний за неделю. Для экранной части этого урока чип безразличен: печать по смене секунды работает с любым из них.
На экран время ложится полями фиксированной ширины: setCursor на строку 0, потом часы, двоеточие, минуты, двоеточие, секунды — каждое число через печать с ведущим нулём, чтобы 09:05 не съезжало в 9:5. Механику полей и дробей я разбирал в уроке про числа и нули; для часов достаточно функции в две строки — если значение меньше десяти, печатаем ноль, потом значение. Сам формат ЧЧ:ММ:СС я впервые собирал на часах millis — раскладка строки та же, меняется только источник чисел: вместо самосчитанных секунд приходит now.second(). Двоеточие я никогда не мигаю: моргающий разделитель живо смотрится в макете и утомляет в приборе, а платить за него приходится перерисовкой вдвое чаще.
Дисциплина кадра простая, и лучше поставить её сразу. Печать идёт строкой поверх старой — clear() в часах не нужен вовсе: clear гасит весь экран, даёт вспышку и при частом вызове — те самые грабли мерцания, на которых построен целый урок раздела. Курсор держите на месте: каждый вывод начинается с setCursor на нужную колонку, иначе после чужой печати или сбоя строка уедет, и часы напечатаются со сдвигом. Хвосты затираются пробелами: переход «23:59:59» в «0:00:00» без ведущих нулей оставил бы на экране призрак из девяток — с полями фиксированной ширины такая история невозможна.
#include <Wire.h>#include <LiquidCrystal_I2C.h>#include <RTClib.h> LiquidCrystal_I2C lcd(0x27, 16, 2);RTC_DS3231 rtc;const int btn = 2; // переключение: время / дата int lastSec = -1;int rezhim = 0;bool lastBtn = LOW; void pechat2(int v) { // ведущий нуль: 09 вместо 9 if (v < 10) lcd.print("0"); lcd.print(v);} void vyvod(DateTime now) { lcd.setCursor(0, 0); pechat2(now.hour()); lcd.print(":"); pechat2(now.minute()); lcd.print(":"); pechat2(now.second()); lcd.print(" ");} void setup() { pinMode(btn, INPUT); lcd.init(); lcd.backlight(); Serial.begin(9600); if (!rtc.begin()) { Serial.println("RTC ne najdena: SDA/SCL ili adres"); } if (rtc.lostPower()) { // adjust только по делу rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); Serial.println("Vremya sbilos - ustanovleno po sborke"); }} void loop() { DateTime now = rtc.now(); if (now.second() != lastSec) { // печать только по смене секунды lastSec = now.second(); vyvod(now); } bool b = digitalRead(btn); if (b == HIGH && lastBtn == LOW) rezhim = 1 - rezhim; lastBtn = b; lcd.setCursor(0, 1); if (rezhim == 0) { lcd.print("CHASY DS3231 "); } else { pechat2(now.day()); lcd.print("."); pechat2(now.month()); lcd.print("."); lcd.print(now.year()); lcd.print(" "); } delay(50);}Строка с датой по кнопке — не украшение: в журналах теплицы день и месяц экономят вечер разбирательства, а совместить время с датой в одном кадре 16x2 впритык — формат с секундами съедает половину строки. После первого подключения прогоните сканер шины: модуль DS3231 обычно отвечает дважды — 0x68 у самих часов и 0x57 у EEPROM AT24C32, распаянного на той же плате. Двойной ответ — не конфликт, а норма, и лишняя ячейка потом пригодится: коды ошибок или последние настройки удобно держать прямо там — механика записи первых байтов в уроке про EEPROM. Дальше по вкусу: будильник с зуммером собирается поверх этих же вызовов, а уход конкретного экземпляра вы умеете мерить из урока про millis против настоящих часов.
Часы и экран спокойно делят одну шину: LCD на I2C опрашивается из скетча, а не сам лезет на шину, и транзакции раз в секунду по десятку байтов никто не замечает. Если на той же шине живут датчики, следите за одним: чтобы их транзакции не растягивались на сотни миллисекунд, иначе и сканер, и печать начнут ждать очереди. По питанию тоже честно: модулю часов хватает токов шины, батарейка питает только внутренние часы при выключенной плате, и в бюджет тока прибора DS3231 входит копейками. Проверка хода после сборки простая: минута наблюдения — секунда на экране должна тикать равномерно; замирания означают плохой контакт в проводах шины, а не разряд батарейки.
Когда модуль нужен, а когда нет
Мнение, с которым можно спорить: DS3231 стоит ставить не тогда, когда «хочется точности», а когда время обязано переживать отключение питания или жить без сети. Таймеру заварки, счётчику кормления кота и секундомеру модуль не нужен: millis дешевле, а уход за сутки на кухне никто не заметит. Я меняю источник времени на модуль в трёх случаях: расписания с привязкой к суткам, журналы с датами и всё, что после перерыва в питании должно проснуться с правильным временем. Обратный ход при этом дёшев: источник времени — одна функция в скетче, и вы в любой момент возвращаетесь к millis, сохранив экранную часть целиком. Дисциплину экрана — без мерцания, без лишних clear — я собирал в уроке про термометр без мерцания, она одинакова для любого источника чисел.
И последнее, что стоит знать до покупки: у DS3231 календарь встроенный — модуль сам знает разную длину месяцев и високосные годы до 2100-го, скетчу не нужны таблицы дней. Дата печатается прямо из now.day() и now.month(), и 28-е февраля не требует ни одной строки кода. Бонус — температурный датчик для термокомпенсации: rtc.getTemperature() отдаёт температуру платы с точностью в пару градусов, замена термодатчику слабая, но живой индикатор перегрева корпуса — бесплатный. Батарейку держите в списке расходников: два-три года — плановый срок, а не поломка, и чек-лист прибора должен включать год постановки таблетки. Дешёвые модули без площадки под батарейку вообще не переживают выключения — на второй день после сборки вы узнаете об этом по нулям на экране.
| Источник времени | Уход | Когда ставить |
|---|---|---|
| millis() в скетче | До 0,5% — до 7 минут в сутки у Uno | Таймеры и счётчики без привязки к датам |
| DS1307 | Десятки секунд в сутки, без термокомпенсации | Дешёвые проекты, где точность не критична |
| DS3231 | Около 2 ppm — секунды за месяц | Расписания, журналы, будильники |
| DS3231 + CR2032 | Время держит 2-3 года без питания | Любой прибор с перерывами питания |
- 1. rtc.lostPower() вернул true при первом запуске. Что делать?
- 2. Каждый сброс платы откатывает часы на момент сборки скетча. Виновник?
- 3. Сканер шины нашёл 0x68 и 0x57 на одном модуле. Второй адрес — это…
- 4. Часы точны при питании, но после выключения стоят. Причина?
- 5. Как обновлять экран часов, чтобы не мигал и не грузил шину?
- begin с проверкой, lostPower и только потом adjust: безусловный adjust откатывает часы на момент сборки
- Печать по смене секунды, ведущие нули, немигающее двоеточие — кадр живёт без clear
- Сканер покажет 0x68 и 0x57: второй адрес — EEPROM AT24C32 на плате модуля
- CR2032 держит 2-3 года; часы стоят после выключения — батарейка, а не модуль
- 2165 год и 85 секунд — признак отсутствия ответа с шины, а не сбитое время
Частые вопросы по теме урока
- rtc.lostPower() вернул true при первом запуске. Что делать?
- Признак сбитого питания — повод для установки времени. Безусловный adjust при каждом старте откатывал бы часы на момент компиляции. Вызвать rtc.adjust с временем сборки скетча
- Каждый сброс платы откатывает часы на момент сборки скетча. Виновник?
- Установка по DateTime(__DATE__, __TIME__) при каждом старте заново ставит момент компиляции. Оборачивайте её в проверку lostPower. rtc.adjust без проверки lostPower
- Сканер шины нашёл 0x68 и 0x57 на одном модуле. Второй адрес — это…
- На большинстве плат DS3231 стоит 32 КБ EEPROM с адресом 0x57. Двойной ответ — норма, память пригодится под журнал. EEPROM AT24C32, распаянный на плате модуля
- Часы точны при питании, но после выключения стоят. Причина?
- Время при выключенном питании держит только батарейка. Стоят после перерыва — держать нечем: менять таблетку и смотреть цепочку подзаряда. Разряженная батарейка или цепочка подзаряда под неё
- Как обновлять экран часов, чтобы не мигал и не грузил шину?
- Триггер по разнице now.second() с прошлым значением обновляет кадр раз в секунду, экран не гаснет ни на один тик. Печать только при смене секунды, без clear