Обогреватель-ветерок на даче грел комнату волнами: биметаллический термостат внутри щёлкал около 24, отпускал воздух до 19 и снова врубался на полную. К утру в комнате жили две климатические зоны, а я записывал показания настенного градусника от руки, пытаясь понять закономерность. Потом я собрал на Arduino автономный контроллер — и через неделю просто выкинул родной термостат: единственным, кто принимает решения, стало моё реле.
Проект собирает весь раздел в один корпус: DHT22 смотрит воздух, реле включает обогреватель, зуммер кричит о сырости, светодиод издалека показывает режим. Ничего внешнего: без компьютера, без интернета, без экрана — Serial нужен только на этапе настройки, в рабочем режиме он печатает журнал в пустоту, и это нормально. В уроке пройдём от техзадания до готового скетча и стенда в тренажёре, а в конце — куда добавить экран, лог и сеть, когда железо обкатается.
Техзадание: пороги вместо импульсов
Каждое решение контроллера — про состояние, а не про импульс. Цель — 22 °C, рабочая полоса — два градуса: ниже 21,0 реле включает обогреватель, выше 23,0 отпускает, внутри полосы ничего не трогает. Это тот самый гистерезис термостата, и ширина полосы выбрана не с потолка: она вчетверо шире погрешности DHT22, иначе реле щёлкало бы на каждой десятой доле и дожило бы до месяца.
Второй контур — аварии. Влажность 65 % и выше в моей местности — предвестник конденсата на окнах и плесени в углу, поэтому зуммер подключается, но только если сырость держится два опроса подряд: одиночный скачок от чайника сирену не будит. Перегрев 27 °C — сигнал, что обогреватель не слушается: реле глушится принудительно, мимо всех режимов. Светодиод и Serial — телеметрия: режим видно с дивана и в журнале. Всё, что не попало в таблицу ниже, скетч делает сам и молча — это и есть автономность.
| Режим | Условие | Реле | Зуммер | Светодиод |
|---|---|---|---|---|
| hold | 21 < t < 23 и h < 65 % | выключен | молчит | погашен |
| heat | t ≤ 21,0 (держит до 23,0) | включён | молчит | горит ровно |
| alarm: перегрев | t ≥ 27,0 | принудительно выключен | писк раз в цикл | мигает |
| alarm: сырость | h ≥ 65 % два опроса подряд | как было | писк раз в цикл | мигает |
Схема и питание: где проект портят заранее
Распиновка совпадает с макеткой тренажёра: DHT22 на D2, модуль реле на D7, активный зуммер на D8, светодиод с резистором 220 Ом на D12. Реле берите модулем с оптронной развязкой и клеммниками: силовая часть 220 В живёт в клеммах, а не на макетке — шаги подключения разобраны в уроке про реле-модуль и нагрузку 220 В. Светодиод здесь не украшение: пока реле гудит где-то в кладовке, огонёк на стенде показывает, что обогрев работает прямо сейчас.
Бюджет тока скромный: катушка реле берёт около 70 мА, зуммер 20–30, DHT22 — единицы миллиампер. Сумма укладывается в USB-порт, но я всё равно кормил плату от телефонной зарядки — мы уже считали бюджет тока проекта, и правило простое: силовая нагрузка питается отдельно, плате достаётся только логика. Землю платы и реле соединяйте в одной точке: без общей земли модуль щёлкает вприсядку, а в Serial лезут призраки.
// Климат-контроль: DHT22 на D2, реле обогревателя на D7,// зуммер на D8, светодиод режима на D12.DHT dht(2, DHT22); unsigned long lastPoll = 0;unsigned long INTERVAL = 2000; int heater = 0; // состояние реле: 1 — обогрев работаетint mode = 0; // 0 hold, 1 heat, 2 alarmint streak = 0; // опросов подряд с влажностью 65 и вышеint ledState = 0; void setup() { Serial.begin(9600); dht.begin(); pinMode(7, OUTPUT); pinMode(8, OUTPUT); pinMode(12, OUTPUT); lastPoll = millis();} void loop() { if (millis() - lastPoll >= INTERVAL) { lastPoll = millis(); float t = dht.readTemperature(); float h = dht.readHumidity(); if (t >= 27.0) { // перегрев главнее всего mode = 2; heater = 0; } else if (h >= 65.0) { // сырость считается два опроса подряд streak = streak + 1; if (streak >= 2) mode = 2; } else { streak = 0; mode = 0; if (t <= 21.0) heater = 1; // нижний порог гистерезиса if (t >= 23.0) heater = 0; // верхний порог гистерезиса if (heater == 1) mode = 1; } digitalWrite(7, heater); if (mode == 2) ledState = 1 - ledState; // в аварии светодиод мигает else ledState = heater; digitalWrite(12, ledState); if (mode == 2) tone(8, 1000, 200); else noTone(8); Serial.print("mode="); if (mode == 0) Serial.print("hold"); if (mode == 1) Serial.print("heat"); if (mode == 2) Serial.print("alarm"); Serial.print(" t="); Serial.print(t, 1); Serial.print(" h="); Serial.print(h, 0); Serial.print(" relay="); Serial.println(heater); }}Скелет скетча — три переменные состояния. heater хранит положение реле, mode — имя режима для Serial, streak считает опросы с сырой влажностью подряд. Переменных всего три, но каждая отвечает за своё: таким скетчем через полгода будете пользоваться вы сами, не заглядывая в комментарии. Порядок проверок важен: перегрев главнее сырости, сырость главнее комфорта, поэтому аварийная ветка проверяется первой и перекрывает остальные. Внутри спокойной ветки живут два порога гистерезиса — и только они трогают реле: второго digitalWrite на пине D7 во всём скетче нет, а это и есть дисциплина состояния.
Счётчик streak — антидребезг по-крупному: та же мысль, что с кнопками, только вместо миллисекунд — два опроса по две секунды. Зуммер пищит не постоянно, а коротким сигналом tone(8, 1000, 200) раз в цикл: непрерывная сирена выматывает, короткий писк слышен и не бесит. Опрос сидит на millis с интервалом 2000 — тот же планировщик задач, что мы разбирали в уроке про планировщик на millis: между опросами loop свободен, и в него позже ляжет экран. Четыре ветки с переходами — младший брат светофора из урока про конечный автомат: те же правила, та же дисциплина.
После недели работы: лог, экран, сеть
Первую неделю контроллер жил без доработок, и Serial-журнал был судьёй: по нему видно, что порог 21,0 срабатывает около шести вечера, полоса держит воздух в 22 с размахом в градус, а аварии сырости случаются только после готовки. Моя полоса 21–23 лежит внутри зон комфорта, которые мы брали в уроке про индикацию комфорта светодиодами. Если журнал показывает чаще трёх-четырёх переключений реле в час — расширяйте полосу, а не частоту опроса: двух секунд для воздуха хватает с запасом, у интервала опроса есть свой предел.
Следующие ступени садятся без переделки ядра: пороги — в EEPROM, чтобы переживали отключение света; кадр данных — строкой CSV на SD-карту; режим и показания — на LCD с меню; когда надоест ходить на дачу — ESP32 с MQTT опубликует mode= и t= в брокер. Каждое направление разобрано в своём разделе, и всем им нужно одно и то же — свободный loop между опросами, а не какие-то новые пороги. Место для них освободил именно таймер на millis: delay(2000) похоронил бы экран и сеть ещё на старте.
- 1. Зачем термостату гистерезис с двумя порогами?
- 2. Пороги 21,0 и 23,0. Сейчас t = 22,0, реле выключено. Что сделает скетч?
- 3. Зачем счётчик streak при аварии по влажности?
- 4. Температура поднялась до 27,5 °C. Что делает скетч с реле?
- 5. Почему опрос раз в 2000 мс, а не каждые 100 мс?
- Каждый порог — это состояние, а не импульс: heater и mode меняются по правилам, и digitalWrite на реле живёт в одном месте скетча.
- Гистерезис 21,0/23,0 вчетверо шире погрешности DHT22 — воздух без волн, реле без износа.
- Аварии двух уровней: перегрев 27 °C глушит реле немедленно, сырость 65 % считается только по двум опросам подряд.
- Светодиод и Serial — минимальная телеметрия: по mode=heat/hold/alarm состояние видно с дивана и по журналу.
- Сначала неделя автономной работы с Serial-журналом, потом EEPROM, экран и сеть — ядру проекта они не нужны.
Частые вопросы по теме урока
- Зачем термостату гистерезис с двумя порогами?
- Один порог заставляет реле дребезжать на границе: включился, нагрелся на сотую — выключился. Полоса 21–23 вчетверо шире погрешности сенсора, и реле живёт годами. Чтобы реле не щёлкало на каждой десятой доле градуса
- Пороги 21,0 и 23,0. Сейчас t = 22,0, реле выключено. Что сделает скетч?
- Внутри полосы гистерезиса ни один порог не сработал, и heater остаётся как был. Смена состояния — только на выходе за границу полосы. Ничего: внутри полосы состояние сохраняется
- Зачем счётчик streak при аварии по влажности?
- Одиночный выброс влажности — не событие. Два опроса подряд выше 65 % — уже тренд, и зуммер имеет право пищать. Авария засчитывается только по двум опросам подряд — скачок от чайника не будит сирену
- Температура поднялась до 27,5 °C. Что делает скетч с реле?
- Перегрев — аварийная ветка: heater = 0 безусловно, дальше телеметрия и писк. Никакая другая ветка не должна иметь права включить реле в этой ситуации. Принудительно выключает, минуя режимы
- Почему опрос раз в 2000 мс, а не каждые 100 мс?
- Интервал опроса ограничен самим датчиком: чаще спрашивать — значит собирать nan или кэш. Реакция контроллера на комнатные процессы в две секунды укладывается с запасом. DHT22 обновляет внутренние значения раз в две секунды — чаще новых данных нет