Кормушка для кота была моим первым «умным домом»: в 7:30 утра открывается шлюз, сыплется корм, кот доволен. Логика заняла шесть строк — и шесть дней я не мог понять, почему зуммер орёт с 7:30 до 7:31 без остановки, а реле щёлкает шестьдесят раз подряд. Условие было безупречным на вид: если час равен семи и минута равна тридцати — сработай. Проблема в том, что это условие истинно все шестьдесят секунд, а loop() успевает пробежать тысячи раз за секунду. Будильник, который не умеет останавливаться, — классический первый тупик в работе с часами, и в этом уроке мы его обойдём по всем правилам.
Соберём настольные часы с будильником: DS3231 отвечает за время, LCD 16x2 показывает его человеку, зуммер сигналит в заданный час и минуту — ровно один раз в сутки. По дороге разберём два устройства на одной шине I2C, ведущие нули, флаг «уже сработал» и дневной цикл.
Два устройства на одной шине I2C
Экран с переходником I2C и модуль часов вешаются на одни и те же два пина: SDA (A4) и SCL (A5). Шина I2C для этого и придумана: на пару проводов навешивается десяток устройств, каждое отвечает на свой адрес. У типового LCD-переходника на микросхеме PCF8574 адрес 0x27, у DS3231 — 0x68. Конфликта нет: оба адреса разные, библиотеки сами разбираются, кому какой пакет.
Инициализация двойная: lcd.init() с lcd.backlight() для экрана и rtc.begin() для часов — каждая библиотека будит своё устройство. Оба модуля питаются от 5 В, ток копеечный: подсветка экрана — 20-25 мА, часы — доли миллиампера. Проверка подключения делается за минуту: сканер I2C-адресов из раздела про Wire покажет на шине сразу две отметки — 0x27 и 0x68.
Часы на LCD: курсор и ведущие нули
LCD 16x2 рисует символы по клеткам: левый верхний угол — позиция (0,0), первый символ второй строки — (0,1). Каждый цикл я ставлю курсор в (0,0), печатаю время, а дату ставлю во вторую строку. Экран не стирается между кадрами — и это правильно: lcd.clear() вызывает видимое мигание, а перезапись поверх старых символов проходит бесшовно. Следите только, чтобы новая строка была не короче старой, иначе останутся хвосты от предыдущей.
Ведущие нули решаются маленькой функцией: если число меньше десяти — сначала печатаем символ ноль, потом само число. Без неё 9:05 выглядит как 9:5, и часы кажутся самоделкой из-под подушки.
Нюанс, о котором никто не предупреждает: на классических экранах с контроллером HD44780 кириллицы нет — в его знакогенераторе латиница и японская катакана. Надписи латиницей — не лень, а суровая правда железа; русские строки возможны через самодельные символы, но это отдельное ремесло. Поэтому в моих часах Alarm, а не «Будильник».
#include <Wire.h>#include <RTClib.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2);RTC_DS3231 rtc; void printTwoDigits(int value) { // 5 -> 05 if (value < 10) lcd.print("0"); lcd.print(value);} void setup() { Serial.begin(9600); lcd.init(); lcd.backlight(); rtc.begin();} void loop() { DateTime now = rtc.now(); lcd.setCursor(0, 0); printTwoDigits(now.hour()); lcd.print(":"); printTwoDigits(now.minute()); lcd.print(":"); printTwoDigits(now.second()); lcd.setCursor(0, 1); printTwoDigits(now.day()); lcd.print("."); printTwoDigits(now.month()); lcd.print("."); lcd.print(now.year()); delay(500);}Ловушка: условие срабатывает 60 раз
Наивный будильник выглядит так: if (now.hour() == 7 && now.minute() == 30) — сработай. Часы врут? Нет — врут ожидания. Условие истинно в течение всех шестидесяти секунд с 7:30:00 по 7:30:59, а loop() прогоняется тысячи раз в секунду. Зуммер перезапускается всю минуту, реле щёлкает, как дрель.
Лечится флагом. Заводим bool fired = false; рядом с часом и минутой будильника. Когда время совпало и флаг ещё не поднят — срабатываем один раз и поднимаем флаг. Остальные проверки внутри той же минуты проходят мимо. А когда минута закончилась — сбрасываем флаг, и будильник снова заряжен. Это и есть дневной цикл: в 7:30 — выстрел, дальше тишина, завтра в 7:30 — снова выстрел.
Сброс флага — самое деликатное место. Правильно сбрасывать в ветке else: любая минута, которая не будильная, возвращает флаг в false. Если же сбрасывать по секундной шкале — по second() или по таймеру внутри минуты, — легко поймать повторное срабатывание на границе секунд или, наоборот, будильник, который молчит, потому что флаг поднялся дважды.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc; const int ALARM_HOUR = 7;const int ALARM_MINUTE = 30;bool fired = false; void setup() { Serial.begin(9600); rtc.begin(); // время уже установлено ранее; строку с adjust здесь не повторяем} void loop() { DateTime now = rtc.now(); if (now.hour() == ALARM_HOUR && now.minute() == ALARM_MINUTE) { if (fired == false) { fired = true; tone(8, 1000, 500); Serial.println("Budilnik! 7:30"); } } else { fired = false; // минута прошла - снова заряжен } delay(200);}- Добавьте на вторую строку LCD надпись Alarm HH:MM — по образцу ведущих нулей из часов.
- Сделайте тишину во второй половине будильной минуты: сравнивайте now.second() с 30 и подавайте tone только до тридцати.
- Заведите второй будильник — на минуту после первого — и убедитесь, что оба срабатывают по одному разу. Понадобится два флага или массив флагов.
- Перенесите вызовы tone/noTone в отдельную функцию alarmTick(int hour, int minute) и вызывайте её из loop() — логика отделится от звука.
- LCD 16x2 (адрес 0x27) и DS3231 (0x68) живут на одной шине I2C — два провода A4/A5, у каждого устройства свой адрес
- Условие hour == h && minute == m истинно 60 секунд: без флага будильник сработает тысячи раз
- Схема «флаг + сброс в else» даёт ровно одно срабатывание в сутки — это и есть дневной цикл
- Задержки в loop() при работе с RTC — не больше пары сотен мс, иначе можно проскочить будильную минуту
- На контроллере HD44780 нет кириллицы без самодельных символов — надписи латиницей
- tone(пин, частота, длительность) не блокирует loop(); повтор звука — периодическими вызовами
- 1. Почему наивное условие будильника срабатывает 60 раз?
- 2. Что хранит bool fired и когда его сбрасывать?
- 3. Какой адрес у DS3231 на шине I2C?
- 4. Почему надпись Alarm пишут латиницей?
- 5. Что произойдёт, если будильную проверку спрятать за delay(60000)?
- 6. Как сделать, чтобы будильник звенел каждое утро, а не один раз в жизни?
Получился законченный прибор: точные часы, экран, будильник с дневным циклом. Куда расти дальше? Храните будильники в EEPROM — тогда они переживут отключение питания; добавьте три кнопки и настройку времени без перепрошивки; выведите температуру — у DS3231 есть встроенный датчик, и RTClib умеет его читать. А когда надоест ручная установка времени, в разделе про ESP32 синхронизируем эти же часы по интернету.
Частые вопросы по теме урока
- Почему наивное условие будильника срабатывает 60 раз?
- Сравнение hour/minute не различает секунды: вся будильная минута выглядит как «время пришло» тысячи раз подряд. Условие истинно всю минуту, а loop() выполняется тысячи раз в секунду
- Что хранит bool fired и когда его сбрасывать?
- Флаг поднимается при первом срабатывании и возвращается в false в любой небудильной минуте — так работает дневной цикл. Признак «уже сработал»; сброс — когда будильная минута закончилась, в ветке else
- Какой адрес у DS3231 на шине I2C?
- 0x68 — часы, 0x27 — типовой LCD-переходник на PCF8574. Адреса уникальны, конфликтов нет. 0x68
- Почему надпись Alarm пишут латиницей?
- Знакогенератор классических экранов содержит латиницу и катакану; русские буквы требуют самодельных символов. Контроллер HD44780 не умеет кириллицу без самодельных символов
- Что произойдёт, если будильную проверку спрятать за delay(60000)?
- Цикл просыпается раз в минуту и может заглянуть между 7:30:05 и 7:31:05. Задержки в RTC-скетчах — короткие. Есть шанс проскочить будильную минуту целиком — сигнал не прозвучит
- Как сделать, чтобы будильник звенел каждое утро, а не один раз в жизни?
- Сброс флага в небудильные минуты перезаряжает механизм: завтра в 7:30 он выстрелит снова. Сбрасывать флаг после будильной минуты — в ветке else