Аквариумный шкаф у сестры держит порог подогрева, яркость ночника и дневной или ночной режим. Через неделю после установки звонок: выключила из розетки утром, вечером включила — порог сбежал на заводской. Диагноз я поставил по памяти, потому что грабля была моя: я сохранял настройки таймером раз в пять минут, а она выдёргивала вилку раньше очередного тика. В оперативке осталось несохранённое, в памяти лежал вчерашний день. С тех пор я не доверяю автосейву без светодиода: пользователь должен видеть, есть ли несохранённое. Я тогда переписал сохранение на кнопку за вечер и с тех пор ставлю этот цикл во все устройства с ручками.
Правильный цикл скучнее и надёжнее: поменял — сохранил по событию — выключил когда угодно — включил и сверил. Четыре шага, ни один не просит хитростей, но каждый надо сделать честно. Сценарий — это и есть тест приёмки: устройство, которое его проходит, можно отдавать в чужие руки. Этот урок — про сборку цикла целиком: структура Settings, загрузка с проверкой, кнопка сохранения, светодиод состояния и контрольная печать после включения.
Место урока в серии: проектирование ящика мы делали в уроке про struct Config, здесь та же школа — только сверху добавляется сценарий. Соберём устройство с ручкой, кнопкой и тумблером и прогоним полный круг: от покрученной ручки до проверенных после включения настроек.
Структура Settings и загрузка при старте
Структура Settings нарочно маленькая: magic, версия, порог, яркость, режим — пять байт, на Uno без дырок. Порог у меня в градусах от 18 до 30, яркость — 0-255 для ШИМ ночника, режим — 0 или 1. Все поля байтовые: диапазоны честно влезают, бокс остаётся копеечным, а int понадобился бы только для значений за 255 — например, таймера в секундах. Байтовые поля ещё и безопасны для многолетнего хранения: меньше байт — меньше мест, где после сбоя питания может остаться обрывок старого значения. Минимальный бокс — ещё и быстрая загрузка: пять ячеек читаются за микросекунды, и setup не тяжелеет.
Загрузка в setup: get, затем проверка magic. Не совпал — свежая плата: ставим дефолты и сразу пишем их put-ом, первый запуск самообслуживается. Механика та же, что в уроке про magic-байт, просто контролька теперь едет внутри ящика и отдельного адреса под неё не выделяют. Двойная проверка — magic и версия — стоит одного условия и закрывает случай, когда ящик писан скетчем другой раскладки.
Сразу после загрузки — контрольная печать всех полей. Это не украшение: строка na starte: porog=24 bright=90 mode=0 в мониторе и есть ответ на вопрос сестры, что именно устройство прочло при включении. Без этой строки отладка сценария превращается в гадание по косвенным признакам. Печатайте и в дефолтной ветке: пользователь должен видеть, что устройство откатилось на заводские значения, а не тихо изменило поведение.
Ручка, кнопка, лампочка
Дальше события. Ручка на A0 крутит порог, тумблер на D3 переключает режим, яркость для простоты фиксируется — её изменение вопрос меню из урока про настройки на LCD. Сохранение висит на кнопке D2, по фронту нажатия, а не по уровню: уровень держится всё нажатие, и каждый проход loop был бы попыткой записи. Фронт превращает нажатие ровно в одно сохранение. Ручка без фиксации меняет порог постоянно — и это нормально: dirty-состояние для того и придумано, чтобы держать разницу между «на экране» и «в памяти». Дебаунс в двадцать-тридцать миллисекунд сюда уже входит: фронт фиксируется один раз на нажатие.
Между изменением и сохранением живёт dirty-состояние: ручка уже повернулась, бокс — ещё нет. Светодиод на D12 горит ровно в этом окне — «поменяли, не сохранили». Нажатие кнопки пишет бокс и гасит лампу. Правило для пользователя собирается из одного жеста: питание гасить можно, когда лампочка не горит. Окно несохранённого может длиться хоть неделю: бокс не портится, пока пользователь не нажал кнопку, а лампа всё это время честно горит.
Сравнение для dirty честное: текущие поля против сохранённой копии в оперативке. Копия обновляется только при сохранении — так скетч знает не «менялось ли вообще», а «сохранено ли то, что сейчас выставлено». Разница тонкая, но именно она отличает рабочий цикл от декоративного. Копию в оперативке держат не для экономии чтений, а для честного сравнения: сравнение двух структур — одна строка, а сверка с ячейками — цикл.
#include <EEPROM.h> const int addrBox = 0;const int pinSave = 2; // кнопка «сохранить»const int pinTumb = 3; // тумблер режимаconst int pinLed = 12; // «поменяли, не сохранили»const int pinPot = A0; // ручка порога struct Settings { byte magic; // 170 byte version; // 1 byte porog; // 18-30 градусов byte bright; // 0-255 byte mode; // 0 или 1}; Settings box; // рабочие значенияSettings sohran; // последняя сохранённая копияint knopkaByla = HIGH; bool gryazno() { return box.porog != sohran.porog || box.bright != sohran.bright || box.mode != sohran.mode;} void zagruzka() { EEPROM.get(addrBox, box); if (box.magic != 170 || box.version != 1) { box.magic = 170; box.version = 1; box.porog = 24; box.bright = 90; box.mode = 0; EEPROM.put(addrBox, box); Serial.println("defolty zapisany"); } sohran = box; Serial.print("na starte: porog="); Serial.print(box.porog); Serial.print(" bright="); Serial.print(box.bright); Serial.print(" mode="); Serial.println(box.mode);} void setup() { Serial.begin(9600); pinMode(pinSave, INPUT_PULLUP); pinMode(pinTumb, INPUT); pinMode(pinLed, OUTPUT); zagruzka();} void loop() { box.porog = map(analogRead(pinPot), 0, 1023, 18, 30); if (digitalRead(pinTumb) == HIGH) box.mode = 1; else box.mode = 0; if (gryazno()) { digitalWrite(pinLed, HIGH); } int knopka = digitalRead(pinSave); if (knopka == LOW && knopkaByla == HIGH) { sohran = box; EEPROM.put(addrBox, box); digitalWrite(pinLed, LOW); Serial.print("sohraneno: porog="); Serial.print(box.porog); Serial.print(" mode="); Serial.println(box.mode); } knopkaByla = knopka; delay(30);}Выключил — включил: проверка на честность
Второй круг сценария — включение. Плата стартует, setup читает бокс, magic совпал — поля поднимаются из EEPROM, печатается контрольная строка. Если напечатанное совпало с тем, что было на экране перед выключением, круг сошёлся. Я прогоняю его на приёмке каждого устройства: поменять, сохранить, выдернуть питание, включить, сверить — минута делов, а уверенность на годы. Питание в этом круге гасится грубо — выдёргиванием вилки: EEPROM не боится резкого отключения, в отличие от записи на середине. После сверки я печатаю строку cikl ok — в логе приёмки она стоит между номером платы и датой.
Если не сошлось, у расхождения три типовых подозреваемых, и печать при старте их разделяет. put не дошёл: кнопка нажималась, но питание ушло раньше записи. Magic не совпал: сработали дефолты — в печати заводские значения вместо ваших. Соседи по карте переехали: что-то другое пишет в те же адреса — здесь поможет разбор из урока про адресное пространство. Первый случай лечится привычкой нажимать кнопку, второй — пересмотром контрольного значения, третий — сверкой карты адресов. Третий случай самый коварный: он выглядит как первый, но кнопка тут ни при чём.
Отдельный случай — режим, привязанный к тумблеру. После включения скетч читает его положение, и если пользователь щёлкнул тумблер после сохранения, режим честно уедет в dirty со зажжённым светодиодом. Это не сбой памяти, а продолжение логики: устройство показывает «не сохранено» и ждёт кнопки. Если тумблер мешает — уберите его и переключайте режим кнопкой по длинному нажатию: суть цикла не меняется.
| Шаг сценария | Что делает скетч | Что видит человек |
|---|---|---|
| Покрутил ручку порога | значение поменялось в оперативке, dirty поднят | светодиод загорелся |
| Нажал кнопку сохранить | put бокса, копия обновлена, dirty сброшен | светодиод погас |
| Выдернул вилку из розетки | ничего: бокс уже в EEPROM | ячейки это не коснулось |
| Включил снова | get, проверка magic, печать полей | настройки на месте |
Про таймеры, чтобы закрыть историю с автосейвом. Раз в пять минут — 105120 циклов в год: гарантия 100000 кончается меньше чем за год. Раз в час — 8760, одиннадцать лет запаса. Но событие всё равно лучше таймера: оно пишет ровно тогда, когда что-то изменилось, и ни разу, когда не менялось. Задержки без delay для таких проверок — в уроке про millis. Между этими крайностями — весь диапазон разумного: считайте сохранения в сутки и делите 100000 на них. И держите в уме суммарную арифметику: у бокса настроек и у журнала из соседних уроков ресурсы считают раздельно.
У цикла настроек пристойно иметь выход: заводской сброс из отдельного урока возвращает дефолты, когда пользователь запутался, а восстановление режима после аварии питания разобрано в уроке про термостат — там бокс поднимается даже после сбоя. Здесь собран базовый круг; те уроки достраивают его до продукта. Оба опираются на тот же Settings — один ящик, который мы здесь научились гонять по кругу.
Движок симулятора EEPROM не эмулирует, поэтому полный круг показан на оперативной модели: бокс — массив, оперативка — переменные, выключение — команда 9 из монитора, которая обнуляет переменные и повторяет загрузку. Логика на модели та же, что на плате: ручка, кнопка, лампа, включение. Модель честная в главном: обнуление переменных — это то, что на плате происходит при потере питания, а массив box играет роль ячеек. Перенос на плату — замена массива на put и get из скетча выше.
- Settings — это magic, версия и поля настроек: загрузка get-ом, проверка magic, дефолты на первом запуске
- сохранение по событию — кнопка, меню, команда; таймер раз в пять минут съедает 105120 циклов в год
- dirty-флаг и светодиод: горит — «поменяли, не сохранили», погас — питание можно гасить
- контрольная печать всех полей при старте — тест «выключил — включил» без приборов
- настройки не дожили — три подозреваемых: put не дошёл, magic не совпал, соседи по карте переехали
- 1. Когда можно выдернуть питание, чтобы настройки точно дожили?
- 2. Автосохранение раз в пять минут. Сколько циклов наберёт ячейка за год?
- 3. Что скетч проверяет при загрузке до чтения полей бокса?
- 4. Почему сохранение по кнопке вешают на фронт нажатия, а не на уровень?
- 5. После включения настройки не те. Что проверить первым делом?
Частые вопросы по теме урока
- Когда можно выдернуть питание, чтобы настройки точно дожили?
- Гашение светодиода — граница: бокс уже в EEPROM. До неё питание терять нельзя — в оперативке осталось несохранённое. когда светодиод «не сохранено» погас после нажатия кнопки
- Автосохранение раз в пять минут. Сколько циклов наберёт ячейка за год?
- 12 сохранений в час — 288 в сутки, 105120 в год: гарантия 100000 кончается меньше чем за год. Таймер — плохая замена событию. около 105 тысяч — на гарантийной границе
- Что скетч проверяет при загрузке до чтения полей бокса?
- Magic отличает первый запуск от настроенного устройства. Свежая EEPROM читается числом 255 — на это проверка и опирается. magic-байт: совпал — настройки есть, не совпал — свежая плата и дефолты
- Почему сохранение по кнопке вешают на фронт нажатия, а не на уровень?
- Кнопка держится полсекунды — это пятнадцать-двадцать проходов loop. Без проверки фронта каждое нажатие превращается в серию сохранений. уровень держится всё нажатие — put пойдёт десятки раз
- После включения настройки не те. Что проверить первым делом?
- Три подозреваемых: сохранение не прошло, magic не совпал и сработали дефолты, соседи по карте затёрли адреса. Печать при старте отделяет первый случай от остальных. контрольную печать при старте и magic: дошёл ли put и тем ли ящиком читаем