Часы в прихожей отработали зиму, потом моргнуло питание — и до весны они показывали «16:07» в семь вечера, потому что снимать их со стены ради двух минут правки мне было лень, а ноутбука рядом нет. В тот день я понял, что часы с раскладкой и температурой остались половиной изделия: без правки на корпусе они всё равно требуют стола с IDE. Здесь добираю вторую половину: день недели по-русски, чтобы дата читалась с порога, мигающие двоеточия, чтобы прибор выглядел живым, и правка времени кнопками — за тридцать секунд, не снимая со стены.
Правка без ноутбука приносит прибыль в непредвиденных местах. Перевод часов на зимнее время — тридцать секунд на стене; съехавшая после замены батарейки CR2032 дата — те же тридцать секунд; таймер полива, переехавший на час из-за отпуска, — и снова без шнура. У моей мамы такие часы висят третий год, и слово «прошивка» она не знает: три тактовые кнопки на корпусе, подпись поля на второй строке — и всё. Это граница между самоделкой и прибором: изделие обязано чинить себя без того, кто его сделал.
Экран у нас прежний, 16x2 по I2C, и клетки по-прежнему жёсткий бюджет. Строка времени HH:MM:SS занимает восемь клеток, температура с пробелом — пять, итого тринадцать из шестнадцати. Вторая строка: дата «08.03» — пять клеток, пробел, день недели двумя буквами «ПН» — ещё три. Одиннадцать из шестнадцати — остаётся воздух, который я не занимаю: пустота справа делает оба кадра читаемыми с трёх метров, а плотно набитая строка читается как один длинный шум.
День недели по-русски: нумерация, массив, кириллица
Метод now.dayOfTheWeek() возвращает число 0..6, которое библиотека вычисляет из календарной даты — чип про недели не знает. Нумерация на плате: 0 — воскресенье, 6 — суббота; в симуляторе движок считает с понедельника, 0 — ПН. Массив имён обязан идти в порядке нумерации метода, а не в порядке вашего вкуса, и лучшая страховка — калибровка по дате, которую вы помните: вся процедура с проверкой на 1 января 2025 года (среда) разобрана в уроке про дни недели и месяцы. Смещённый массив не роняет программу — он просто неделю показывает «СБ» в пятницу, и замечают это позже, чем обрыв провода.
Почему две буквы, а не «понедельник» целиком: одиннадцать букв съедают всю строку, и дата вместе с ними на экран не влезает. Пара «ПН» опирается на навык чтения настенного календаря и стоит две клетки. Кириллица на LCD — отдельная история: знакогенератор большинства дешёвых экранов латинский, и русские буквы на нём не появятся ни при каком байте. Варианты честные: экран с кириллическим ROM — тогда русский печатается как есть (подробно в уроке про кириллицу в знакогенераторе); translit «PN/VT»; или свои символы через createChar — восемь клеток хватит на весь алфавит сокращений. В симуляторе шрифт браузера рисует кириллицу честно, поэтому на макетке вы увидите «ПН» без ухищрений.
Мигающие двоеточия — то, что отличает часы от таблички. Механика из прежнего урока: чётная секунда — печатаем двоеточие, нечётная — пробел той же ширины, строка не прыгает. Здесь добавка: в режиме правки двоеточие мигает по millis с периодом 800 мс, а не по секундам — быстрый пульс глазами отличим от обычного хода часов, и вы всегда знаете, что находитесь в режиме. На показе период ровно секунда: чаще нет смысла, экран перерисовывает кадры впустую, а редкое мигание выглядит как заикание.
#include <Wire.h>#include <RTClib.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2);RTC_DS3231 rtc; // на плате RTClib нумерует дни с воскресенья: Вс=0const char* DEN[7] = {"ВС", "ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ"}; const int BTN_POLYE = 2; // следующее поле / записьconst int BTN_PLUS = 3; // +1const int BTN_MINUS = 4; // -1 int redakt = 0; // 0 - показ, 1 - правка часов, 2 - правка минутint chR = 0, miR = 0;bool prev1 = false, prev2 = false, prev3 = false;unsigned long lastF1 = 0, lastF2 = 0, lastF3 = 0;unsigned long lastKadr = 0; void dvoe(int v) { if (v < 10) lcd.print("0"); lcd.print(v);} bool klik(int pin, bool &prev, unsigned long &lastF) { // фронт с глушением дребезга bool tek = (digitalRead(pin) == LOW); bool front = (!prev) && tek; prev = tek; if (front && millis() - lastF > 150) { lastF = millis(); return true; } return false;} void kadrik(DateTime now) { // перезапись поверх, без clear() lcd.setCursor(0, 0); if (redakt == 0) { dvoe(now.hour()); if (now.second() % 2 == 0) lcd.print(":"); else lcd.print(" "); dvoe(now.minute()); if (now.second() % 2 == 0) lcd.print(":"); else lcd.print(" "); dvoe(now.second()); lcd.print(" "); lcd.print(rtc.getTemperature(), 1); // 23.5 от датчика кристалла } else { lcd.print("*"); // маркер режима dvoe(chR); if (millis() % 800 < 400) lcd.print(":"); else lcd.print(" "); dvoe(miR); lcd.print(" "); } lcd.setCursor(0, 1); if (redakt == 0) { dvoe(now.day()); lcd.print("."); dvoe(now.month()); lcd.print(" "); lcd.print(DEN[now.dayOfTheWeek()]); lcd.print(" "); // затирание хвостов прошлых кадров } else { if (redakt == 1) lcd.print("pravka: chasy "); else lcd.print("pravka: minuty "); }} void setup() { lcd.init(); lcd.backlight(); pinMode(BTN_POLYE, INPUT_PULLUP); pinMode(BTN_PLUS, INPUT_PULLUP); pinMode(BTN_MINUS, INPUT_PULLUP); if (rtc.begin() == false) { lcd.print("DS3231 ne najden"); while (1) {} }} void loop() { DateTime now = rtc.now(); if (klik(BTN_POLYE, prev1, lastF1)) { if (redakt == 0) { // вход в правку: снимок в черновик redakt = 1; chR = now.hour(); miR = now.minute(); } else { redakt++; if (redakt > 2) { // выход = запись одним вызовом rtc.adjust(DateTime(now.year(), now.month(), now.day(), chR, miR, 0)); redakt = 0; } } } if (redakt > 0) { if (klik(BTN_PLUS, prev2, lastF2)) { if (redakt == 1) { chR++; if (chR > 23) chR = 0; } else { miR++; if (miR > 59) miR = 0; } } if (klik(BTN_MINUS, prev3, lastF3)) { if (redakt == 1) { if (chR == 0) chR = 23; else chR--; } else { if (miR == 0) miR = 59; else miR--; } } } if (millis() - lastKadr >= 200) { lastKadr = millis(); kadrik(now); } delay(20);}Правка кнопками: конечный автомат режима
Правка — это маленький автомат с тремя состояниями: показ, правка часов, правка минут. Первая кнопка листает состояния, вторая и третья крутят значение активного поля, а выход из правки — это одновременно запись: rtc.adjust собирается из свежего снимка даты и черновых часов-минут, секунды обнуляются осознанно — так вы знаете, с какого момента пошли часы. Механику adjust и подвод по компиляции скетча я разбирал в уроке про установку времени; здесь добавляется ровно одно правило — дата для adjust берётся из снимка now, снятого в момент записи, а не из черновика, и тогда правка часов не трогает ни число, ни месяц.
В режиме правки экран обязан показывать черновик, а не живое время. Если оставить на экране rtc.now(), значение будет убегать под пальцами: крутишь минуты, а секунды тикают, и каждый кадр перерисовывает вашу работу — через минуту такой правки хочется разбить прибор. Черновик в переменных chR/miR решает это наглухо: экран статичен, пульсирует только двоеточие с периодом 800 мс, а звёздочка в первой клетке и подпись pravka на второй строке объясняют, чей кадр вы видите. Ровно та же дисциплина полей, что в меню на кнопках вверх-вниз: сначала состояние, потом отрисовка, никогда наоборот.
Дребезг в симуляторе отсутствует — кнопка там идеальный контакт, и фронты чистые. На плате контакт так не умеет: 20–50 мс металлической дрожи дают несколько фронтов на одно нажатие, и черновик перескочит через нужное значение. В скетче стоит глушение по времени: фронт принимается не чаще раза в 150 мс, а полной теории с цепочками RC и программными окнами посвящён урок про антидребезг на счётчике кнопки. И про темп правки: шаг плюс-минус на один удобен для минут, но переводить через полночь двадцатью нажатиями утомительно — не жалейте долгих нажатий, если автомата пишете сами.
Про железо редактора пара заметок. Три кнопки на INPUT_PULLUP со второй ногой на землю — резисторов и конденсаторов не нужно, фильтрация чисто программная. Шину I2C часы и экран делят без конфликтов: у DS3231 адрес 0x68, у типового переходника LCD — 0x27 или 0x3F, и сканер шины из примера Wire показывает обе точки за секунду; если экран молчит, первым делом я проверяю адрес, а не провод. И не вешайте кнопки на те же ноги, что SDA/SCL: однажды перепутанные провода дали мне пустой экран и полчаса поиска при невиновном скетче.
Прогон: щёлкните тумблер — на второй строке появится подпись поля, звёздочка встанет в первую клетку, двоеточие замигает быстрее хода секунд. Ползунок прокрутит черновые часы от 0 до 23, кнопка перебросит на минуты, тумблер обратно — и в мониторе строка про запись, после которой живой ход продолжается с установленного времени, а секунды стартуют с нуля. На плате то же самое делают три тактовые кнопки из скетча выше: первая листает поля и пишет, плюс и минус крутят значение. Перевод часов на летнее и зимнее время этим автоматом занимает тридцать секунд дважды в год — та же правка, что у скетча со сверкой по расписанию, только руками и без сети.
Кадр строится перезаписью поверх старых символов, без lcd.clear() в цикле: полная очистка даёт вспышку пустого экрана, и вся теория с лечением — в уроке про мерцание LCD. Правило фиксированной ширины соблюдено в обоих кадрах: температура 23.5 занимает четыре клетки и с минусом пять, подпись правки дописана пробелами до прежней длины строки — призраки прошлых кадров не живут на экране дольше одного цикла. Тем же способом, обновляя только изменившиеся клетки, экран масштабируется до точечной перерисовки — для часов с секундами это перебор, для меню с графиками уже нет.
Пределы автомата тоже стоит знать. Три состояния и три кнопки — потолок комфорта: добавите настройку будильника на том же экране, и пользователю придётся помнить, на каком экране он живёт, — я так сломал пульт полива и вернулся к трём. Второй предел: правка не должна требовать шпаргалку — подпись поля, звёздочка режима, быстрый пульс двоеточия уже в скетче, и они дешевле инструкции на стене. А кому правки нужны реже раза в год, тому честнее вообще не выводить её на корпус: редко значит никогда.
| Состояние экрана | Строка 1 (16 клеток) | Строка 2 (16 клеток) |
|---|---|---|
| Показ | HH:MM:SS + 23.5 — 13 клеток с воздухом | 08.03 ПН — 8 клеток, хвосты затёрты пробелами |
| Правка часов | *ЧЧ:ММ с миганием 800 мс — черновик | pravka: chasy — подпись поля |
| Правка минут | тот же кадр, пульсирует то же двоеточие | pravka: minuty |
- 1. Что возвращает dayOfTheWeek() на плате в понедельник?
- 2. Почему в режиме правки экран показывает черновик, а не живое время?
- 3. Что происходит с датой при записи rtc.adjust(DateTime(now.year(), now.month(), now.day(), chR, miR, 0))?
- 4. Почему на части LCD 1602 кириллица не печатается?
- 5. Как обновлять кадр часов без мерцания?
- День недели по-русски — массив в порядке нумерации dayOfTheWeek(): на плате Вс=0, в симуляторе ПН=0
- Кириллица на LCD зависит от знакогенератора: ROM-вариант, translit или createChar — и 16 клеток на раскладку
- Мигающие двоеточия: показ — по чётности секунды, правка — по millis с периодом 800 мс
- Правка — автомат: поле листает кнопка, значение крутят плюс-минус, adjust пишет одним вызовом из снимка даты
- Кадр — перезапись поверх без lcd.clear(): фиксированная ширина полей и затирание хвостов пробелами
Частые вопросы по теме урока
- Что возвращает dayOfTheWeek() на плате в понедельник?
- На плате нумерация с воскресенья: Вс=0, Пн=1. В симуляторе движок считает с понедельника, там понедельник — 0, и массив имён переупорядочивается. 1
- Почему в режиме правки экран показывает черновик, а не живое время?
- Живое время тикает каждый кадр, и правка превращается в ловлю убегающих цифр. Черновик в переменных держит экран статичным, пульсирует только двоеточие с периодом 800 мс. Иначе значение убегает под пальцами, пока крутишь плюс-минус
- Что происходит с датой при записи rtc.adjust(DateTime(now.year(), now.month(), now.day(), chR, miR, 0))?
- Поля даты приходят из свежего снимка rtc.now(), черновыми правятся только часы и минуты, секунды стартуют с нуля осознанно. Так правка времени не трогает число и месяц. Дата берётся из даты компиляции скетча
- Почему на части LCD 1602 кириллица не печатается?
- Символы рисует знакогенератор экрана, и у дешёвых модулей он латинский. Выходы: экран с кириллическим ROM, латинская транслитерация или свои символы через createChar. Знакогенератор некоторых экранов латинский — нужен ROM-вариант, translit или createChar
- Как обновлять кадр часов без мерцания?
- clear() вспыхивает пустым экраном на каждом кадре — это и есть мерцание. Перезапись поверх с фиксированной шириной полей и затиранием пробелами держит кадр стабильным. Перезаписью поверх старых символов раз в 200–500 мс, без clear()