Часы на millis() я собрал за вечер и через месяц поймал себя на том, что сверяю их с телефоном, как с оракулом: за четыре недели прибор ушёл на десять минут. Виноват не мой код — резонатор Uno, керамика с допуском до половины процента; при таком допуске часы обязаны плыть, и мой экземпляр плыл примерно минута за трое суток. Лечится это не хитрой компенсацией, а сменой источника времени: микросхема DS3231 держит секунды с паспортной точностью около минуты-двух в год в диапазоне 0…+40 °C — температурно-компенсированный кварц внутри, держатель CR2032 снаружи.
Подключение — подарок: DS3231 живёт на той же шине I2C, что и экран. SDA на A4, SCL на A5, два адреса на двух проводах — 0x68 у часов и 0x3C у OLED. Как разводить провода и беречь длину перемычек — в уроке про схему подключения OLED; перед первой сборкой я всегда прогоняю сканер I2C-адресов — обе микросхемы должны быть видны сразу — это снимает половину вечерних догадок.
План урока такой: крупное время двойным шрифтом, ведущие нули и мигающее двоеточие, кадр раз в секунду по millis и температура модуля в подвале экрана. В симуляторе модуль часов подключается к макетке одной кнопкой, время в нём живое — как на настоящей плате.
Время — самая частая надпись в моих проектах: будильник, полив, журнал событий с метками, таймер инкубатора — везде нужен один и тот же восьмизначный блок. Поэтому часы на RTC — это не столько прибор, сколько заготовка: соберёте её один раз — и просто вставляете в следующий проект блок из setup и loop, не думая заново.
Крупный шрифт: setTextSize(2) и арифметика строки
Время — та часть интерфейса, которую читают издалека и мельком, поэтому я считаю setTextSize(2) единственным осмысленным размером для часов на 0.96 дюйма: знак вырастает до 12×16 пикселей, в строку влезает десять символов, в экран — четыре такие строки. Строка времени HH:MM:SS занимает восемь знаков из десяти — запас в два символа. Мнение с границей: size 3 уже не проходит — знак 18 пикселей шириной, семь символов на строку, и восьмизначное время не помещается; после size 2 следующий осмысленный шаг — не шрифт, а другой экран.
Две мелочи, на которых держится аккуратный вид. Первая — ведущие нули: print печатает 5 как 5, и время 9:05 рассыпается в строку переменной длины; функцию-помощник вида dva(n) — меньше десяти, значит приписать ноль слева — я перетаскиваю из проекта в проект уже год. Вторая — мигающее двоеточие: вместо того чтобы не печатать его вовсе, я заменяю колон пробелом на нечётной секунде — строка остаётся восьмизначной, хвост не дёргается, глаз не цепляется. И правило компоновки, которое экономит нервы: строку собирайте целиком в одну переменную и печатайте одним print на зону — куда после print переходит курсор, мы уже разбирали в уроке про print и setCursor.
Тупик, в который я сам себя загнал на первой версии: печатал время тремя вызовами подряд — print часов, print двоеточия, print минут — и добавляя секунды, получил хвост, перескакивающий то в одну строку, то в другую: где-то между вызовами проскочил println, и курсор уехал. На плате это лечится явным setCursor перед каждой печатью, но проще приучить себя к одной печати на зону сразу: строка собрана, курсор поставлен, print — и зона неподвластна ни расширению формата, ни перестановкам мест.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h>#include <RTClib.h> RTC_DS3231 rtc;Adafruit_SSD1306 display(128, 64, &Wire, -1); String dva(int n) { if (n < 10) return "0" + String(n); return String(n);} long lastS = -1; void setup() { Serial.begin(9600); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE); rtc.begin(); if (rtc.lostPower()) { // Батарейка села или отсутствует: берём время компиляции скетча rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); }} void loop() { DateTime t = rtc.now(); long s = t.unixtime(); if (s != lastS) { // сменилась секунда — пересобираем кадр lastS = s; String colon = ":"; if (t.second() % 2 == 1) colon = " "; // колон мигает заменой на пробел String vremya = dva(t.hour()) + colon + dva(t.minute()) + colon + dva(t.second()); display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); display.println("CHASY DS3231"); display.setTextSize(2); display.setCursor(0, 20); display.print(vremya); // 8 знаков из 10 возможных display.setTextSize(1); display.setCursor(0, 40); display.print(dva(t.day()) + "." + dva(t.month()) + "." + String(t.year())); display.setCursor(0, 50); display.print("T modulya: " + String(rtc.getTemperature(), 1)); display.display(); }}| Зона | y, пиксели | Шрифт | Содержимое |
|---|---|---|---|
| Шапка | 0 | size 1 | CHASY DS3231 — 12 знаков из 21 |
| Время | 20 | size 2 | HH:MM:SS — 8 знаков из 10, колон мигает |
| Дата | 40 | size 1 | ДД.ММ.ГГГГ — 10 знаков через точки |
| Подвал | 50 | size 1 | T modulya: 23.5 C — температура кристалла |
Почему строка времени всегда ровно восемь знаков, а не шесть-семь-восемь по ситуации: постоянная ширина держит левый и правый края кадра на месте, и весь экран выглядит прибором, а не бегущей строкой. Дата через точки — тоже фиксированные десять знаков: 07.09.2026 и 25.12.2027 занимают одинаковую полосу, и взгляд за неделю привыкает находить нужное без чтения.
Подвал с температурой — не украшение, а честная реклама модуля: у DS3231 внутри собственный термометр, который компенсирует уход кварца, и rtc.getTemperature() его отдаёт наружу. Шаг — 0.25 °C, паспортная точность ±3 °C, то есть это индикатор тепла в корпусе, а не метеостанция; печатать лучше с одним знаком после точки — String(t, 1). И раз уж речь о CR2032: именно она питает счёт секунд при выключенной плате — потому устройство и называется часами реального времени, а не таймером.
Ресурс батарейки тоже стоит знать — чтобы не менять её из суеверия. В режиме счёта секунд DS3231 берёт от батарейки единицы микроампер, и элемента CR2032 на 220 мА·ч хватает на годы: мои модули живут на первых батарейках по пять-шесть лет. Сюда же — вариант для вытянутого экрана 128×32: время size 2 занимает ровно верхнюю половину, дата и температура садятся в нижнюю строку — раскладка из таблицы сжимается вдвое без потери зон.
Тик раз в секунду: DateTime и millis
Библиотека RTClib даёт весь словарь в трёх строках: RTC_DS3231 rtc; rtc.begin(); DateTime t = rtc.now(). У объекта DateTime есть поля hour(), minute(), second(), day(), month(), year(), день недели dayOfTheWeek() и unixtime() — счёт секунд с 1970 года; базу я считал в уроке про DS3231 и первое время, здесь только то, что нужно экрану. Каждое чтение now() — короткая транзакция по шине на несколько байт, доли миллисекунды: читать можно хоть каждый проход loop без всяких последствий.
Тик я ловлю не по таймеру, а по смене значения: long s = t.unixtime(); если s разошёлся с прошлым — началась новая секунда, пересобираем кадр. Между сменами loop пустой и живой: опрашивает кнопки, читает Serial, ведёт свои дела — delay() здесь разом заморозил бы всё хозяйство, схема с millis() и разностью стоит две строки, как в уроке про millis вместо delay.
Перерисовка раз в секунду — предельно щадящий режим для шины: clearDisplay, печать трёх зон, display() — и кадр уходит одной посылкой, около 92 мс на 100 кГц. Для сравнения: дашборд из урока про буфер и display() гонял по сто кадров в секунду и душ те же 92 мс на каждый; раз в секунду даже длинные провода и соседи по шине этого не заметят.
Почему триггером стоит именно смена секунды, а не millis-интервал: при интервале 250 мс одна и та же секунда нарисуется четыре раза — четыре лишних прохода по шине ради одной и той же картинки, а при интервале 1200 мс секунды начнут пропускаться, и часы соврут в темпе. Смена unixtime() даёт ровно один кадр на одну секунду при любой скорости loop — тайминг сам подстраивается, вы его даже не задаёте.
- Формат 12 часов. После полудня отнимайте от hour() двенадцать и печатайте AM или PM в шапке: ширина строки сохранится, привычка американских будильников — бесплатно.
- День недели. dayOfTheWeek() даёт число 0-6; массив из семи String-сокращений выведет MON-SUN латиницей — кириллицы в стандартном шрифте экрана нет.
- Лишний трафик. Поставьте пересборку кадра каждые 250 мс вместо смены секунды: одна и та же секунда будет рисоваться четыре раза — сравните журнал шины до и после.
- На настоящей плате. Выньте CR2032, выключите питание, включите снова: lostPower() вернёт истину, и часы возьмут время компиляции скетча — заодно проверите батарейку.
- DS3231 держит время с точностью около минуты-двух в год (0…+40 °C); резонатор Uno с допуском до 0.5 % плывёт минутами в сутки — источник времени меняет всё
- Часы и OLED живут на одной шине: SDA A4, SCL A5; адреса 0x68 и 0x3C — сканер должен видеть обе сразу
- setTextSize(2): знак 12×16, строка — 10 символов, четыре строки на экран; HH:MM:SS занимает 8 знаков, size 3 уже не влезает
- Ведущие нули — helper вида dva(n); колон мигает заменой на пробел, чтобы ширина строки не прыгала
- Кадр пересобирается по смене unixtime(); чтение now() стоит доли миллисекунды, полный кадр по шине — ~92 мс раз в секунду
- 1. Какой адрес I2C у модуля DS3231 на общей шине с OLED 0x3C?
- 2. Сколько символов помещается в строку при setTextSize(2) на экране 128 пикселей?
- 3. Как получить время 07:05 вместо 7:5?
- 4. Почему мигающее двоеточие заменяют пробелом, а не убирают из строки?
- 5. Что на самом деле показывает rtc.getTemperature()?
Часы собрались: источник секунд с годовым уходом меньше двух минут, крупная строка, которую видно с другого конца комнаты, дата и живая температура в подвале. На плате скетч из код-блока зарабатывает после трёх проводов и одного прогона сканера; в симуляторе вы уже могли подводить время кнопками на компоненте — на модуле DS3231 той же цели служат две тактовые кнопки на плате, если они распаяны.
Домашнее задание на плату: соберите схему, залейте скетч, выключите питание на минуту и включите снова — часы должны продолжить с той же секунды, а не с момента загрузки. Если продолжили — CR2032 на месте и связка работает как надо; подвести же точное время можно кнопками на самом модуле, если они распаяны, или строкой adjust со временем компиляции. Следующий урок резко сместит жанр: тот же экран, та же шина, но вместо будильника — игра с прыжками, трубами и счётом.
Частые вопросы по теме урока
- Какой адрес I2C у модуля DS3231 на общей шине с OLED 0x3C?
- 0x68 — паспортный адрес DS3231. 0x27 — переходник LCD, 0x3D — вариант OLED, 0x76 — часть датчиков давления. 0x68
- Сколько символов помещается в строку при setTextSize(2) на экране 128 пикселей?
- Знак size 2 — 12 пикселей шириной: 128 / 12 = 10 символов. Для справки: size 1 даёт 21, size 3 — только 7. 10
- Как получить время 07:05 вместо 7:5?
- print печатает числа без выравнивания; ноль слева приписывает функция вида dva(n) — классический helper для часов. Helper-функция: число меньше десяти дополняется нулём слева
- Почему мигающее двоеточие заменяют пробелом, а не убирают из строки?
- Строка постоянной длины стоит на месте; исчезающий знак сдвигал бы всю правую часть каждую секунду — глаз это замечает. Ширина строки остаётся постоянной — хвост не дёргается
- Что на самом деле показывает rtc.getTemperature()?
- Внутри DS3231 стоит термометр для температурной компенсации: шаг 0.25 °C, точность ±3 °C — живой индикатор, но не метеостанция. Температуру кристалла DS3231 для компенсации кварца, шаг 0.25 °C