Мой термостат для рассады прожил на подоконнике две недели и за это время успел разозлить всю семью. Схема была честная: DHT22, реле на обогреватель, пороги 22 и 28 градусов. Но каждый вечер кто-то выключал удлинитель — то розетка нужна пылесосу, то просто «лампочка лишняя горит». Питание возвращалось, Arduino грузился, а пороги читались из скетча: те же 22 и 28, которые я задал при прошивке. Поменять их без ноутбука было нельзя, и прибор из умного превращался в упрямый.
Виновата была не логика, а память. Переменные живут в RAM и умирают вместе с питанием за миллисекунды; EEPROM на той же плате держит данные годы после отключения — по даташиту Atmel около ста лет при комнатной температуре. В этом уроке мы поселим пороги в энергонезависимую память: научимся кодировать их в байты, не изнашивать ячейки, распознавать самый первый запуск и менять настройки двумя кнопками — уже без ноутбука и без перепрошивки. База работы с этой памятью лежит в уроке про первый байт в EEPROM, а порогами мы будем рулить теми же, что и в термостате с гистерезисом.
Что куда кладём: раскладка по адресам
EEPROM на Uno — это 1024 ячейки по одному байту, каждая адресуется номером от 0 до 1023. Раскладку я придумываю до кода, на бумажке, потому что адреса, однажды занятые, переезжать не любят: старые платы в поле продолжают читать старые адреса, и рассинхрон между версиями скетча — классический источник загадочного поведения. Для порогов нужны три ячейки, и всё помещается в первые адреса.
Отдельная ячейка отводится под контрольный байт — подпись «здесь лежат мои данные». Дело в том, что свежая EEPROM с завода читается сплошными 255, а скетч никак иначе не узнаёт, писал он сюда или нет. Я записываю в нулевой адрес число 77: если при старте там 77 — настройки уже сохранялись, читаем их; если что-то другое — память девственно чиста, кладём значения по умолчанию. Хитрые системы считают CRC всех настроек, но для трёх байтов простая подпись честно живёт у меня годами.
Скорости работы с этой памятью стоит знать заранее, чтобы не ждала неприятность. Чтение ячейки почти бесплатное — микросекунды, читай хоть каждый проход loop. А вот запись одного байта занимает около 3,3 миллисекунды, и всё это время процессор честно ждёт: контроллер не может ни считать пороги, ни мигнуть светодиодом, пока ячейка прожигается. Десяток байтов в setup на старте — незаметно; блок настроек, переписываемый посреди рабочего цикла, уже ощутим. Отсюда простая привычка: вся запись — в момент нажатия кнопки или команды из порта, никогда — по кругу.
| Адрес | Что лежит | Диапазон | Почему так |
|---|---|---|---|
| 0 | подпись 77 | 77 | маркер «настройки уже записаны» |
| 1 | нижний порог | 20–42 | полуградусы: 20 = 10,0 °C, 42 = 21,0 °C |
| 2 | верхний порог | 46–60 | полуградусы: 46 = 23,0 °C, 60 = 30,0 °C |
Кодируем градусы в байты
Байт умеет хранить целые от 0 до 255, а пороги у нас дробные — 26,5 °C. Решение старое, как полевые журналы: умножить на два и хранить полуградусы. Порог 26,5 становится числом 53, шаг настройки — половина градуса, обратное преобразование — одно деление на 2.0. Для DHT22 с его шагом 0,1 °C полуградус грубовато, но порог — не измерение: пользователю всё равно лишняя сотая не нужна, а один байт на значение экономит и адреса, и нервы. Если понадобится полная точность, float раскладывается в четыре байта — способ разобран в уроке про int и float в байтах.
Второе правило — писать через EEPROM.update, а не EEPROM.write. Update сравнивает новое значение со старым и физически записывает ячейку только при изменении. Зачем эта перестраховка: ресурс ячейки — около 100 000 циклов записи, и порог, переписываемый каждые две секунды в loop, убьёт ячейку за два с лишним суток непрерывной работы. Update при чтении-сравнении продлевает жизнь памяти в сотни раз — механика износа и защиты подробно вскрыта в уроке про износ ячеек.
#include <EEPROM.h> #define A_MAGIC 0#define A_LO 1#define A_HI 2 void saveLo(float c) { EEPROM.update(A_LO, (int)(c * 2)); // 21.5 -> 43} void saveHi(float c) { EEPROM.update(A_HI, (int)(c * 2));} void loop() { if (Serial.available()) { String cmd = Serial.readStringUntil('\n'); cmd.trim(); if (cmd.startsWith("lo=")) saveLo(cmd.substring(3).toFloat()); if (cmd.startsWith("hi=")) saveHi(cmd.substring(3).toFloat()); Serial.print("lo="); Serial.print(EEPROM.read(A_LO) / 2.0, 1); Serial.print(" hi="); Serial.println(EEPROM.read(A_HI) / 2.0, 1); }}Пороги в деле: кнопки вместо ноутбука
Симулятор ниже собирает всё вместе: датчик на пине 2, две кнопки — минус и плюс верхнего порога, светодиод с ритмами из урока про зоны. При старте скетч проверяет подпись в нулевом адресе: нет её — записывает значения по умолчанию 20.0 и 26.0; есть — молча читает то, что вы настроили раньше. Управление через Serial тоже никуда не делось — полный словарь команд разобран в уроке про командный парсер, а кнопки здесь просто быстрее.
Обратите внимание, как скетч читает пороги на каждом проходе: не держит копию в переменной «чтобы быстрее», а лезет в EEPROM каждый раз. Чтение этой памяти бесплатное — ресурс тратит только запись, а такой стиль гарантирует, что в решении всегда то, что физически лежит в памяти, и отладка сводится к печати lo= и hi= в монитор.
- Покрутите пороги кнопками до упора вниз и вверх: скетч молча игнорирует выход за 10,0 и 30,0. Уберите ограничения if и посмотрите, как одна зажатая кнопка ломает логику — потом верните.
- Добавьте в setup печать всех трёх ячеек подряд с адресами: маркер, нижний, верхний. Такой дамп — половина отладки энергонезависимой памяти, привыкайте смотреть её глазами.
- Сдвиньте ползунок датчика за верхний порог и посчитайте по монитору, за сколько строк строб подтвердит превышение. Потом сузьте зону до одного градуса и сравните поведение.
- 1. Почему нельзя просто хранить пороги в глобальных переменных?
- 2. Как скетч понимает, что EEPROM ещё ни разу не записывался?
- 3. Порог переписывается в EEPROM каждые 2 секунды через write. Сколько проживёт ячейка?
- 4. Чем EEPROM.update лучше EEPROM.write для настроек?
- 5. Порог 26,5 °C надо уложить в один байт. Как поступить?
- Пороги живут в EEPROM и переживают выключение: RAM — на время работы, EEPROM — годы.
- Раскладку адресов рисуйте до кода: маркер, нижний порог, верхний; менять потом — боль.
- Контрольный байт 77 отличает первый запуск от обычного, где настройки честно читаются из памяти.
- Пишите через EEPROM.update и только при изменении: ресурс ячейки — 100 000 циклов, его легко сжечь за двое суток.
- Дробные пороги кодируйте в байты полуградусами; кнопки плюс-минус делают настройку независимой от ноутбука.
Дальше у этой истории два красивых продолжения. Первое — хранить настройки пакетом: структура с порогами, гистерезисом и адресом I2C дисплея кладётся одной командой EEPROM.put, механика разобрана в уроке про put и get со структурой. Второе — вернуть настройкам наглядность: тот же порог в виде трёхзонной индикации из урока про светодиоды комфорта, только теперь границы не зашиты в скетч, а лежат в памяти и правятся кнопками.
И держите в голове главный ритуал этого урока: соберите прибор, покрутите пороги, выключите питание из розетки, включите снова — и убедитесь, что настройки на месте. Одна такая проверка руками стоит трёх страниц кода: энергонезависимость — это не флаг компилятора, а свойство, которое надо увидеть собственными глазами.
Частые вопросы по теме урока
- Почему нельзя просто хранить пороги в глобальных переменных?
- RAM живёт только при поданном питании. Пороги в переменных переживают reset платы, но не выключение — а приборы выключают постоянно. RAM теряет содержимое при отключении питания
- Как скетч понимает, что EEPROM ещё ни разу не записывался?
- С завода EEPROM читается сплошными 255. Контрольный байт-подпись надёжнее: он отличает именно ваши данные от любого мусора. Свежая ячейка читается как 255, но надёжнее свой контрольный байт
- Порог переписывается в EEPROM каждые 2 секунды через write. Сколько проживёт ячейка?
- 100 000 циклов умножить на 2 секунды — около 200 000 секунд, это примерно 55 часов или чуть больше двух суток непрерывной работы. Около 55 часов
- Чем EEPROM.update лучше EEPROM.write для настроек?
- Update сравнивает новое значение со старым и физически записывает ячейку только при различии. Ресурс 100 000 циклов расходуется в сотни раз медленнее. Не тратит цикл записи, если значение не изменилось
- Порог 26,5 °C надо уложить в один байт. Как поступить?
- Умножение на два превращает дробь в целое 53, которое помещается в байт. Обратное преобразование — деление на 2.0. Float тоже возможен, но это уже четыре ячейки. Хранить полуградусы: 53, а при чтении делить на 2