Мой первый выводок сходил с ума по одной причине: лоток надо крутить, и не один раз. Режим, по которому я работаю с куриными яйцами, выглядит так: с первого по одиннадцатый день держим 37,9 °C и крутим лоток каждые три часа, с двенадцатого температуру опускаем до 37,3 °C, а за три дня до вывода повороты кончаются — зародыш готовится проклюнуться сам. Пропущенный поворот стоит дороже, чем кажется: зародыш прилипает к скорлупе, и два пропущенных за выводок съедают половину выводка. Будильник на телефоне решал задачу наполовину: я просыпался, шёл в тёмную кухню крутить лоток и в четыре утра проклинал всё, что не придумали мотор.
Автоматика этой главы распадается на две независимые половины, и это осознанное решение. Поворот лотка — задача расписания: таймер на millis, серво, три секунды работы раз в три часа. Температура — задача контроля: DS3231 умеет отдавать температуру собственного кристалла методом getTemperature(), и хоть это не термометр воздуха, вторая, независимая линия контроля стоит своего места в приборе. Зуммер на пине D8 объявляет выход за коридор, а журнал в Serial помнит каждый поворот и каждую тревогу с меткой времени.
Честность сразу: кристалл DS3231 мерит температуру собственного корпуса с допуском ±3 °C и сам себя подогревает, поэтому основным термостатом инкубатора он быть не может — там DHT22 или DS18B20, как в уроке про инкубаторный термостат. Но как вторая линия — «а жив ли вообще контроль температуры» — кристалл работает отлично: если в ящике, где термостат держит 38, чип показывает пятнадцать, случился не дрейф датчика, а смерть термостата или обрыв его питания. Разбор самого метода с замерами я делал в уроке про термометр DS3231 — там же про нагрев кристалла после включения.
Поворот лотка: таймер, состояния, серво
Интервал в три часа — это 10 800 000 миллисекунд, и тут обязателен unsigned long: int умирает на 32 767, а беззнаковая арифметика сравнения millis() - lastPovorot >= INTERVAL спокойно переживает переполнение счётчика на 49,7 суток. Инкубатор с его тремя неделями непрерывной работы до потолка не доберётся, но привычка писать интервалы беззнаково спасает полугодовые приборы, и переплачивать за неё нечем. Отсчёт я привязываю к millis, а не к меткам RTC: таймер не умеет пропускать поворот при перескоке времени и не зависит от того, что кто-то поправил часы в чипе посреди выводка. У DS3231 есть и настоящие будильники, и первое желание — крутить лоток по будильнику чипа; я отказался: будильники требуют проверки регистров или провода SQW, выигрыша против millis-таймера при питании от сети нет, а код делается хрупче — после перезагрузки флаг будильника читается заново, и поворот уезжает.
Сам поворот я делаю туда-обратно: чётный поворот уводит лоток в девяносто градусов, нечётный возвращает в ноль. Причина приземлённая — привод через рычаг, и одностороннее вращение за три недели накрутило бы провод, а вместе с ним поставило бы лоток на упор. Ход разбит на шаги по пять градусов с паузой двадцать миллисекунд: SG90 на полном ходу дёргает рычаг так, что звенит корпус ящика, а пошагово движение выходит тихим и плавным — та же механика, что в плавном ходе серво из раздела про серво, только с расписанием вместо ручки.
Питание серво — отдельная строка бюджета, и у меня она с историей: в покое SG90 ест около десяти миллиампер, под нагрузкой рвётся к 100-250 мА, а старт с грузом лотка подскакивает выше. На пине 5 В платы это выглядит как просадка в момент каждого третьего поворота — контроллер перезагружается посреди выводка и забывает счётчик. Правило то же, что с насосом в теплице: серво от своего блока, земля общая, конденсатор у разъёма — вся механика с числами собрана в уроке про внешнее питание серво.
#include <Wire.h>#include <RTClib.h>#include <Servo.h> RTC_DS3231 rtc;Servo lotok;const int ServoPin = 9;const int Zummer = 8; const unsigned long INTERVAL = 10800000UL; // 3 часаconst unsigned long TAJMAUT = 15000UL; // ожидание возврата температурыfloat niz = 37.2, verh = 38.2; // коридор: широкий, кристалл ±3 C int schet = 0; // счётчик поворотов: чёт -> 90, нечёт -> 0unsigned long lastPovorot = 0;int ugol = 0, cel = 0; // текущий и целевой уголbool trevoga = false;unsigned long lastZvuk = 0, lastZhurnal = 0; void setup() { Serial.begin(9600); pinMode(Zummer, OUTPUT); digitalWrite(Zummer, LOW); lotok.attach(ServoPin); lotok.write(0); if (rtc.begin() == false) { Serial.println("DS3231 ne najden: SDA A4, SCL A5"); while (1) {} } Serial.println("Inkubator: povorot kazhdye 3 ch, koridor 37.2-38.2 C (kristall)");} void loop() { DateTime now = rtc.now(); float t = rtc.getTemperature(); // кристалл чипа, а не воздух ящика // --- тревога: вход мгновенный, выход с гистерезисом 0,3 C if (trevoga == false && (t < niz || t > verh)) { trevoga = true; Serial.print("TREVOGA: t="); Serial.print(t, 1); Serial.println(" C"); } if (trevoga && t > niz + 0.3 && t < verh - 0.3) trevoga = false; if (trevoga && millis() - lastZvuk >= 2000UL) { lastZvuk = millis(); digitalWrite(Zummer, HIGH); delay(150); // импульс 150 мс слышно из другой комнаты digitalWrite(Zummer, LOW); } // --- поворот по таймеру: чётные в 90, нечётные в 0 if (millis() - lastPovorot >= INTERVAL) { lastPovorot = millis(); if (schet % 2 == 0) cel = 90; else cel = 0; schet++; Serial.print("Povorot #"); Serial.print(schet); Serial.print(" na "); Serial.print(cel); Serial.println(" grad"); } if (ugol != cel) { // шаги по 5 градусов без резких рывков int delta = 5; if (cel < ugol) delta = -5; ugol += delta; if (delta > 0 && ugol > cel) ugol = cel; if (delta < 0 && ugol < cel) ugol = cel; lotok.write(ugol); delay(20); } // --- журнал раз в минуту: по нему видно ритм и историю тревог if (millis() - lastZhurnal >= 60000UL) { lastZhurnal = millis(); Serial.print("Zhurnal "); Serial.print(now.hour()); Serial.print(":"); Serial.print(now.minute()); Serial.print(" t="); Serial.print(t, 1); Serial.print(" povorotov="); Serial.print(schet); Serial.print(" ugol="); Serial.println(ugol); }}Прогон: на старте чип симулятора холодный — 23,5 °C против яичного коридора, и тревога включается честно, зуммер стрекочет импульсами по 150 миллисекунд каждые две секунды, журнал печатает строку ТРЕВОГА с числами. Именно так на плате выглядит смерть термостата, и этот звук я слышал дважды — оба раза он спас кладку. Командой C23 24 сузьте коридор к показаниям чипа: тревога погаснет, журнал начнёт жить в штатном ритме, а команда V назначит поворот немедленно — рог серво уйдёт в девяносто шагами по пять градусов, счётчик в журнале подрастёт, обратный ход случится на следующем интервале. I20 ужмёт интервал до двадцати секунд и покажет оба направления подряд.
На плате числа возвращаются к яичным: интервал 10 800 000 мс, коридор 37,2-38,2 с запасом на кристалл. Первый прогон я делаю с ужатым INTERVAL — десять секунд вместо трёх часов, и весь цикл поворотов туда-обратно виден за минуту, вместе с журналом и тревогой, если нарочно накрыть модуль ладонью и погреть кристалл. Заодно проверяется питание: если серво при ручном повороте роняет монитор — блок ещё не готов, и закладывать яйца в прибор без пройденного теста я не берусь. Родственный сюжет — серво, которое живёт по меткам времени RTC, собран в уроке про кормушку на серво: там по часам выезжает порция корма, и после него разница между «каждые три часа» и «в 09:00 по расписанию» видна как на ладони.
| Дни выводка | Температура | Поворот лотка |
|---|---|---|
| 1-11 | 37,9 °C | каждые 3 часа, туда-обратно |
| 12-17 | 37,3 °C | каждые 3 часа, плюс проветривание 2 раза в сутки по 5 минут |
| 18-21, вывод | 37,2 °C, влажность выше | не крутить: зародыш готовится к проклёву |
Тревога, журнал и кто в доме главный
Гистерезис тревоги — обязательная деталь, а не украшение: показания, гуляющие ровно по границе коридора, без него превращают зуммер в стрекот на весь вечер. Схема простая: вошли за границу — тревога включилась, выход только когда показания вернулись внутрь на три десятых градуса, и зуммер замолкает. Звук я делаю импульсным: 150 миллисекунд каждые две секунды слышно из другой комнаты, но не бьёт по голове в той же. Флаг тревоги с возвратом и повторным срабатыванием — механика из урока про будильник с зуммером, там же про то, как гасить повторные звонки.
Журнал раз в минуту — дешёвая страховка, которая окупилась дважды: строка со временем, температурой и счётчиком поворотов показывает и ритм, и историю. По журналу я увидел, что один выводок крутился честно, а во втором пауза в поворотах совпала с провалом температуры — это оказался отошедший провод термостата, и журнал назвал время. Механику журнала с метками и форматами я раскладывал в уроке про журнал в Serial. Одна ловушка живёт в самом счётчике: он лежит в оперативке, и перезагрузка обнуляет его вместе с чётностью — скетч после сброса считает поворот с нуля и может «повернуть» лоток, который уже стоит на девяноста. Лечение — ячейка EEPROM со счётчиком: шесть поворотов в сутки против ста тысяч циклов записи, бюджет я считал в уроке про износ EEPROM.
Иерархия в приборе простая и проговорённая: основной термостат держит температуру, скетч урока следит за ритмом поворотов и подстраховывает по температуре кристалла. Кристалл заметит падение на десятки градусов, но не полградуса — поэтому коридор тревоги на нём я расширил с 37,8 до 38,2: ловить тут нужно катастрофы, а не дрожание десятых, и за это решение я готов отвечать. Попытка сделать из DS3231 единственный термостат инкубатора — путь к испорченной кладке: допуск ±3 °C шире всей яичной вилки, и это не лечится никакой калибровкой скетчем.
Дни выводка в скетче считаются одной строкой, если в день закладки записать якорь: разность unixtime текущего и якорного, делённая на 86 400 секунд, даёт номер дня, и таблица режимов превращается в массив условий. Якорь я держу в EEPROM рядом со счётчиком поворотов: закладка яиц, кнопка, запись текущего unixtime — с этой секунды прибор знает возраст кладки и сам останавливает повороты на восемнадцатый день. Сутки — 86 400 секунд, и число это живёт только в unsigned long: в int оно не влезает, как и вообще всё, что связано с unixtime. Проверка занимает минуту: печатаете разность для якоря трёхдневной давности, результат обязан выйти ровно в трёх сутках — если вышло мусорное число, где-то остался int.
- 1. Сколько миллисекунд проходит между поворотами при интервале три часа?
- 2. Что показывает rtc.getTemperature()?
- 3. Зачем повороты чередуются: чётный в 90 градусов, нечётный в ноль?
- 4. Почему зуммер стрекочет импульсами, а не ревёт непрерывно?
- 5. Что происходит со счётчиком поворотов после перезагрузки платы?
- Интервал 3 часа = 10 800 000 мс: только unsigned long, сравнение millis() - last >= INTERVAL переживает переполнение
- Поворот туда-обратно (чёт — 90°, нечёт — 0°) шагами по 5°: провод привода не накручивается, SG90 не дёргает питание
- getTemperature() — кристалл ±3 °C: для основного термостата нет, для второй линии «жив ли контроль» — да
- Тревога с гистерезисом 0,3 °C и импульсами 150 мс раз в 2 с: слышно из другой комнаты, но не изматывает
- Журнал раз в минуту — время, температура, счётчик; счётчик после перезагрузки спасает ячейка EEPROM
Частые вопросы по теме урока
- Сколько миллисекунд проходит между поворотами при интервале три часа?
- Три часа — это 10 800 000 мс, и константа обязана жить в unsigned long: int кончается на 32 767. Сравнение с millis() в беззнаковой арифметике переживает переполнение счётчика. 10 800 000
- Что показывает rtc.getTemperature()?
- Датчик стоит внутри для компенсации кварца и мерит корпус чипа, который сам себя греет. Воздух ящика он не показывает, и термостатом инкубатора быть не может. Температуру кристалла чипа с допуском ±3 °C
- Зачем повороты чередуются: чётный в 90 градусов, нечётный в ноль?
- Привод через рычаг: одностороннее вращение за выводок накручивает провод и ставит лоток на упор. Туда-обратно снимает проблему ценой одной переменной-счётчика. Чтобы не накрутить провод привода за три недели выводка
- Почему зуммер стрекочет импульсами, а не ревёт непрерывно?
- 150 мс каждые 2 секунды — звук, который заметен, но не бьёт по голове. Непрерывный вой в кухне заглушает всё, а по импульсам ещё и понятно, что плата не зависла. Импульс слышен из другой комнаты, но не изматывает, и по паузам видно, что скетч жив
- Что происходит со счётчиком поворотов после перезагрузки платы?
- Счётчик живёт в оперативке: после сброса скетч стартует с нуля и может повернуть лоток, который уже стоит на девяноста. Лечение — ячейка EEPROM со счётчиком. Счётчик обнулится, и чётность направления собьётся