Насос в моей теплице щёлкает по расписанию, и однажды он устроил двойной сеанс: в 7:00 полил как положено, а в 7:06, после того как моргнул свет, включился заново и долил поверх мокрых грядок. Скетч был без обмана — окно с 7:00 до 7:10, реле включено, пока условие верно. Только флаг «уже полил» жил в переменной, а переменная умерла вместе с питанием. Arduino проснулся, увидел 7:06 — внутри окна — и добросовестно начал всё сначала. Мораль тут простая: само окно — половина дела, вторая половина — память о том, что сеанс уже был.
Базовое расписание с окнами и днями недели собрано в уроке про таймер полива на неделю — тот скетч задаёт каркас, и я не пересказываю его здесь. Спускаюсь ниже: почему сравнение «на равенство» не работает даже с точными часами, как построить окно 07:00–07:10, куда положить флаг, чтобы пережить перезагрузку, и что делать, если полив пропущен целиком. Вариант, где датчик влажности имеет право перекричать часы, я собирал отдельно — в уроке про автополив с датчиком почвы; тут вода идёт чисто по часам.
Окно вместо мгновения
Первая интуиция — включать по точному совпадению: hour == 7 && minute == 0 && second == 0. Часы DS3231 точные, секунда есть секунда. Ломается это на loop: круг занимает несколько миллисекунд, но между проверками лежат delay, чтение датчиков, печать в Serial. Поставьте в цикл опрос влажности на 800 миллисекунд — и конкретная секунда «00» пролетает мимо в половине случаев: в момент проверки время уже 7:00:01. Насос молчит, вы узнаёте к обеду по свисающим листьям. Сравнение на равенство — лотерея, где выигрыш зависит от того, насколько толстый у вас loop.
Лечится окном: условие hour == 7 && minute < 10 истинно целых десять минут, и любой цикл с периодом короче поймает его сотни раз. Дальше вопрос не «поймать», а «не поймать дважды»: пока условие истинно, оно истинно для каждого прохода — при периоде полсекунды это 1200 повторов за сеанс. Для насоса с обычным реле повторный digitalWrite(HIGH) безвреден, уровень и так высокий. А вот импульсный шаровый кран с электроприводом получает сотню команд «открыть» подряд — в проекте дождевателя у Amperka кран вешают на нормально-замкнутые контакты реле именно потому, что крану нельзя дёргаться каждые полсекунды. Отсюда правило: действие совершаем один раз, на фронте условия, и держим о нём флаг. Период опроса при этом всё равно держите на millis, как в уроке про millis вместо delay, — delay внутри loop съедает и время старта, и реакцию.
Крайний случай окна — пересечение полночи: свет должен гореть с 23:45 до 00:15, и условие hour == 23 && minute >= 45 уже не склеивается в одну строку. Рабочий приём — перевести обе границы в минуты суток и сравнивать числа: старт равен 23 умножить на 60 плюс 45, окно истинно, пока от старта прошло меньше длины. Через ноль помогает развёрнутое условие из двух половин — знакомый ход по скетчу света по расписанию, где окно переворачивалось одной строкой. Поливу в 7:00 эта экзотика не нужна, но свет в зимней теплице как раз просится до полуночи — запишите приём заранее, а не в четыре утра с фонариком.
#include <Wire.h>#include <RTClib.h>#include <EEPROM.h> RTC_DS3231 rtc; const int RELAY = 7; // модуль реле насосаconst byte CHAS = 7; // окно открывается в 07:00const byte DLIT = 10; // и длится 10 минутconst int ADRES = 0; // ячейка: день последнего полива bool polil = false; // быстрый флаг текущих суток void setup() { Serial.begin(9600); digitalWrite(RELAY, LOW); // уровень - раньше pinMode: гасим импульс старта pinMode(RELAY, OUTPUT); if (!rtc.begin()) { Serial.println("DS3231 молчит: проверьте SDA A4 и SCL A5"); while (true) delay(100); } DateTime now = rtc.now(); polil = (EEPROM.read(ADRES) == now.day()); Serial.print("Сегодня уже поливали: "); Serial.println(polil ? "да" : "нет");} void loop() { DateTime now = rtc.now(); if (polil && now.day() != EEPROM.read(ADRES)) { polil = false; // сутки сменились - разрешаем новый полив } bool vOkne = (now.hour() == CHAS && now.minute() < DLIT); if (vOkne && !polil) { // фронт: внутри окна ещё не поливали polil = true; EEPROM.update(ADRES, now.day()); // update пишет только при изменении digitalWrite(RELAY, HIGH); Serial.print("Насос включён в "); Serial.print(now.hour()); Serial.print(":"); if (now.minute() < 10) Serial.print("0"); Serial.println(now.minute()); } if (!vOkne) { digitalWrite(RELAY, LOW); } delay(500);}Разберите порядок в setup: digitalWrite(RELAY, LOW) вызван до pinMode — это не перестраховка, а защита от мусора на старте. Пин до настройки висит в воздухе, и оптрон релейного модуля успевает услышать плавающий уровень: насос дёргается на полсекунды при каждой прошивке. Уровень задаём раньше направления.
Флаг здесь двухэтажный. polil в RAM — быстрый, живёт до перезагрузки. lastDay в EEPROM — медленный, но переживает всё: моргание света, перешивку, перенос на другую макетку. В setup читаем ячейку и сравниваем с текущим днём — так после сбоя скетч знает, что сеанс уже был. В цикле флаг сбрасывается только при смене суток, поэтому внутри одного дня второй запуск невозможен физически: условие vOkne && !polil сработает единожды.
Пара слов о ресурсе, чтобы спать спокойно: при одной записи в сутки сто тысяч циклов — это двести лет службы, поливом EEPROM вы не убьёте. Хуже другое: у клонов Nano ячейка бывает потрёпана чужими экспериментами, и байт читается как 255. Это безопасно — 255 не совпадёт ни с каким числом месяца — но пусть будет сигналом присмотреться к плате. Если EEPROM уже занята другим проектом, отведите флагу дня свободный байт и запишите карту памяти одной строкой в шапке скетча: через полгода вы иначе наступите на собственные же значения и будете ловить призрачный повторный полив.
Где хранить «уже полил»
Место хранения определяет характер отказа. Переменная в RAM — мгновенно и бесплатно, но любая перезагрузка её стирает, что и случилось в истории с двойным поливом. EEPROM переживает всё, только писать туда надо по уму: ресурс ячейки у ATmega328P — порядка 100 000 записей, поэтому байт дня пишется раз в сутки, в момент старта полива, и функция update — не перестраховка: write долбит ячейку каждым вызовом, update трогает её лишь при изменении значения. Одна запись в сутки при таком ресурсе — это двести с лишним лет. А вот писать в EEPROM каждый проход loop — выжжете ячейку за неделю: частота важнее выбора ячейки.
У компромисса есть оборотная сторона, и я про неё говорю прямо: если питание моргнёт в середине сеанса, после перезагрузки скетч увидит «сегодня уже поливали» и не запустит второй заход. Грядки получат четыре минуты воды вместо десяти — но не двадцать. Я осознанно жертвую частью сеанса ради спокойствия: недополив лечится завтрашним окном, перелив — корнями, которые сгнили за ночь. Практика форумов говорит, что тема не праздная: вопрос «включить реле дважды в сутки по времени» на arduino.ru живёт с 2015 года, и в каждой ветке повторяется одно и то же — равенство по секундам и флаг, умерший при первом моргании. Часы тут ни при чём: DS3231 держит время на батарейке годами, и англоязычные коллеги ставят RTC именно ради перебоев питания — память о времени у железа есть, а память о сеансе остаётся заботой скетча. Кстати, флаг дня — тот же приём, на котором четырёхканальное реле времени от iarduino хранит расписания в EEPROM: один байт на один факт.
| Где живёт флаг | Переживает сбой питания | Цена и риск |
|---|---|---|
| Переменная в RAM | нет: перезагрузка — второй заход в окне | ноль усилий; годится при стабильно горящем питании |
| Байт дня в EEPROM, update раз в сутки | да: после сбоя скетч видит, что сегодня уже поливали | одна ячейка, ресурс около 100 000 записей — хватит на века |
| Флаг в EEPROM каждый проход loop | да | ресурс ячейки выжигается за дни — так не делаем |
| Никакого флага: реле по состоянию окна | частично: сбой внутри окна даёт повторное включение | терпимо для насоса, беда для импульсного крана |
Последний случай: питание пропало в 6:40 и вернулось в 7:20. Окно прошло, флаг ни при чём — полива не было вовсе. Простейший догоняющий вариант — хранить в EEPROM не день, а unixtime последнего полива: при старте, если прошло больше суток, полить сразу, вне расписания. Работает, но у ловушки есть цена: после недельного отпуска плата зальёт грядки в два часа ночи, когда рядом никого нет. Для дачи я оставил жёсткое расписание и наливной бак с поплавком: недополив за день рассада переживает, перелив в чужое время — уже не она. Дни недели и выходные поверх этого каркаса — в уроке про будни и выходные, а если реле в расписании несколько, держите цикл без delay — как в уроке про часы без delay, иначе окна начнут наезжать друг на друга.
- 1. Почему условие hour == 7 && minute == 0 && second == 0 — лотерея?
- 2. Окно 07:00–07:10, флага нет. Что происходит с насосом и реле?
- 3. Зачем EEPROM.update вместо EEPROM.write?
- 4. Свет моргнул в 7:05, Arduino перезагрузился. Что делает скетч урока?
- 5. Светодиод релейного модуля загорелся, когда скетч написал LOW. Что это значит?
- Равенство секунд — лотерея: loop перескакивает конкретную секунду, окно hour == 7 && minute < 10 ловится сотней проходов
- Действие по расписанию — на фронте условия и один раз: флаг polil против повторных команд
- День полива живёт в EEPROM одним байтом и переживает моргание света; update, а не write
- После сбоя в середине окна второй заход не запускается: недополив дешевле перелива
- Уровни реле проверяйте по светодиоду модуля: у оптронных плат включение часто по LOW
Частые вопросы по теме урока
- Почему условие hour == 7 && minute == 0 && second == 0 — лотерея?
- Круг loop занимает миллисекунды, но внутри лежат delay и чтение датчиков: при опросе на 800 мс половина секунд выпадает. Окно из десяти минут ловится сотней проходов гарантированно. loop с задержками перескакивает конкретную секунду — условие может не наступить ни разу
- Окно 07:00–07:10, флага нет. Что происходит с насосом и реле?
- Условие истинно все десять минут: повторные digitalWrite безвредны для обычного реле, но кран с электроприводом дёргается на каждом проходе loop. Поэтому действие — на фронте и один раз. Реле держит включённым весь сеанс: для насоса терпимо, импульсный кран получает сотню команд
- Зачем EEPROM.update вместо EEPROM.write?
- write перезаписывает ячейку каждым вызовом, update — только когда значение реально изменилось. При записи раз в сутки разница невидима, а при честолюбивом «писать почаще» она спасает ячейку. update пишет только при изменении значения и бережёт ресурс ячейки
- Свет моргнул в 7:05, Arduino перезагрузился. Что делает скетч урока?
- В setup флаг восстанавливается из EEPROM сравнением с текущим днём. Это осознанный компромисс: часть сеанса теряется, зато грядки не получают двойную дозу за одну ночь. Ждёт конца окна и не включает второй заход: день совпал с записью в EEPROM
- Светодиод релейного модуля загорелся, когда скетч написал LOW. Что это значит?
- Оптронные модули чаще всего активные-LOW. Логика работы правильная, но уровни противоположны скетчу урока: HIGH — выключено, LOW — включено. Проверяйте по светодиоду модуля без нагрузки. Модуль активный-LOW: контакт замыкается при LOW, уровни в скетче надо перевернуть