Инкубатор на тридцать яиц я собрал за выходные и месяц доводил до ума. Скетч был классический по ранним урокам: loop() читал датчик, сравнивал с целью, щёлкал реле и в конце делал delay(2000) — DHT22 нельзя опрашивать чаще, чем раз в два секунды. Кнопка смены цели висела на обычном пине и опрашивалась тем же loop(). Чтобы поднять цель с 22 до 24 градусов, надо было попасть нажатием в тот глазок, когда loop() добирался до digitalRead, — а глазок был открыт 150 миллисекунд из двух тысяч. Из десяти нажатий до инкубатора долетало меньше одного.
Реле показало себя вторым. Датчик на границе цели выдавал то 23,8, то 24,2 — и реле честно отрабатывало каждый шорох: четыре-пять щелчков в минуту. У типового реле модуля вроде SRD-05VDC ресурс коммутаций — порядка ста тысяч; при таком темпе контакты кончаются за две-три недели, это не теория, я так похоронил первый модуль. Третьим симптомом приехала батарейка: когда инкубатор переехал в гараж без розетки, семь ампер-часов хватало на шесть с половиной суток — 45 миллиампер постоянного жора Uno съедали их, хотя сам нагрев питался отдельным блоком.
Итоговый проект раздела собирает ответы на все три беды в один скетч: неблокирующий millis-цикл — шаблон if (millis() - prev >= N) из первого урока раздела — вместо delay, кнопка цели на прерывании, гистерезис у реле, watchdog как страховка от зависаний и сон для батарейки. В симуляторе ниже термостат работает целиком; watchdog и сон движок не эмулирует — их смотрим на честном коде для настоящей платы.
Архитектура: четыре навыка в одном loop()
Скелет — планировщик задач на millis() из урока про планировщик: три независимые задачи, у каждой своя пара «метка плюс период». Опрос датчика и решение по реле — раз в 500 мс (DHT22 отвечает не чаще раза в два секунды, запас четырёхкратный), печать состояния — раз в 1000 мс, события кнопки — по флагу из прерывания, без расписания. Ни одного delay() в loop(): правила кооперативного цикла из соответствующего урока здесь не пожелание, а условие жизни — заблокируетесь, и обе оставшиеся задачи перестанут успевать.
Кнопка цели живёт на пине 2 — это INT0. Опрос в loop() работал бы, пока скетч бодрствует, но в итоговой версии loop() уходит в сон на восемь секунд, и разбудить его может только внешнее прерывание. В обработчике три строки: цель плюс единица с переносом по диапазону 18-28 и флаг для loop(); печать, зуммер и решения — всё в loop(), по флагу. Правила те же, что в уроке про attachInterrupt: volatile, краткость, никакого Serial.
Реле решает автомат с гистерезисом — мёртвой зоной вокруг цели. Включили нагрев, когда температура упала на градус ниже цели; выключили, когда доросла до цели; внутри полосы реле не трогаем, что бы ни показывал датчик. Именно полоса в один градус превращает дрожащую границу в редкие осмысленные щелчки. Логику я подробно разбирал в уроке про термостат на DHT — здесь она встроена в общий millis-цикл и обязана пережить сон, зависание и смену цели на ходу.
| Задача | Инструмент из раздела | Число с условием |
|---|---|---|
| Опрос датчика и решение по реле | millis-гейт из планировщика задач | раз в 500 мс; DHT22 отвечает не чаще раза в 2 с — запас четырёхкратный |
| Кнопка цели | прерывание INT0, FALLING, INPUT_PULLUP | ISR в три строки: volatile-переменная и флаг, всё остальное в loop() |
| Реле нагрева | гистерезис — мёртвая зона 1 °C | SRD-реле живёт около 100 тыс. коммутаций: без полосы дрожащий датчик щёлкает 4-5 раз в минуту |
| Самоспасение | watchdog, WDTO_2S в активной фазе | wdt_reset() обязан вызываться раньше лимита — иначе перезагрузка чипа |
| Батарейка | power-down + будильник от watchdog 8 с | чип во сне 5-7 мкА против 45 мА активной Uno; средний ток узла 40-50 мкА |
Термостат целиком: симулятор
Движок симулятора watchdog и сон не эмулирует, зато весь остальной термостат здесь живой. Датчик изображает pot на A0: ползунок — температура от 15 до 35 градусов. Почему не DHT? Макетка DHT садится на пин D2, а этот пин отдан кнопке INT0; конфликт пинов решён заменой датчика, а не отказом от кнопки — на настоящей плате вернёте DHT22, тайминг опроса не изменится.
Шум random(-1, 2) изображает дрожь реального сенсора в плюс-минус градус: на идеально ровном сигнале вы бы не увидели, как гистерезис держит реле в мёртвой зоне. Светодиод D12 зеркалит реле D7, зуммер D8 подтверждает смену цели, печать раз в секунду идёт парами имя=значение — эти же ряды построит плоттер из урока про Serial. Крутите ползунок через цель и считайте щелчки: включение — на градус раньше цели, выключение — ровно на цели.
// Сборка: датчик на A0, реле на D7, кнопка цели на D2 (INT0).#include <avr/wdt.h>#include <avr/sleep.h> const int PIN_BTN = 2;const int PIN_RELAY = 7;volatile byte budilnik = 0; // флаг будильника от watchdogvolatile byte press = 0; // флаг нажатия кнопкиvolatile int ustavka = 22;int podyomov = 0; ISR(WDT_vect) { // прозвенел будильник: 8 секунд прошли budilnik = 1;} void naPress() { // кнопка будит чип: из power-down ловится LOW press = 1;} void zavodBudilnika() { // watchdog в режим прерывания на 8 с MCUSR &= ~(1 << WDRF); // снять флаг сброса, иначе вечный рестарт WDTCSR |= (1 << WDCE) | (1 << WDE); WDTCSR = (1 << WDIE) | (1 << WDP3) | (1 << WDP0); // прерывание, 8 с} void usnut() { set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_enable(); sleep_mode(); // замираем тут до будильника или кнопки sleep_disable();} void setup() { pinMode(PIN_BTN, INPUT_PULLUP); pinMode(PIN_RELAY, OUTPUT); attachInterrupt(0, naPress, LOW); Serial.begin(9600); zavodBudilnika(); Serial.println("Start: power-down, budilnik 8s"); Serial.flush();} void loop() { if (press == 1) { // смена цели: та же логика, что в симуляторе press = 0; ustavka = ustavka + 1; if (ustavka > 28) { ustavka = 18; } } // страховка активной фазы: две секунды на измерение и реле wdt_enable(WDTO_2S); int temp = map(analogRead(A0), 0, 1023, 15, 35); if (temp <= ustavka - 1) { digitalWrite(PIN_RELAY, HIGH); // на градус ниже цели — греть } if (temp >= ustavka) { digitalWrite(PIN_RELAY, LOW); // доросли до цели — хватит } Serial.print("temp="); Serial.print(temp); Serial.print(" ustavka="); Serial.print(ustavka); Serial.print(" pod="); Serial.println(podyomov); Serial.flush(); // дописать буфер до конца, потом спать wdt_disable(); // дальше спим под будильником, не под сбросом budilnik = 0; podyomov = podyomov + 1; usnut();}Watchdog в итоговом проекте играет две роли, и перепутать их легко. В активной фазе — страховка: wdt_enable(WDTO_2S), и каждый круг обязан дойти до wdt_reset() за две секунды; зависло чтение датчика — чип перезапустится сам, без вашей поездки в гараж. Во сне — будильник: watchdog переводится в режим прерывания через регистр WDTCSR, дзынькает раз в восемь секунд, и обработчик ISR(WDT_vect) будит чип. А вот наивный wdt_enable(WDTO_8S) на весь скетч со сном не годится: в режиме сброса досчитавший watchdog не будит, а перезагружает — каждые восемь секунд, вечно. Это уже не будильник, а молоток.
Сон — power-down, самый глубокий режим: set_sleep_mode(SLEEP_MODE_PWR_DOWN) и sleep_mode(), после чего чип замирает до внешнего события. Из power-down ATmega328P будят ровно два источника: сам watchdog и низкий уровень на INT0 или INT1 — поэтому в скетче со сном кнопка ловится через attachInterrupt(0, naPress, LOW), хотя в бодрствовании логичнее FALLING. Об это я споткнулся первым делом: скетч с FALLING засыпал навсегда, и сутки я подозревал датчик, а не регистры. Перед сном — Serial.flush(): при 9600 бод строка из шестнадцати символов выходит примерно за семнадцать миллисекунд, и сон посреди печати режет её на полуслове. Выходы пинов уровень во сне держат: реле, включённое перед засыпанием, греет все восемь секунд — для термостата это правильно.
Теперь арифметика узла. Активная фаза — чтение датчика, решение по реле, строка печати — занимает 15-20 мс при примерно 15 мА. Остальное — сон, где чип в power-down с включённым watchdog берёт 5-7 мкА. Средний ток выходит 40-50 мкА — в тысячу раз меньше активной платы с обвязкой. Честная оговорка к красивому числу: катушка механического реле тянет 70-90 мА, пока нагрев включён, и сон её не отключает. Поэтому в батарейном термостате нагрузку я коммутирую полевым транзистором или твердотельным реле, а не модулем с катушкой — тогда батарейка АА на 2500 мА·ч живёт месяцами. Полную историю энергопотребления я считал в уроке про watchdog и режимы сна.
Что докрутить самому
Докрутите планировщик до полного. Добавьте четвёртую задачу — раз в десять секунд печатать uptime и число сторожевых рестартов; пятую — мигать светодиодом, если нагрев не выключался дольше пятнадцати минут подряд: так себя выдаёт залипшее реле. Цель сохраняйте в EEPROM и восстанавливайте после сторожевого перезапуска — иначе каждый сброс возвращает уставку к 22 градусам, а вашему инкубатору нужны свои 37,8.
И пересчитайте батарейный бюджет под свои условия: свой период сна, свою длительность активной фазы, свой ток нагрузки — подставьте в формулу среднего тока из разбора. Вышло больше обещанного — ищите жреца по очереди: катушка реле, светодиоды, незакрытый Serial. Находить жреца в собственной схеме — главный навык раздела, и после этого проекта он у вас в пальцах.
- 1. Почему в симуляторе датчик температуры — pot на A0, а не DHT?
- 2. Датчик показывает то 23,8, то 24,2 при цели 24, и реле щёлкает каждые десять секунд. Что забыли?
- 3. Какое внешнее прерывание будит ATmega328P из power-down?
- 4. wdt_enable(WDTO_8S) включён, но одна ветка loop() иногда тянется десять секунд. Что произойдёт?
- 5. Где в итоговом скетче место для Serial.print?
- Итоговая архитектура: millis-планировщик (датчик 500 мс, печать 1000 мс), кнопка на INT0, реле с гистерезисом 1 °C — в loop() нет ни одной блокировки
- ISR кнопки — три строки и volatile-флаг; печать, зуммер и решения по реле живут в loop()
- Гистерезис — мёртвая зона в 1 °C вокруг цели: без неё реле щёлкает 4-5 раз в минуту, а ресурс SRD-реле — около 100 тыс. коммутаций
- Watchdog на две роли: страховка WDTO_2S с wdt_reset() в активной фазе и будильник WDIE раз в 8 с во сне; сброс + power-down = вечные перезагрузки
- Из power-down будят только watchdog и LOW на INT0/INT1; перед сном Serial.flush(); средний ток узла 40-50 мкА — если нагрузка без катушки реле
Частые вопросы по теме урока
- Почему в симуляторе датчик температуры — pot на A0, а не DHT?
- Макетка DHT занимает пин D2, а этот пин отдан кнопке прерывания INT0. Конфликт пинов решён заменой датчика на pot, а не отказом от кнопки: на плате вернёте DHT22 с тем же таймингом. у DHT и кнопки общий пин D2, а кнопка обязана сидеть на INT0
- Датчик показывает то 23,8, то 24,2 при цели 24, и реле щёлкает каждые десять секунд. Что забыли?
- Дрожащий датчик на границе цели заставляет реле срабатывать на каждый шорох. Мёртвая зона в градус держит реле внутри полосы: включение ниже цели, выключение на цели, между ними — тишина. гистерезис: нужна мёртвая зона в один градус вокруг цели
- Какое внешнее прерывание будит ATmega328P из power-down?
- Из самого глубокого сна даташит разрешает будить только LOW-уровнем на INT0/INT1. FALLING и CHANGE работают в бодрствовании и idle, но спящий чип по фронту не просыпается. только низкий уровень LOW на INT0 или INT1
- wdt_enable(WDTO_8S) включён, но одна ветка loop() иногда тянется десять секунд. Что произойдёт?
- Сторож сбрасывается только вызовом wdt_reset(). Круг длиннее лимита — перезагрузка. Долгие ветки делят на шаги и гладят собаку между ними, либо ставят лимит с запасом. чип перезагрузится: wdt_reset() не дошёл за восемь секунд
- Где в итоговом скетче место для Serial.print?
- Печать — медленная работа для loop(). ISR оставляет себе volatile-переменные и флаг, а строку BTN ustavka= выводит loop, когда заметит флаг: на плате Serial в обработчике вешает скетч. в loop(): из ISR только флаг, печать в обработчике вешает плату