Аквариум стоял у меня в спальне, и свет над ним щёлкал реле ровно в 07:00 — я слышал этот щелчок сквозь сон, а рыбы получали ежедневный испуг: полумрак превращался в полную яркость за одну секунду. Месяца через два я заметил и вторую беду: водоросли облепили стекло именно на том боку, где лампа била в полную мощность без перехода, а светобоязливые харацинки первую минуту после щелчка жались у дна. Ветка на аквариумном форуме дала диагноз, который я перепроверил на себе за неделю: резкое включение — это ежедневный стресс для рыбы и привилегия для водорослей, и лечится оно рампой — свет должен наливаться плавно, минут за тридцать-сорок, как настоящий рассвет. Крутить диммер руками каждое утро я передумал на второй день, и в дело пошли часы.
Инструменты задачи очерчиваются сразу: DS3231 говорит, который час, а analogWrite плавно ведёт яркость туда, куда велит расписание. Реле здесь не годится в принципе — у него два состояния, вкл и выкл, и щелчок никуда не денется; нужен транзисторный ключ с ШИМ: MOSFET логического уровня и LED-лента на 12 В над крышкой. Базу про яркость через analogWrite я разбирал в уроке про ШИМ и яркость светодиода, а включение света строго по часам без всякой плавности — в уроке про свет по расписанию; здесь обе половины соединяются в один скетч.
Что получается в конце: лампа наливается с нуля до полной за сорок минут, горит весь световой день, к девятнадцати нулям тем же темпом сползает в темноту, а кнопка на корпусе даёт ручной обход — посмотреть на рыб в тёмное время, не трогая расписание. Скетч живёт на millis, время берёт из RTC и после перезагрузки сам находит свою яркость за один проход цикла: обнуление счётчика миллисекунд ему не страшно.
Окно светового дня и рампа из map
Вся арифметика живёт в минутах от полуночи: в семь утра окно рассвета открывается и работает сорок минут, с 07:40 до 19:00 день горит на полной, с девятнадцати — сорок минут заката, дальше ночь. У каждого отрезка своя формула: внутри рампы map() переводит минуты в 0-255, днём стоит константа 255, ночью — ноль. Ни одного float: минута — целое число, и яркость за минуту прибавляет шагом в шесть-семь единиц, глазу на тёмной воде это сливается в непрерывный подъём.
Переход я обновляю раз в десять секунд, и это не лень, а расчёт: чаще незачем — за десять секунд рампа прибавляет меньше процента полной яркости, реже — глаз уже ловит лесенку на границе светового дня. Цикл при этом остаётся свободным: millis-стиль из урока про millis вместо delay держит loop() живым, и кнопка обхода срабатывает всегда, а не в паузах между рампами. Одна секунда экономии на delay(10000) здесь стоила бы живой кнопки — сделка глупая, и я её не заключаю.
Рампа — окно, а не мгновение, и это тот же принцип, что в срабатывании по часам: механику окон с флагами я раскладывал в уроке про срабатывание в 18:30. Разница в том, что здесь окно не щёлкает нагрузкой, а рисует градиент: каждое попадание цикла внутрь окна пересчитывает яркость из текущей минуты, поэтому и пропустить момент невозможно, и повторный запуск ничего не ломает. Хочется привязать рампу не к фиксированным 07:00, а к настоящему восходу в вашем городе — арифметика восхода и заката уже посчитана в уроке про восход и закат, окно рассвета просто начинает следовать за солнцем.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;const int Svet = 9; // затвор MOSFET через 100 Ом, лента 12 В в стокеconst int Knopka = 2; // ручной обход: свет на полную до следующей границыconst int RAMPA = 40; // длительность рассвета и заката, минут unsigned long lastStep = 0;bool ruchnoy = false;bool lastKn = false; int yarkost(int tekMin) { // tekMin - минуты от полуночи if (tekMin >= 420 && tekMin < 420 + RAMPA) // 07:00 - 07:40 подъём return map(tekMin - 420, 0, RAMPA, 0, 255); if (tekMin >= 420 + RAMPA && tekMin < 1140) // день на полной return 255; if (tekMin >= 1140 && tekMin < 1140 + RAMPA) // 19:00 - 19:40 спад return map(tekMin - 1140, 0, RAMPA, 255, 0); return 0; // ночь} void setup() { Serial.begin(9600); pinMode(Knopka, INPUT_PULLUP); if (rtc.begin() == false) { Serial.println("DS3231 ne najden: SDA A4, SCL A5"); while (1) {} } Serial.println("Rassvet 07:00 (+40 min), zakat 19:00 (+40 min)");} void loop() { DateTime now = rtc.now(); int tekMin = now.hour() * 60 + now.minute(); if (tekMin == 420 || tekMin == 1140) ruchnoy = false; // граница возвращает авто bool kn = digitalRead(Knopka) == LOW; // кнопка на землю, подтяжка внутри if (kn && !lastKn) ruchnoy = !ruchnoy; // фронт нажатия, без delay lastKn = kn; if (millis() - lastStep >= 10000UL) { // шаг рампы раз в десять секунд lastStep = millis(); int y = 0; if (ruchnoy) y = 255; else y = yarkost(tekMin); analogWrite(Svet, y); Serial.print(now.hour()); Serial.print(":"); Serial.print(now.minute()); Serial.print(" yarkost "); Serial.println(y); }}Прогоните сценарий: R запускает сорокосекундный подъём, светодиод в макетке наливается ровно, без лесенки, и в Serial каждые две секунды ползёт строка с временем и яркостью. Во время рампы нажмите кнопку на макетке — яркость прыгнет на полную, повторное нажатие вернёт авто, и рампа продолжит с того же шага, потому что shag никто не трогал. Команда S покажет закат, A гасит всё до обычного расписания: по живым часам движка скетч днём держит полную, ночью ноль, а в окна 07:00-07:40 и 19:00-19:40 рисует рампу сам, без команд.
Первый запуск на плате я делаю со сжатым временем: RAMPA на минуту, границы на ближайший час — и весь цикл рассвет, день, закат, ночь прокручивается за несколько минут на глазах. Смотрите на два момента: формула выдаёт полную яркость ровно на границе конца рампы и ноль на границе начала заката; если яркость где-то прыгает, а не наливается, дело в пине без ШИМ, о котором ниже. Только после такой проверки возвращаются рабочие сорок минут и границы под жильцов: править константы дешевле до пайки, чем после, а увидеть вживую границу окна стоит потраченных пяти минут.
На плате к скетчу добавляется силовая часть, и в ней две детали, которые я научился ставить с первого раза. Первая — MOSFET с логическим уровнем затвора: IRLZ44N открывается от пяти вольт полностью, а IRF-собратья при 5 В на затворе работают вполнакала, греются и не дают ленте яркости. Вторая — подтяжка затвора на землю резистором десять кило: до первого analogWrite пин платы висит в высокоимпедансном состоянии, и без подтяжки лента после подачи питания светится вполнакала, ловя наводки. Термостат для самого аквариума — отдельная история, собранная в уроке про аквариумный термостат на DS18B20; свет и тепло я держу на разных платах хотя бы из-за перезагрузок: падение подсветки нервы рыбам треплет меньше, чем падение обогрева.
| Исполнитель света | Чем хорош | Чем платит |
|---|---|---|
| MOSFET + лента 12 В | плавная яркость ШИМом, 0,3-1,5 А на метр ленты | нужен ключ с логическим уровнем затвора и подтяжка |
| Реле-модуль | дёшево, изоляция силовой части, готовый модуль | только вкл и выкл: ни рассвета, ни заката, щелчок остаётся |
| Диммируемый драйвер ленты | ток держит стабильно, вход читается с пина напрямую | дороже MOSFET втрое, для метра ленты выглядит избыточно |
Перезагрузка, обход и ловушки ШИМ
Перезагрузка этому скетчу не страшна, и стоит проговорить почему: яркость не хранится, а вычисляется из текущего времени на каждом шаге. millis обнулился, плата перезагрузилась посреди рассвета — на первом же проходе RTC скажет 07:23, формула выдаст свою середину рампы, и свет продолжит с той точки, где его застал сбой. EEPROM пустой, флагов нет, восстанавливать нечего: расписание — это функция времени, а не память. Единственный честный разрыв — секунды между сбросом и первым analogWrite, и именно их закрывает подтяжка затвора из прошлого абзаца: без неё лента эти секунды горит вполнакала.
Длительность рампы — сорок минут в моём скетче, и это не догма, а результат подгонки: в банке на тридцать литров подъём за двадцать минут по ощущениям рыбы почти не отличим от мгновенного, в двухсотлитровом травнике с мощной лентой сорок минут как раз сглаживают переход и не дают водорослям форы. Границы 07:00 и 19:00 выбраны под режим жильцов: рассвет раньше семи будит людей, закат позже девяти греет воду лишним часом света. Константа RAMPA и обе границы правятся тремя числами в одном месте, и за два года жизни скетча мне не понадобилось ничего большего — ни плавных кривых, ни датчиков наружной освещённости.
Ловушка, которая у меня съела неделю, сидит в пинах: на Uno и Nano ШИМ живёт только на 3, 5, 6, 9, 10 и 11, а analogWrite на остальных пинах молча превращается в digitalWrite — яркость прыгает в полную при значении выше половины. Компилятор не предупреждает ни словом, и я вешал рампу на D12, получал вместо рассвета два состояния и месил скетч, пока не полез в справочник по выводам. Симулятор движка прощает ШИМ на любом пине — для логики это удобно, но привычку держать силовой выход на ШИМ-пине она портит, поэтому в макетке LED стоит на D12 с оговоркой, а в скетче для платы — D9.
И про питание ленты, потому что без этого аквариумный свет мигает загадочными способами. Метр ленты на 12 В ест 0,3-0,6 А в зависимости от плотности, блок питания берите с запасом в полтора раза, а землю ленты и платы сводите в одну точку — иначе ШИМ превращается в мерцание, а помехи лезут в аналоговые датчики. Часам всё это безразлично: DS3231 держит время от батарейки и переживает любые качели пятивольтовой части, о чём у меня есть отдельный разговор в уроке про millis и часы — обнуление счётчика и перезагрузка для них незаметны.
- 1. Почему для рассвета не годится реле?
- 2. Что выдаст analogWrite(12, 200) на настоящей Uno?
- 3. Почему рампа переживает перезагрузку платы?
- 4. Сколько длится рампа в симуляторе урока и на плате?
- 5. Зачем резистор 10 кОм между затвором и землёй?
- Рампа — окно в минутах: map() переводит минуты от начала окна в 0-255, шаг обновления 10 с, без float
- Реле для рассвета не годится: два состояния; нужен MOSFET с логическим уровнем (IRLZ44N) и лента 12 В
- ШИМ на Uno живёт только на 3, 5, 6, 9, 10, 11: analogWrite на остальных молча деградирует до digitalWrite
- Яркость вычисляется из RTC на каждом шаге — перезагрузка не рвёт рампу, хранить состояние не нужно
- Кнопка обхода: флаг ручного режима гаснет на следующей границе окна, расписание не трогается
Частые вопросы по теме урока
- Почему для рассвета не годится реле?
- Реле щёлкает раз и горит: для наливания яркости нужен транзисторный ключ с ШИМ — MOSFET или диммируемый драйвер. Щелчок в семь утра при этом остаётся в прошлом. У реле только два состояния: вкл и выкл, плавности не будет
- Что выдаст analogWrite(12, 200) на настоящей Uno?
- ШИМ на Uno живёт на 3, 5, 6, 9, 10, 11. На остальных пинах analogWrite деградирует до digitalWrite без предупреждений компилятора — я на этом потерял неделю. Полную яркость: не-ШИМ пин молча работает как digitalWrite
- Почему рампа переживает перезагрузку платы?
- Расписание — функция времени: на первом проходе после сброса скетч читает RTC и выдаёт ту яркость, которая положена по часам. Хранить нечего, восстанавливать нечего. Яркость вычисляется из времени RTC на каждом шаге, а не хранится
- Сколько длится рампа в симуляторе урока и на плате?
- Часы движка не перематываются, поэтому в симе рампа ужата до сорока секунд командами R и S; на плате константа RAMPA равна сорока минутам. 40 секунд и 40 минут
- Зачем резистор 10 кОм между затвором и землёй?
- До первого analogWrite пин платы высокоимпедансный, затвор ловит наводки и лента светится вполнакала. Подтяжка держит ключ закрытым в любой паузе жизни платы. Чтобы затвор не висел в воздухе до первого analogWrite и после сброса