Настольные часы для мастерской я рисовал с паузой delay(500) — так они собраны в уроке про раскладку на LCD, и для чистых часов это честный выбор. Всё сломалось, когда я прикрутил к этим же часам опрос датчика с собственным delay(2000): двоеточие стало замирать на такт, секунды перескакивать через две, а подвод времени кнопками превратился в лотерею — держишь кнопку, а плата спит в паузе и не слышит нажатий. Хуже всего выглядел момент правки: ткнул в кнопку, ничего не случилось, ткнул ещё раз — время уехало на два часа. Часы шли, но жили своей жизнью, отдельно от остального скетча.
Соберём часы по-другому: перерисовка не привязана к паузе, а случается в момент смены секунды. Каждым кругом loop() снимает rtc.now() — это дешёвый вызов, доли миллисекунды, — сравнивает секунду с прошлой и рисует кадр только когда она изменилась. Остальное время loop свободен: кнопки опрашиваются на каждом круге, датчики не ждут, и никакой delay в главном цикле не живёт. Ритм без пауз — та же дисциплина, что в уроке про millis вместо delay, только период тут задаёт не таймер, а сами часы. Между кадрами живёт обычная работа: опрос датчика, разбор команд из монитора, мигание служебного светодиода — часы не требуют себе выделенного времени, потому что один кадр — это пересчёт полей и десяток байт на шину.
Что это даёт в числах. Перерисовка раз в секунду против раз в 500 мс — вдвое меньше трафика на шине I2C, где висят и часы, и дисплей. Кадр не пропускает показаний, пока loop укладывается в секунду. И лучший бонус: подвод времени кнопками отвечает мгновенно — после правки дата изменилась, секунда тоже, экран перерисовался сам, без отдельного кода «обновить немедленно». Заодно заметка на полях: сам снимок now() стоит доли микросекунды и не создаёт строк — снимать его хоть тысячу раз в секунду можно без сожалений; дорого не чтение, а перерисовка.
Смена секунды как событие
Механика помещается в две строки: переменная poslSek держит секунду прошлого кадра, а кадр рисуется под условием now.second() != poslSek, после чего счётчик обновляется. Снимок now() обязан жить в начале каждого круга loop() — именно про вынос его в setup() с последующим «застывшим» временем был разбор в уроке про LCD, здесь та же дисциплина. Всё остальное — обычная раскладка: клетки считаются до печати, ведущие нули рисует помощник z2, и поверх старых символов пишутся новые, без clear() и без мигания.
Подвод времени — то место, где событийность раскрывается. На плате я ставлю две кнопки на пины 2 и 3: первая добавляет час, вторая минуту, антидребезг на millis() держит интервал двести миллисекунд между срабатываниями, а правка уходит через rtc.adjust с собранным заново DateTime — та же замена времени, которой проверяют расписания в уроке про установку времени. Дальше работает чистая арифметика события: секунда после правки почти наверняка сменилась — экран перерисовался в тот же круг. Никакого «флага нужно обновить экран» в проекте нет, и это моё любимое свойство подхода: меньше состояний — меньше мест, где они расходятся. Единственная тонкость кнопочного подвода — переполнение: час 23 плюс один обязан стать нулём, а не двадцатью четырьмя, поэтому в правке стоит остаток от деления; минуты — та же история с шестьюдесятью. Долгое удержание кнопки прокручивает сутки — это нормально, именно так подводят время в настольных приборах.
В симуляторе кнопки уже живут на модуле часов: −1ч, ±1м, +1ч и синхронизация с браузером. Ткните +1м во время работы — и посмотрите, как кадр оживёт в ту же секунду. С двоеточием всё как в большом уроке раскладки: чётная секунда — «:», нечётная — пробел; в событийных часах эта проверка живёт внутри кадра и бесплатна.
#include <Wire.h>#include <RTClib.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2);RTC_DS3231 rtc; const int KNOP_CHAS = 2; // +1 часconst int KNOP_MIN = 3; // +1 минута int poslSek = -1;unsigned long poslNazhatie = 0; void z2(int v) { // 5 -> 05 if (v < 10) lcd.print("0"); lcd.print(v);} void setup() { lcd.init(); lcd.backlight(); pinMode(KNOP_CHAS, INPUT_PULLUP); pinMode(KNOP_MIN, INPUT_PULLUP); rtc.begin(); rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));} void loop() { DateTime now = rtc.now(); // подвод времени: антидребезг 200 мс на millis if (millis() - poslNazhatie > 200UL) { if (digitalRead(KNOP_CHAS) == LOW) { poslNazhatie = millis(); rtc.adjust(DateTime(now.year(), now.month(), now.day(), (now.hour() + 1) % 24, now.minute(), now.second())); } if (digitalRead(KNOP_MIN) == LOW) { poslNazhatie = millis(); rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), (now.minute() + 1) % 60, now.second())); } } // кадр рисуется только в момент смены секунды if (now.second() != poslSek) { poslSek = now.second(); lcd.setCursor(4, 0); // (16-8)/2 = 4 z2(now.hour()); lcd.print(":"); z2(now.minute()); lcd.print(":"); z2(now.second()); }}Ритм перерисовки и его границы
У любого ритма есть цена и граница, и у событийного она такая: если loop занят дольше секунды, показание между кадрами пропадёт — экран честно покажет 09:05:58, а следующим кадром 09:06:00. Лечится не ускорением перерисовки, а освобождением цикла: тяжёлые опросы уходят на интервалы millis, а для педантичного тика существует аппаратный выход — квадратная волна 1 Гц с ножки DS3231 и прерывание по ней, вся связка собрана в уроке про SQW и пробуждение. Часы, которые идут на прерывании, не зависят ни от занятости loop, ни от его пауз. Для большинства приборов, впрочем, смена секунды достаточна: секунда длинная, датчики короткие, и конфликт возникает лишь тогда, когда кто-то тащит в главный цикл паузы, которые и без часов там не нужны.
Мое правило по числам: кадр чаще секунды часы не рисуют ничего полезного — секунды меняются раз в секунду, промежуточные кадры только грузят шину. При событийном подходе обновлений ровно шестьдесят в минуту; при delay(500) их было бы сто двадцать, и половина — бессмысленные повторы того же кадра. Для OLED-версии цифры те же, но маршрут другой: библиотека рисует в буфер ОЗУ, и кадр выносится вызовом display() — буфер и его особенности разобраны в уроке про буфер OLED, а крупный циферблат на нём — в уроке про часы на OLED. Перезапись поверх без clear() остаётся в силе на обоих экранах, дата при секундной перерисовке не трогается вовсе — её рисуют отдельным кадром раз в минуту, когда меняется минута.
| Стратегия обновления | Обновлений в минуту | Слабое место |
|---|---|---|
| delay(500) | 120 | loop спит: кнопки и датчики ждут паузы |
| Миллис-интервал 1000 мс | 60 | может рисовать дважды за одну секунду |
| Смена секунды (этот урок) | 60 | loop занят дольше секунды — показание пропускается |
| SQW 1 Гц + прерывание | 60 | нужна ножка INT и дисциплина коротких ISR |
Проверка в симуляторе занимает полминуты. Часы идут, двоеточие пульсирует, строка статуса приходит раз в две секунды — и всё это без единого delay в скетче. Теперь ткните +1м на модуле часов: показание уедет на минуту вперёд и появится на экране в тот же круг, потому что смена секунды после правки неизбежна. Ткните +1ч несколько раз подряд — каждый щелчок доехал до экрана, ни одно нажатие не утонуло в паузе: вот что значит свободный loop. Если хотите прочувствовать разницу подходов, перепишите условие на миллис-интервал: часы пойдут так же, но двойной перерисовки одной секунды уже не избежать — увидите её в трафике шины. Строку статуса в мониторе можно спугнуть подводом: нажмите кнопку модуля в момент печати — строка придёт со свежим временем, и по ней видно, что статус и кадр живут на одном такте, а не в разных таймерах.
Отдельный повод потрогать строку статуса в Serial: она печатается внутри того же кадра по смене секунды, а её собственный ритм держит millis — два слоя планирования в десяти строках. Обратите внимание, как на этот такт ложится подвод времени: правка уходит в чип вне зависимости от того, внутри кадра она случилась или между кадрами, — и следующим кадром экран уже показывает новое время. На плате добавьте к ним кнопки из code-блока, и часы станут прибором: время подводится без перепрошивки, экран живёт своей секундой, а главный цикл всегда свободен для следующей задачи.
- 1. Когда событийные часы рисуют новый кадр?
- 2. Почему после правки времени кнопкой экран обновляется без отдельного кода?
- 3. Loop занят 2 секунды между кругами. Что покажут событийные часы?
- 4. Сколько обновлений экрана в минуту даёт delay(500) и смена секунды соответственно?
- 5. Где обязан жить вызов rtc.now() в событийных часах?
- Кадр рисуется по смене секунды: 60 обновлений в минуту против 120 у delay(500), вдвое меньше I2C-трафика
- Снимок rtc.now() — в начале каждого круга loop(); снимок в setup() даёт застывшее время
- loop без delay остаётся свободным: кнопки подвода времени отвечают мгновенно, датчики не ждут
- Граница подхода: loop занят дольше секунды — показание пропускается; педантичный такт даёт SQW 1 Гц
- На OLED маршрут другой: рисуем в буфер, кадр выносит display(); перезапись поверх вместо clear() — на обоих экранах
Часы стали живыми — следующий шаг делает их полезными: будильник поверх этого же каркаса с зуммером и флагом тревоги собран в уроке про часы с будильником, а тревогу без участия loop возьмут аппаратные будильники самого чипа. Экран при этом остаётся свободен для чего угодно: секунда — длинный интервал, и места в нём хватает всем.
Частые вопросы по теме урока
- Когда событийные часы рисуют новый кадр?
- Кадр привязан к смене секунды: шестьдесят обновлений в минуту, ни одного лишнего. Снимок now() при этом снимается каждым кругом. Когда секунда rtc.now() отличается от прошлого кадра
- Почему после правки времени кнопкой экран обновляется без отдельного кода?
- Это следствие событийности: now.second() после правки отличается от poslSek — условие срабатывает само. Флага «обновить экран» в проекте нет. Правка почти наверняка меняет секунду, а кадр рисуется по её смене
- Loop занят 2 секунды между кругами. Что покажут событийные часы?
- Подход честно показывает последнее увиденное. Пропуски лечатся освобождением цикла или тиком от SQW на 1 Гц, а не учащением перерисовки. Покажут последнее увиденное показание — часть секунд пропадёт
- Сколько обновлений экрана в минуту даёт delay(500) и смена секунды соответственно?
- Пауза 500 мс рисует 120 кадров, и половина повторяет тот же кадр. Смена секунды — ровно 60 полезных, вдвое меньше трафика на I2C. 120 и 60
- Где обязан жить вызов rtc.now() в событийных часах?
- Снимок в setup() — застывшее время, знакомый симптом. Снимок в начале круга loop даёт свежую секунду для сравнения и честные поля для кадра. В начале каждого круга loop()