Кормушка из урока про серво-дозатор месяц работала безупречно и подвела в самый невинный момент: на даче мигнул свет, плата перезагрузилась — и утренняя выдача уехала с семи утра на половину восьмого. Расписание-то было не «в семь утра», а «через двенадцать часов после включения». millis() честно отсчитал свои миллисекунды от нового старта, а на каком часу это случилось, его не интересовало. Кот, между прочим, заметил.
В этом уроке привяжем выдачу порции к настоящему времени: DS3231 хранит часы, серво на пине 11 открывает заслонку, а скетч сверяет момент с расписанием из двух слотов — 7:00 и 19:00. Механику дозатора — углы, окно выдачи, время открытого состояния — я беру готовой из серво-урока и пересказывать не буду: здесь речь о том, чтобы кормление случилось в назначенный час, а не когда получится.
Пару слов о масштабе задачи, чтобы не перемудрить. Домашнему коту хватает двух выдач в сутки; серво SG90 при этом делает около семисот циклов в год — при ресурсе в тысячи циклов он их даже не заметит. Ток тоже копеечный: выдача длится 0,7 секунды и тянет порядка 650 мА только в момент движения, всё остальное время серво молчит. Поэтому кормушку я питаю от сетевого адаптера, а не от батареек: батарейный проект тут выигрывает копейки, зато заводит будильник на замену элементов.
Почему millis-расписание сползает
У расписания на millis() две врождённые беды. Первая — точка отсчёта. Период «каждые 43 200 000 миллисекунд» считается от момента включения платы, и вся привязка к суткам держится на чистом везении: включили в 7:00 — и двенадцатичасовые отметки сходились с календарём. Любая перезагрузка — сторожевой сброс, мигание света, выдёрнутый провод — переносит всё расписание на новый момент включения.
Вторая беда — ход самого резонатора. Керамический резонатор Uno живёт с допуском порядка ±0,5 %, и за двенадцатичасовой период это до трёх-четырёх минут гулянки на каждом цикле. Недельный журнал моей дачной версии показывал выдачи в 7:02, 7:06, 6:58: период честный, а какое на дворе время — ему неведомо. Если слот важен точнее, чем «примерно к обеду», нужен якорь в настоящем времени.
Отдельно про отключение питания. millis() умирает вместе с платой — и вместе с ним умирает счётчик «сколько уже прошло». Часы на батарейке — нет: DS3231 тикает от CR2032 годами, и после включения первый же rtc.now() вернёт правильный момент. Это разделение я разбирал по косточкам в уроке про millis против часов — там же про lostPower(), который подскажет, что батарейка садилась и время пора перезаводить.
И ещё одна причина, по которой для кота я держу слоты, а не интервал «раз в двенадцать часов» даже на честном unixtime. Интервал ведёт себя как маятник: пропустили выдачу или покормили вручную — все следующие уплыли на полсуток вперёд. Слот же привязан к календарю: что бы ни случилось, вечернее кормление случится в 19:00, и хозяин может подстроить под него свой день. Рыбкам и поливу, где важна только пауза между выдачами, интервал подходит лучше — там календарь никто не смотрит.
Слоты кормления и флаги
Слот — это час и минута, в которые кормление обязано случиться. Проверка выглядит как будильник: текущий час и минута совпали со слотом — сработай. Подводный камень тот же, что у звонка из урока про часы с будильником: условие истинно всю минуту, а loop() прогоняется тысячи раз. Лечится флагом на каждый слот: сработали — подняли; минута прошла — сбросили в ветке else, и слот снова заряжен на завтра.
Когда слотов больше одного, флаги складываются в массив. У меня их два — утро и вечер, это ровно то, что держит домашнего кота в форме; в самодельных кормушках для рыб чаще встречаются одна-две выдачи в сутки, раз в двенадцать часов. Захотите три слота — добавляете третью пару чисел и третий флаг, скетч не меняется. А само время на модуле ставится одной строкой с adjust, как в самом первом уроке раздела.
Для демо и для команды «покормить сейчас» пригодится unixtime: разница двух моментов в секундах — честное «прошло двадцать секунд» без календарных тонкостей. На плате интервалы меряют прямо по rtc.now().unixtime(). В симуляторе ниже часы движка дают только момент запуска и дальше стоят, поэтому виртуальное время тикает от millis() с якорем от часов — а логика проверки не меняется ни на букву.
Процессу проверки я уделяю минуту сомнений, потому что новичкам кажется, что дёргать часы часто — расточительно. Считаем: rtc.now() читает по I2C семь байт, на стандартных 100 кГц это доли миллисекунды. Десять проверок в секунду съедят меньше одного процента времени платы — при условии, что между ними нет delay(). Если в скетче есть ещё работа — дисплей, датчики, — разносите её по таймерам, как в уроке про millis вместо delay, а слоты проверяйте в каждом проходе loop().
#include <Wire.h>#include <RTClib.h>#include <Servo.h> RTC_DS3231 rtc;Servo srv; const int ZAKRYTO = 15; // углы дозатора - из урока про серво-кормушкуconst int OTKRYTO = 75; const int CHAS_KORM[] = {7, 19}; // слоты кормленияconst int MINUTA_KORM[] = {0, 0};const int SLOTOV = 2;bool srezal[2] = {false, false}; // флаг "уже выдал" на каждый слот void setup() { Serial.begin(9600); rtc.begin(); if (rtc.lostPower()) { // батарейка садилась - время надо поставить rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); } srv.attach(11); srv.write(ZAKRYTO); Serial.println("Kormushka: sloty 7:00 i 19:00");} void loop() { DateTime now = rtc.now(); for (int i = 0; i < SLOTOV; i++) { if (now.hour() == CHAS_KORM[i] && now.minute() == MINUTA_KORM[i]) { if (srezal[i] == false) { srezal[i] = true; nakormit(); } } else { srezal[i] = false; // минута прошла - слот снова заряжен } } if (Serial.available() > 0) { // покормить сейчас, с меткой времени while (Serial.available() > 0) Serial.read(); nakormit(); } delay(200);} void nakormit() { DateTime now = rtc.now(); Serial.print("Kormlenie v "); Serial.print(now.hour()); Serial.print(":"); Serial.print(now.minute()); Serial.print(":"); Serial.println(now.second()); srv.write(OTKRYTO); delay(700); srv.write(ZAKRYTO);}| Планировщик | После перезагрузки | Точность к настенным часам | Где годится |
|---|---|---|---|
| millis-период | отсчёт заново от момента включения | резонатор ±0,5% — минуты гулянки за сутки | интервалы «не чаще чем», мигалки |
| unixtime-интервал | якорь берётся из часов, интервал живёт | равна точности RTC | паузы между выдачами, защита от дублей |
| слоты час:минута + флаги | слоты в скетче, время — в RTC на батарейке | равна точности RTC | кормление, полив, всё по расписанию дня |
Что смотреть в симуляторе. Метки времени в мониторе растут вместе с выдачами: расписание привязано к «часам», а не к счётчику порций. Ручная выдача из Serial приходит с собственной меткой и не сдвигает плановую — если хотите, чтобы внеочередная порция переносила и следующий слот, добавьте lastFeed обновление внутрь ветки Serial и сравните поведение. Сюда же просится счётчик порций в EEPROM: два байта и рецепт из урока про счётчик включений — и журнал выдач переживёт любое мигание света.
Технически якорь устроен так: в setup() из boot.hour(), boot.minute() и boot.second() собираются секунды суток, и дальше виртуальная шкала непрерывно растёт от запуска. Интервалы живут в этой непрерывной шкале, поэтому полночь их не ломает; за склейку суток отвечает остаток от деления на 86 400, из которого берутся часы и минуты для печати. На настоящей плате вместо virtSek стоит now.unixtime() — шкала та же по смыслу, только вестись она будет месяцами, а не до закрытия вкладки.
Оговорка о честности: раз в двадцать секунд — демонстрационный темп, иначе наблюдателю пришлось бы смотреть в монитор двенадцать часов. На настоящей кормушке массив CHAS_KORM из скетча выше — тот же механизм при нормальных интервалах: два раза в сутки, в 7:00 и 19:00. Симулятор проверяет логику, плата — расписание; путать их назначения не стоит.
- Поставьте третий слот на 13:30 и добавьте для него флаг — убедитесь, что массив слотов расширяется без переписывания логики.
- Верните период 12 часов из серво-урока, но через unixtime: храните момент последней выдачи и требуйте разницы в 43200 секунд.
- Заведите лимит: если с последней выдачи прошло меньше 3600 секунд, команда из Serial получает отказ и печатает, сколько ждать.
- Печатайте в Serial момент следующей плановой выдачи — удобно сверять с расписанием в телефоне, не заглядывая в код.
- Расписание «каждые 12 часов» живёт на millis() и умирает от перезагрузки; слот «7:00» живёт в RTC на батарейке
- Слот = час:минута плюс личный флаг; сброс флага — в ветке else, когда слотная минута прошла
- Проверяйте слоты каждые 100–200 мс: длинный delay() внутри loop() пропускает будильные минуты
- unixtime() даёт моменты в секундах: интервалы «не раньше чем через N» считаются вычитанием
- Углы заслонки (15° закрыто, 75° открыто) — из серво-урока; механика и расписание сознательно живут в разных уроках
- 1. Почему выдача «раз в 12 часов» на millis() уехала после мигания света?
- 2. Как часто достаточно проверять слоты в loop()?
- 3. Как ведёт себя флаг слота?
- 4. Как защититься от двойной выдачи — слотной и ручной в одну минуту?
- 5. Где хранить счётчик выданных порций, чтобы он пережил отключение питания?
Расписание встало на настоящие часы, и кормление больше не зависит от миганий света. Остался последний враг — сами часы: если в кормушке стоит дешёвый DS1307, его кварц уползает на минуты в месяц, и слот 7:00 медленно дрейфует по утрам. Заметьте: смена модуля в скетче этого урока — одна строка, библиотека одинаковая; разница проявится не в коде, а в том, через сколько недель слот перестанет совпадать с семью утра. Следующий урок — про то, сколько секунд теряют дешёвые часы и куда они деваются, с раскладкой по источникам и гонкой двух модулей в цифрах.
Частые вопросы по теме урока
- Почему выдача «раз в 12 часов» на millis() уехала после мигания света?
- Точка отсчёта millis() — момент включения платы: любая перезагрузка переносит все периоды. Слоты в RTC переживают сброс, потому что тикают от батарейки, а не от питания платы. millis() считает от включения, и расписание переехало на новый момент старта
- Как часто достаточно проверять слоты в loop()?
- Длинные задержки пропускают слот целиком: цикл засыпает в 7:00:05 и просыпается в 7:01:05. Пара сотен миллисекунд между проверками — норма; чтение времени по I2C стоит доли миллисекунды. Каждые 100–200 мс, чтобы не проскочить слотную минуту
- Как ведёт себя флаг слота?
- Флаг — приём из урока про будильник: внутри слотной минуты он говорит «уже выдал», а сброс в else перезаряжает слот на следующий день без участия человека. Поднимается при срабатывании в слотную минуту, сбрасывается в ветке else после неё
- Как защититься от двойной выдачи — слотной и ручной в одну минуту?
- unixtime даёт моменты в секундах: разница «сейчас минус последняя выдача» — честный интервал, который переживает и полночь, и перезагрузку с якорем от RTC. Сравнить unixtime последней выдачи: если прошло меньше заданных секунд — отказ
- Где хранить счётчик выданных порций, чтобы он пережил отключение питания?
- EEPROM хранит данные без питания, millis() и переменные ОЗУ умирают вместе с платой, а DateTime — лишь снимок одного момента, а не накопитель. В EEPROM