Погреб в старом доме я грёл обогревателем на два киловатта через термостат собственной сборки: реле держало плюс пять, и всё было хорошо, пока однажды ночью не мигнуло питание на пару секунд. Прибор проснулся, вежливо прочитал порог из EEPROM — и получил его, порог цел. Но режим работы я хранил в обычной переменной: по умолчанию скетч стартовал в режиме «прогрев», грел до плюс пятнадцати и отключался. К утру в погребе было минус один, а банки с заготовками честно замёрзли.
Тупик был в моей уверенности, что «настройки сохранены» — это всё. Настройки-то сохранены: порог цел, я даже проверил его из монитора порта и порадовался. Но у прибора кроме порога есть состояние: режим, гистерезис, то, чем он занимался до сбоя. Прибор после перезагрузки обязан просыпаться в том же состоянии, в котором его застала темнота, а не в том, которое мне было удобно написать в setup().
Соберём термостат, который переживает сбой целиком: порог и гистерезис лежат в структуре в EEPROM, кнопки тюнинга меняют порог, а запись происходит по таймеру — через пять секунд тишины после последнего нажатия. В структуру зайдёт и подпись-байт: не сходится — считаем память пустой и пишем дефолты, мусор к реле не проходит. В одном скетче сведётся всё, что мы уже умеем: put и get из урока про структуры, отрицательные пороги из урока про байты, гистерезис из урока про термостат на DHT.
В симуляторе EEPROM не эмулируется — память настроек изображает массив mem в самом скетче: на реальной плате он переживает питание, в симуляторе живёт до перезапуска. Температуру изображает потенциометр A0, реле — модуль D7, кнопки тюнинга — D2 и D3. Покрутите порог кнопками: через несколько секунд тишины появится строка save с содержимым модельной памяти, а при старте скетч напечатает, что восстановил. Гистерезис виден на глаз: реле щёлкает не на самом пороге, а с зазором в десятую градуса с каждой стороны.
Что помнить, чтобы пережить сбой
Инвентаризация состояния термостата начинается со списка. Первое — порог срабатывания: целое со знаком в десятых долях градуса, чтобы и минус восемь для погреба, и плюс двадцать шесть для теплицы ложились без извращений. Второе — гистерезис: тот самый зазор, из-за которого реле щёлкает не пятьсот раз в час, а два. Третье — режим: авто, выключено, форсаж. Четвёртый пункт самый спорный — положение реле на момент сбоя. К списку сразу прицепляется защита от мусора: если читаете структуру после сбоя, проверяйте каждую цифру на рабочий диапазон — порог вне минус тридцати..плюс сорока значит, что память битая, и добрее подставить дефолты, чем греть по ерунде.
С положением реле я ошибался в обе стороны. Первый инстинкт — сохранить и восстановить: погреб должен продолжать греться, даже если сбой случился в три ночи. Контрпример заставил передумать: обогреватель в теплице, сбой питания, датчик отдал мусор, а реле восстановилось включённым — ТЭН сутки работал без контроля. Теперь у меня правило с границей: холодные и пассивные нагрузки — обогрев погреба, вытяжка, вентиляция — состояние восстанавливают; мощные тепловые — ТЭН, калорифер — всегда стартуют с выключенным реле и включаются только по свежему замеру. Логика самого гистерезиса с двумя порогами та же, что мы катали в отдельном уроке про термостат на DHT, — меняется только источник настроек.
#include <EEPROM.h>#include <DHT.h> DHT dht(2, DHT22); const int knPlus = 3; // +0.5 к порогуconst int knMinus = 4; // -0.5 к порогуconst int rele = 7;const int led = 12; // зеркалит реле struct Nastrojki { byte podpis; // 165: признак живых настроек int t10; // порог в десятых градуса (может быть минус) byte gist; // гистерезис в десятых градуса byte rezhim; // 1-авто, 2-выкл}; Nastrojki cfg; unsigned long tZamera = 0;unsigned long tPravki = 0;bool ispravleno = false; // правки есть - пора писать void setup() { Serial.begin(9600); pinMode(knPlus, INPUT_PULLUP); pinMode(knMinus, INPUT_PULLUP); pinMode(rele, OUTPUT); pinMode(led, OUTPUT); dht.begin(); EEPROM.get(0, cfg); if (cfg.podpis != 165 || cfg.t10 < -300 || cfg.t10 > 400) { cfg.podpis = 165; cfg.t10 = 50; // +5.0 для погреба cfg.gist = 10; // 1.0 градус cfg.rezhim = 1; EEPROM.put(0, cfg); Serial.println("pamyat chista - zapisany defolty"); } digitalWrite(rele, LOW); // реле всегда OFF до свежего замера Serial.print("vosstanovlen t10="); Serial.println(cfg.t10);} void loop() { // тюнинг кнопками: меняем только RAM и взводим флаг if (digitalRead(knPlus) == LOW) { cfg.t10 = cfg.t10 + 5; ispravleno = true; tPravki = millis(); delay(250); } if (digitalRead(knMinus) == LOW) { cfg.t10 = cfg.t10 - 5; ispravleno = true; tPravki = millis(); delay(250); } // запись по таймеру: 5 секунд тишины после последней правки if (ispravleno && millis() - tPravki > 5000) { EEPROM.put(0, cfg); // put пишет по байтам через update ispravleno = false; Serial.print("save t10="); Serial.println(cfg.t10); } // замер не чаще 2 секунд: физический минимум DHT22 if (millis() - tZamera >= 2000 && cfg.rezhim == 1) { tZamera = millis(); float t = dht.readTemperature(); if (t > -40 && t < 80) { // мусор не пускаем к реле if (t * 10 < cfg.t10 - cfg.gist) { digitalWrite(rele, HIGH); // холоднее зазора - греем } if (t * 10 > cfg.t10 + cfg.gist) { digitalWrite(rele, LOW); // теплее зазора - отдыхаем } } digitalWrite(led, digitalRead(rele)); } delay(50);}В скетче два ритма, и оба честнее любых комментариев. Кнопки меняют только переменную в оперативной памяти и взводят флаг: никакой записи на каждое нажатие. Запись происходит, когда пользователь убрал руку и пять секунд ничего не трогал, — один заход за сессию тюнинга, сколько бы цифр он ни накрутил. Дребезг между нажатиями ресурс не тратит вовсе, а таймер снова на millis из урока про delay: замеры и реле во время правок не останавливаются. Задержка delay(250) внутри обработчика кнопки выглядит как уступка лени, но здесь она безвредна: 250 мс подвисания раз в нажатие термостат переживёт, а запись и замер в этот момент и так стоят.
EEPROM.put внутри проходит по структуре байт за байтом и пишет через update: изменился порог — потратится цикл на ячейки с t10, остальные пропустятся. Прикинем износ: тридцать правок порога в день — это порядка одиннадцати тысяч циклов в год на пару ячеек из ста тысяч, запас лет на девять; типичный пользователь трогает порог в десять раз реже. Арифметику мы подробно считали в уроке про ресурс, и вывод там тот же: редкая запись — вечная память.
Подпись-байт со значением 165 закрывает первый запуск: чистая EEPROM читается как мусор, подпись не сходится — пишем дефолты и живём дальше. Число 165 не святое: это 10100101 в двоичном виде, чередование нулей и единиц, которое редко выходит из случайной мёртвой памяти. Диапазонная проверка порога в той же строке условия подстраховывает второй случай: подпись уцелела случайно, а вот порог внутри — мусор. Когда настроек станет больше десятка, к подписи стоит добавить контрольную сумму: не сошлась — считаем настройки битыми и ставим заводские без попыток угадать, какой байт виноват.
Ритмы прибора: замер, решение, запись
Термостат с памятью — это три независимых таймера на millis: замер не чаще двух секунд (меньше DHT22 физически не успевает), переключение реле по двум порогам с зазором и запись настроек через пять секунд после последней правки. Масштаб зазора подбирается под нагрузку: для погреба хватает градуса, для инкубатора, где цена вопроса 37,5 против 38,2, зазор режется до двух-трёх десятых — иначе реле будет ходить по кругу весь срок. Ни один delay не должен мешать соседям: пока кнопки ждут пользователя, реле греет, пока реле греет — таймер записи тикает. В этом скетче собрался почти весь раздел: чтение настроек при старте из первого урока про EEPROM, побайтовая экономия через update, структуры через put и get, расчёт ресурса. Осталось одно умение, и оно вынесено в следующий урок: а что делать, если вашей плате EEPROM вообще не досталось.
Проверяется всё издевательством над питанием. Мой стенд: настроил порог кнопками, дождался строки save, выдернул вилку, воткнул снова — прибор обязан проснуться с тем же порогом и в том же режиме. Повторить трижды, включая выдёргивание прямо во время сессии тюнинга: последняя правка, не успевшая дожить до таймера, имеет право потеряться — всё, что до неё, выжить обязано. Если после третьего цикла порог уползает хоть на полградуса, ищите, где вы пишете чаще, чем думаете, — обычно виноват двойной вызов put в двух ветках.
| Что сохраняем | Тип и размер | Восстанавливать при старте |
|---|---|---|
| Порог срабатывания t10 | int, 2 байта, десятые градуса | всегда, с проверкой диапазона |
| Гистерезис | byte, 1 байт | всегда |
| Режим: авто или выкл | byte, 1 байт | всегда |
| Положение реле | 1 бит | только холодным нагрузкам; тепловые стартуют с OFF до свежего замера |
- 1. С какой минимальной частотой можно опрашивать DHT22 в термостате?
- 2. Пользователь десять раз подряд жмёт плюс. Сколько записей в EEPROM сделает скетч с записью через 5 секунд тишины?
- 3. Почему реле тепловых нагрузок при старте всегда выключают до первого замера?
- 4. Что делает EEPROM.put со структурой, у которой изменился один байт?
- 5. Порог минус восемь целых ноль десятых градуса в переменной t10 — сколько это?
- Порог, гистерезис и режим — в одной структуре через EEPROM.put/get; подпись-байт 165 отличает чистую память от настроенной
- Тюнинг кнопками меняет только RAM и взводит флаг; запись — через 5 секунд тишины: одна на сессию, а не на нажатие
- put пишет по байтам через update: одна правка порога — один-два цикла, а не вся структура
- Реле тепловых нагрузок при старте OFF до свежего замера; восстанавливать положение — только холодным нагрузкам
- Замер DHT22 не чаще 2 секунд и с порогом правдоподобия: мусор от датчика не должен командовать реле
Термостат переживает сбой, кнопки тюнинга не жгут память, реле стартует безопасно. Но всё это — на классической AVR с настоящей EEPROM-областью на кристалле. Стоит переехать на ESP32 с Wi-Fi — и привычный EEPROM.read встретит сюрпризом: той памяти, к которой вы привыкли, там физически нет. Чем её заменяют и почему тамошний EEPROM.h — эмуляция поверх флеша, — следующий урок.
Частые вопросы по теме урока
- С какой минимальной частотой можно опрашивать DHT22 в термостате?
- Физический минимум DHT22 — два секунды между чтениями; чаще приходит nan или предыдущий результат. Датчик — не самая медленная часть прибора, но самая капризная к частоте. Не чаще чем раз в 2 секунды
- Пользователь десять раз подряд жмёт плюс. Сколько записей в EEPROM сделает скетч с записью через 5 секунд тишины?
- Правки живут в RAM и взводят флаг, запись одна — после пяти секунд без нажатий. Ресурс тратится за сессию тюнинга, а не за нажатие: десять кнопок против одной записи. Одну
- Почему реле тепловых нагрузок при старте всегда выключают до первого замера?
- Сбой мог оставить реле включённым, а датчик после сбоя может отдавать мусор или nan. Свежий замер и порог правдоподобия — обязательное условие первого включения нагрева. Иначе восстановленное включённым реле с дохлым датчиком греет без контроля
- Что делает EEPROM.put со структурой, у которой изменился один байт?
- put раскладывает структуру на байты и пишет через update, поэтому повторное сохранение не трогает ячейки с прежними значениями. Одна правка порога — один-два цикла, а не вся структура. Пишет по байтам через update: изменённые пишет, одинаковые пропускает
- Порог минус восемь целых ноль десятых градуса в переменной t10 — сколько это?
- Десятые доли: минус восемь градусов — это минус восемьдесят десятых. Int со знаком влезает; раскладку на байты мы считали в отдельном уроке. −80