Часы для мастерской я собрал за вечер и повесил на стену в тот же день: DS3231 тикал, LCD показывал, задача закрыта. Через неделю поймал себя на неприятной вещи — время с них я уже не читал, а угадывал. Цифры жались к левому краю, двоеточие стояло неподвижно, как в дешёвом калькуляторе, и прибор выглядел выключенным, хотя чип шёл с точностью до пары секунд в год. Тогда я сформулировал правило, которое теперь держу железно: правильное время — половина работы; вторая половина — раскладка экрана, чтобы показания читались вполглаза с трёх метров.
Разберём раскладку по клеткам, центрирование строки времени, мигающее двоеточие, которое делает часы живыми, и вторую строку с датой, днём недели и температурой чипа. Железо уже знакомо: DS3231 и LCD 16x2 сидят на одной шине I2C, экран — через переходник PCF8574 на адресе 0x27, часы — на 0x68. Если пара ещё не собрана, прогоните сканер из урока про поиск адресов на шине I2C — на живой шине должны висеть обе отметки сразу. Каркас с флагом и зуммером у этих же часов уже был в уроке про часы с будильником, поэтому здесь только про изображение.
Раскладка: 16 клеток и где живёт время
Экран 16x2 — это ровно шестнадцать клеток в строке и две строки целиком, без промежуточных пикселов. Строка времени HH:MM:SS занимает восемь клеток: по две на часы, минуты и секунды и по одной на двоеточия. Если печатать её с клетки 0, справа останется восемь пустых клеток, и время будет выглядеть прижатым к краю. Центр считается арифметикой второго класса: (16 − 8) / 2 = 4 — ставлю курсор lcd.setCursor(4, 0) и печатаю. Дата DD.MM из пяти символов в одиночестве просит клетки 5, но в связке с днём недели и температурой я прижимаю её к клетке 2: раскладка второй строки всегда компромисс между центром и составом. Под каждую надпись сначала считаем клетки, потом жмём setCursor.
Курсор у LiquidCrystal_I2C работает в клетках: первый аргумент — колонка от 0 до 15, второй — строка от 0 до 1. Это важно держать в голове, потому что в соседнем пиксельном мире OLED setCursor принимает не клетки, а пиксели — об этом целиком следующий урок. Ведущие нули решает маленькая функция z2: число меньше десяти — сначала печатаем символ ноль, потом само число. Она не только красит вывод: без неё строка меняет ширину каждую секунду — 9:05:03 прыгает на клетку влево и обратно, и глаз цепляется за это движение постоянно.
Проверка адресов стоит минуты и экономит вечер. На живой шине сканер показывает две отметки: 0x68 — часы, 0x27 — экран. Если часов на шине нет, а экран молчит, у вашего переходника может стоять чип PCF8574A с адресом 0x3F — скетч с 0x27 будет честно шептать в пустоту. И наоборот: сканер не видит ни одного адреса — проверяйте, не поменяны ли местами SDA и SCL, классика первой сборки. И про питание, потому что оно у пары общее: подсветка тянет 20–25 мА, часы — доли миллиампера, а DS3231 на зашумлённом питании дешёвых клонов Nano начинает врать в регистрах — время скачет на секунды, температура пляшет. Мой сигнал беды: мусор на LCD именно в момент опроса шины — провода длиннее 20 сантиметров на макетке, и шина спотыкается; укорачиваю — оживает.
Вторая строка — место для контекста, который хочется видеть без всякого Serial-монитора. Дата DD.MM печатается пятью клетками. День недели достаётся методом now.dayOfTheWeek() — он возвращает число от 0 до 6, где ноль — воскресенье, а имя подставляется массивом из семи строк по этому индексу; вся механика полей DateTime разобрана в уроке про объект DateTime. Сэмплов на строку хватает: 16.09 VT — и уже понятно, будний день на календаре или выходной.
Температура — бонус, который у DS3231 настоящий, а не подставной: внутри чипа живёт датчик кристалла, он же источник термокомпенсации кварца. Печатается вызовом rtc.getTemperature() с шагом 0,25 °C — механика компенсации разобрана в уроке про точность DS3231. Для контроля перегрева корпуса в закрытой коробке этого достаточно: нагрелось с 23 до 30 — значит, пора смотреть вентиляцию. Погрешность в доли градуса против комнатного термометра честная — датчик меряет сам кристалл, а не воздух.
#include <Wire.h>#include <RTClib.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2);RTC_DS3231 rtc; void z2(int v) { // 5 -> 05 if (v < 10) lcd.print("0"); lcd.print(v);} void setup() { lcd.init(); lcd.backlight(); if (rtc.begin() == false) { while (1) { } // без часов экран не нужен }} void loop() { DateTime now = rtc.now(); lcd.setCursor(4, 0); // (16-8)/2 = 4 z2(now.hour()); lcd.print(":"); z2(now.minute()); lcd.print(":"); z2(now.second()); lcd.setCursor(2, 1); z2(now.day()); lcd.print("."); z2(now.month()); lcd.print(" "); lcd.print(rtc.getTemperature(), 1); delay(500);}Двоеточие, которое тикает
Неподвижное двоеточие делает часы мёртвыми на вид — даже я сам, зная, что чип идёт, ловил себя на двойной проверке. Лечится одним сравнением: секунда чётная — печатаем «:», нечётная — пробел. Условие now.second() % 2 == 0, символ на символ той же ширины, строка не прыгает. Получается пульс с периодом в секунду: экран дышит, и с порога видно, что прибор жив. На обновлении раз в 500 мс двоеточие переключается ровно раз в секунду — чаще и не нужно.
Перерисовку делаю перезаписью поверх старых символов, а не стиранием: lcd.clear() в каждом кадре даёт видимое мигание, и полный разбор этого симптома лежит в уроке про мерцание LCD. Заодно перезапись воспитывает дисциплину фиксированной ширины: если новая строка короче старой, на клетках остаются хвосты. Классика — убрать день недели из второй строки и навсегда видеть призрак VT в середине; или температура с минусом: 23.5 занимает четыре клетки, а −9.75 — пять, и шестая клетка держит мусор от прошлого кадра. Правило простое: ширина поля в кадре не меняется, а если меняется — дописываем пробел до прежней длины.
Ритм обновления подбирается под глаз и под шину. Пауза 500 мс — мой выбор для часов: смена секунды не пропускается, двоеточие переключается вовремя, а шину I2C можно считать свободной. Пауза дольше секунды рискует пропускать целые показания — глаз замечает прыжок через два. Короче 200 мс — экран гоняется впустую: секунды меняются раз в секунду, и промежуточные кадры не добавляют информации, а на слабом питании только подмешивают шум в мой любимый стабильный кадр. Пока налаживаете раскладку, держите дублирующий вывод в Serial парами h=/m=/s=: монитор не заменяет экран, но честно показывает, что чип идёт, когда на LCD темнота или хвосты.
| Зона экрана | Что вывожу | Зачем |
|---|---|---|
| Строка 0, клетки 4–11 | HH:MM:SS с мигающим двоеточием | главное показание: читается с трёх метров |
| Строка 1, клетки 2–6 | дата DD.MM | число и месяц для сверок без Serial-монитора |
| Строка 1, клетки 7–8 | день недели из dayOfTheWeek() | расписания вида «по будням» читаются честно |
| Строка 1, клетки 10–14 | температура чипа, шаг 0,25 °C | контроль нагрева корпуса заодно с временем |
| Строка 0, клетки 12–13 | индикатор AL у будильного часа | статус виден периферийным зрением, без чтения |
- Двенадцатичасовой формат. После полудня вычитайте 12 из часа и печатайте суффикс AM/PM; крайний случай — полночь: 0 превращается в 12 AM, а не в 0. Посчитайте клетки: вместе с суффиксом строка растёт до 9 знаков, и центр уезжает.
- Правое выравнивание. Печатайте время с клетки 8 вместо 4: цифры прижмутся к правому краю. Сравните оба варианта с двери мастерской — моё мнение: центр читается легче, но для 12-часового формата правый край практичнее.
- Хвосты. Уберите день недели из второй строки симулятора и посмотрите, что останется от VT — без дописывания пробелов старые символы переживают новую строку.
- Заготовка будильника. Заведите будильный час в переменной и печатайте во второй строке AL, когда час совпал: аппарат перейдёт в статусную раскладку — а дальше дело за аппаратными будильниками из третьего урока этой тройки.
- Экран 16x2 — шестнадцать клеток: строка HH:MM:SS занимает 8, центр начинается с клетки (16−8)/2 = 4
- setCursor у LCD работает в клетках (колонка, строка), а не в пикселях — не переносите привычку на OLED
- Перезапись поверх старых символов бесшовна; lcd.clear() в каждом кадре даёт видимое мигание
- Мигающее двоеточие по now.second() % 2 — дешёвый пульс, который делает часы живыми
- Вторая строка: дата, день недели из dayOfTheWeek() и температура чипа с шагом 0,25 °C
- rtc.now() — только внутри loop(): снимок из setup() замораживает экран на одной секунде
- 1. Сколько клеток строки 16x2 займёт строка 23:59:59?
- 2. С какой клетки печатать HH:MM:SS, чтобы выровнять строку по центру?
- 3. Что вернёт rtc.getTemperature() у DS3231?
- 4. Почему двоеточие на нечётной секунде печатают пробелом, а не стирают экран через lcd.clear()?
- 5. Где обязан стоять вызов rtc.now() в скетче часов?
Экран показывает время, дата и температура под рукой — каркас настольного прибора готов. Дальше два пути. Первый — переехать с клеточного LCD на пиксельный OLED и собрать крупный циферблат из шрифта двойного размера: при той же паре проводов он читается с дивана, где клетки 5×8 уже не спасают. Второй — научить часы звонить: в уроке про два будильника DS3231 чип сам следит за расписанием, а скетчу остаётся ловить флаги. И держите под рукой поля DateTime — год, unixtime, день недели: рано или поздно они понадобятся любому журналу или расписанию на неделю.
Частые вопросы по теме урока
- Сколько клеток строки 16x2 займёт строка 23:59:59?
- Шесть цифр по одной клетке и два двоеточия — всего восемь символов. Оставшиеся восемь клеток распределяются по бокам, если центрировать. Восемь
- С какой клетки печатать HH:MM:SS, чтобы выровнять строку по центру?
- (16 − 8) / 2 = 4. lcd.setCursor(4, 0) оставляет по четыре пустые клетки с каждой стороны. С клетки 4
- Что вернёт rtc.getTemperature() у DS3231?
- Датчик меряет кристалл — он же источник термокомпенсации кварца. Шаг 0,25 °C задан разрядностью регистров температуры, поэтому в морозной мастерской показания ходят ступеньками. Температуру кристалла с шагом 0,25 °C
- Почему двоеточие на нечётной секунде печатают пробелом, а не стирают экран через lcd.clear()?
- Полное стирание и перерисовка видны глазу как вспышка. Перезапись символа на символ проходит незаметно, а ширина строки не меняется. clear() даёт видимое мигание, а перезапись поверх бесшовна
- Где обязан стоять вызов rtc.now() в скетче часов?
- Снимок из setup() замирает на одной секунде навсегда. Свежий rtc.now() в начале loop() даёт живое время, согласованное на весь проход цикла: половина решений не по 9:59, а половина по 10:00. Внутри loop(), в начале каждого цикла