Будильник для кормушки я написал за две минуты и получил глупейшую аварию: условие «час 7 и минута 30» истинно всю тридцатую минуту — раз в секунду, шестьдесят раз подряд. Зуммер не умолкал всю минуту, кормушка сыпала порцию шестьдесят раз, аквариум получил суточную норму за минуту. Вторая версия с точным сравнением «7:30:00» упала в другую сторону: loop() со sleep опоздал на полсекунды, секунда проскочила — и кормушка не сработала вовсе. Обе ошибки — из одной семьи: время с RTC приходит каждую секунду, и сравнение нужно строить так, чтобы срабатывание было ровно одно.
Схема, которая не ломается: переведите текущее время и цель в минуты от полуночи, задайте окно из пяти минут, держите флаг fired и гасите его после полуночи. Сработал один раз — и всё, хоть сотня проходов loop() приходится на окно. DS3231 для этого идеален: у него и точность приличная, и чтение времени — одна строка rtc.now(). В уроке про зуммерный будильник эта же идея живёт с snooze-кнопкой, здесь разберём сам механизм сравнения.
Минуты от полуночи: одно число вместо четырёх
DateTime отдаёт год, месяц, день, час, минуту, секунду по отдельности, но сравнивать по четырём полям — рассадник ошибок: забытый час, перепутанные границы, условие, которое не бывает ложным. Переводим всё в минуты от полуночи: now.hour() * 60 + now.minute() — и сутки становятся числом от 0 до 1439. Цель «7:30» — это 450, «19:45» — 1185, и вся логика будильника сжимается до сравнения двух int. Секунды в будильнике обычно не нужны: событие «в 7:30» означает «между 7:30 и 7:35», а не «в 7:30:00 и ни секундой позже».
Флаг fired — вторая половина механизма: условие с окном истинно сотни раз, но действовать оно должно один раз. Сработали, подняли флаг — следующие проверки молчат. Гасится флаг там, где кончились сутки: как только минуты от полуночи снова стали меньше цели (наступила полночь и утро далеко), будильник снова armed. Ручной сброс кнопкой, если нужно перезапустить сегодня же, — это отдельный приём из урока про snooze, здесь держим чистую суточную схему.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;const int PIN_BUZZ = 8; const int ALARM_MIN = 7 * 60 + 30; // цель: 450 минут от полуночиconst int WINDOW_MIN = 5; // окно: сработать можно в 7:30-7:35bool fired = false; void setup() { Serial.begin(9600); pinMode(PIN_BUZZ, OUTPUT); if (!rtc.begin()) { Serial.println("DS3231 не найден: проверьте провода A4/A5"); while (true) {} } Serial.println("будильник на 7:30 заведён");} void loop() { DateTime now = rtc.now(); int mins = now.hour() * 60 + now.minute(); if (!fired && mins >= ALARM_MIN && mins < ALARM_MIN + WINDOW_MIN) { fired = true; Serial.print("будильник! "); Serial.print(now.hour()); Serial.print(":"); Serial.println(now.minute()); for (int i = 0; i < 3; i++) { // три бипа, а не минута сирены tone(PIN_BUZZ, 1000, 200); delay(400); } } if (mins < ALARM_MIN) fired = false; // после полуночи снова armed delay(1000);}Условие читается как пять слов: «ещё не сработали» и «внутри окна». Окно в пять минут защищает от пропуска: если loop() спал в глубоком сне или RTC читался раз в минуту, событие всё равно поймается — будильники DS3231 с аппаратными прерываниями нужны только тогда, когда плате положено спать между срабатываниями. Строка if (mins < ALARM_MIN) fired = false гасит флаг всю вторую половину ночи, с 00:00 до 7:29, — срабатывание снова возможно на следующее утро.
Секунды здесь сознательно выброшены: будильник «в 7:30» на практике — окно, а не мгновение. Если событие правда секундное (фиксация метки в журнал), сравнивайте unixtime: now.unixtime() >= цель — арифметика эпохи у нас разобрана. А вот сравнивать объекты DateTime через == бесполезно: DateTime равен другому только при полном совпадении всех полей до секунды, и такое совпадение в потоке времени не встретится никогда.
| Схема гашения повтора | Как работает | Где ломается |
|---|---|---|
| Точное == 7:30:00 | срабатывает одну секунду | loop опоздал — событие пропущено навсегда |
| Час и минута без флага | истинно все 60 секунд окна | шестьдесят повторов: сирена и sixty порций корма |
| Окно + флаг fired (этот урок) | один срабатывание, сброс после полуночи | почти не ломается; забытый сброс — будильник одноразовый |
| Аппаратный будильник DS3231 + SQW | прерывание будит плату из сна | нужен пин INT и ручное стирание флага в регистре |
- 1. Почему условие «час == 7 и минута == 30» без флага — ошибка?
- 2. Зачем окно из пяти минут, если есть флаг?
- Время сравнивается в минутах от полуночи: hour()*60 + minute() — одно int вместо четырёх полей
- Окно из 3-5 минут + флаг fired: срабатывание одно, пропуск невозможен
- Сброс флага — по условию «цель ещё не наступила», не по поиску точной полуночи
- DateTime через == не сравнивают: равенство всех полей до секунды не встречается; для меток есть unixtime()
Частые вопросы по теме урока
- Почему условие «час == 7 и минута == 30» без флага — ошибка?
- Пока идут тридцатая минута восьмого часа, условие истинно на каждом проходе loop(). Без флага «сработал» получите сирену на минуту и шестьдесят порций корма. Оно истинно всю минуту: событие повторится до шестидесяти раз
- Зачем окно из пяти минут, если есть флаг?
- Точная секунда проскакивает при редких чтениях или сне. Окно ловит событие в пяти минутах, а флаг гарантирует, что внутри окна срабатывание будет одно. Чтобы не пропустить событие, если loop опоздал или читал RTC редко