Инкубатор на сорок яиц я собирал в герметичной коробке: подогрев на реле, датчик на A0, одна кнопка снаружи и один светодиод. Экран не влезал ни по цене, ни по отверстию в крышке. Через месяц сосед, у которого жил прибор, спросил, как подвинуть порог: у него та же коробка, те же две детали и ни одного символа интерфейса. Поездка с ноутбуком и кабелем ради сдвига одной константы — цена, которую платят за интерфейс, который не сделали сразу.
Две кнопки плюс и минус с таймером тишины — рабочий вариант, и он уже разобран в уроке про тюнинг кнопками. Но на крышке инкубатора было место под одну кнопку, и это не вырожденный случай: терморегуляторы в теплицах, таймеры на DIN-рейке, кормушки — везде одна кнопка и ноль дисплеев. Меню с экраном и тремя кнопками живёт в отдельном уроке про LCD; между «один параметр, две кнопки» и «экран» лежит ниша слепого меню: параметры листает та же кнопка, только по удержанию.
План такой: значение ходит не по единицам АЦП, а по заранее размеченной шкале из десяти делений; короткий клик — шаг, удержание — переход к следующему параметру с записью; светодиод разговаривает вспышками. И главный критерий, ради которого всё затевалось: выдернуть питание, воткнуть обратно — и увидеть те же цифры. Скетч обязан пройти этот тест до того, как коробка закроется.
Шкала из десяти делений: значение как номер шага
Порог я храню в ячейке не в единицах АЦП, а номером деления: шкала от 200 до 650 с шагом 50 — это номера с нуля до девяти. В ячейке лежит маленькое число, и в дампе памяти оно читается глазами: 6 — это порог 500, а не загадочный байт. Гистерезис устроен так же: шаг 5 единиц, диапазон 5-50, дефолт 15. Приём родственен упаковке порог/4 из урока про тюнинг кнопками — та же экономия байта, только шаг крупнее и делений ровно десять.
Деления экономят не только байт, но и нервы. От 300 к 500 единицами АЦП — четыре клика вместо сорока: оператор не листает пространство, он выбирает отметку. Запись при каждом клике не происходит вовсе: пока пальцы ходят по шкале, значение живёт в оперативной переменной, и ячейка молчит. Запись случается один раз — в момент ухода из параметра длинным нажатием. Десять кликов — одна запись вместо десяти; update всё равно стоит держать: он пропустит перезапись, если новое деление совпало со старым.
И про главный тест. Скетч считается готовым, когда проходит цикл: настроил — погасил питание — включил — увидел свои цифры. Это не педантичность: у меня была партия терморегуляторов, где ячейка порога писалась, а ячейка гистерезиса забывалась — поодиночке обе ветки работали, вместе прибор жил до первого ребута. Тест на два выдёргивания вилки занимает минуту и ловит ровно такое.
#include <EEPROM.h> // Карта: 0 - деление порога (0-9), 1 - деление гистерезиса (0-9).// Короткий клик - шаг по шкале, удержание 1,2 с - запись и следующий параметр.const int A_POROG = 0;const int A_GIST = 1;const byte PIN_BTN = 2;const byte PIN_LED = 12;const unsigned long KLICH = 600;const unsigned long DOLGO = 1200; int param = 0;int porog = 500;int gist = 15;bool gret = false;bool prevBtn = HIGH;unsigned long tDown = 0;bool dolgoOtrabotano = false; void sohranitIUjti() { if (param == 0) { EEPROM.update(A_POROG, (porog - 200) / 50); } else { EEPROM.update(A_GIST, (gist - 5) / 5); } for (byte i = 0; i < 2; i++) { // двойное мигание: записано digitalWrite(PIN_LED, HIGH); delay(70); digitalWrite(PIN_LED, LOW); delay(70); } Serial.print("sohraneno parametr "); Serial.println(param);} void setup() { Serial.begin(9600); pinMode(PIN_BTN, INPUT_PULLUP); pinMode(PIN_LED, OUTPUT); byte p = EEPROM.read(A_POROG); byte g = EEPROM.read(A_GIST); if (p > 9) p = 6; // свежая память или мусор if (g > 9) g = 2; porog = 200 + p * 50; gist = 5 + g * 5; Serial.print("start: porog="); Serial.print(porog); Serial.print(" gist="); Serial.println(gist);} void loop() { bool btn = digitalRead(PIN_BTN); if (prevBtn == HIGH && btn == LOW) { tDown = millis(); dolgoOtrabotano = false; } if (btn == LOW && !dolgoOtrabotano && millis() - tDown > DOLGO) { sohranitIUjti(); param = param + 1; if (param > 1) param = 0; dolgoOtrabotano = true; Serial.print("parametr "); Serial.println(param); } if (prevBtn == LOW && btn == HIGH) { if (!dolgoOtrabotano && millis() - tDown < KLICH) { if (param == 0) { porog = porog + 50; if (porog > 650) porog = 200; } else { gist = gist + 5; if (gist > 50) gist = 5; } digitalWrite(PIN_LED, HIGH); // вспышка: значение принято delay(40); digitalWrite(PIN_LED, LOW); Serial.print("porog="); Serial.print(porog); Serial.print(" gist="); Serial.println(gist); } } prevBtn = btn; int dat = analogRead(A0); // работа: подогрев по порогу if (dat < porog - gist) gret = true; if (dat > porog) gret = false; digitalWrite(PIN_LED, gret ? HIGH : LOW); delay(20);}Кнопка читается в каждом проходе loop без delay, и вся логика держится на двух метках времени. В момент нажатия скетч запоминает millis() в tDown; в момент отпускания сравнивает: короче 600 мс — клик, шаг по шкале; длиннее 1200 мс — переход уже отработал по флагу, отпускание игнорируется. Длинное нажатие срабатывает не по отпусканию, а прямо в удержании — на 1200-й миллисекунде, пока палец ещё на кнопке: прибор отвечает сразу, а флаг dolgoOtrabotano не даёт тому же нажатию сработать второй раз. Скелет без delay — из урока про millis; при шаге 50 единиц дребезг незаметен, но для старых кнопок есть полный разбор.
Запись привязана к уходу из параметра: длинное нажатие сначала сохраняет текущее значение, потом переключает индекс. Почему не на каждом клике: серия из десяти кликов — это девять промежуточных отметок, которые пользователь не выбирал; в память должен лечь итог. Цена метода — один сценарий: питание пропало посреди правки, до ухода из параметра. Теряется правка текущей шкалы, соседний параметр цел: он был записан при уходе с него. Тот же паттерн «сначала результат, потом фиксация», что и в таймере тишины, только момент фиксации выбирает оператор.
Светодиод у прибора двойной роли: между нажатиями он показывает подогрев — горит, пока датчик ниже порога с гистерезисом. Вспышки поверх индикатора короткие: сорок миллисекунд на клик, две по семьдесят на запись; запись двух байт занимает около семи миллисекунд процессора по 3,3 мс на байт, и loop всё равно замер на время вспышек. Оператору это выглядит как речитатив: щелчок — короткая вспышка, зафиксировано — двойная. Если прибору нужен чистый язык — индикатор уводится на второй светодиод, например на D13, а вспышки остаются на своём.
Отдельно про дребезг и удержание. При шаге 50 единиц фантомный двойной клик сдвигает отметку на лишнее деление — для фоторезистора с его дрожью это незаметно, и я не ставлю сюда отдельную логику. Но проверьте собственный корпус: кнопка с длинными проводами на герметичной крышке дребезжит всерьёз, и тогда фронт стоит прочищать окном 30-50 мс на millis — тот же скелет, одна переменная с меткой последнего фронта.
В модели короткий клик шагает активную шкалу, удержание печатает строку sohraneno с номером ячейки и делением, мигает дважды и переключает параметр. Свежая модель заполнена 255 — загрузка берёт дефолты и честно об этом печатает. Пройдите оба параметра по кругу: порог шагается с 650 обратно на 200, гистерезис — с 50 на 5, кольцо шкалы замкнулось само, без отдельных кнопок «в начало». Крутаните ручку A0: светодиод подогрева гаснет и загорается по порогу — настройка видна в работе, а не только в мониторе.
Честная оговорка про память: EEPROM в симуляторе не эмулируется, модель живёт в оперативке, и перезапуск скетча возвращает её к свежей плате со всеми 255. На настоящей плате массив заменён двумя EEPROM.update — и тогда рестарт покажет главное: цифры после вилки совпали с цифрами до неё. Тест на два ребута обязателен и в железе — об этом третий абзац под заголовком выше.
Запись по уходу против таймера тишины
| Стратегия сохранения | Записей на настройку из 10 кликов | Что с прибором и ячейкой |
|---|---|---|
| Запись на каждый клик | 10 | Девять промежуточных отметок в памяти, 3,3 мс заморозки на клик; меню чувствует себя заторможенным |
| По уходу из параметра (этот урок) | 1 | Серия кликов едет в RAM, запись одна в момент удержания; сбой посреди правки теряет текущую шкалу |
| Таймер тишины 3 с | 1 | Тот же итог, но момент записи выбирает скетч; нужна пауза без нажатий — как в уроке про тюнинг кнопками |
| Отдельная кнопка Save | 1 | Честно и явно, но кнопка вторая, и классика «а Save нажать?» остаётся |
Когда слепое меню перестаёт работать: от трёх параметров и больше. Оператор без экрана не помнит, на каком пункте он стоит, и на четвёртом путается даже я. Дальше — дисплей: автомат просмотра и правки с записью по выходу собран в уроке про меню на LCD, звёздочка на экране заменяет мой язык вспышек. До трёх параметров одна кнопка честно справляется: порог, гистерезис, режим — листаются удержанием, значения ходят кликами.
Шкалу подгоняйте под датчик и под оператора. Фоторезистор дрожит на 10-15 единиц — шаг 50 делает дрожь незаметной; термостат с целевой температурой просит делений по полградуса, и их будет двадцать — на грани. Больше десяти делений — сигнал, что пора экранизировать меню или дробить параметры на группы. Дефолт держите в середине шкалы: прибор с заводскими настройками уже работоспособен, даже если до меню никто не дотронулся. И называйте параметры вслух теми же словами, что на наклейке: «порог» в инструкции и «нижняя отметка» на крышке — разные вещи для человека, который держит коробку в руках.
- 1. Когда скетч из урока пишет параметр в EEPROM?
- 2. Что именно лежит в ячейке порога?
- 3. Оператор держал кнопку 400 мс и отпустил. Что сделает скетч?
- 4. В ячейке при старте читается 255. Что сделает загрузка?
- 5. Почему клик считается по отпусканию, а не по нажатию?
- Порог и гистерезис хранятся номерами делений: шкала 200-650 с шагом 50 — в ячейке число 0-9, читаемое в дампе глазами
- Короткий клик — шаг по шкале без записи; удержание 1,2 с — запись и переход к следующему параметру
- Запись привязана к уходу из параметра: серия кликов превращается в одну запись, потери при сбое ограничены текущей шкалой
- Клик считается по отпусканию, длинное нажатие — по флагу удержания: иначе каждое долгое нажатие двоит меню
- Свежая ячейка 255 — сигнал загрузить дефолт; тест на два выдёргивания вилки обязателен до закрытия корпуса
Частые вопросы по теме урока
- Когда скетч из урока пишет параметр в EEPROM?
- Пока оператор ходит по шкале, значение живёт в RAM. Запись одна — в момент ухода: серия кликов превращается в одну запись итогового деления. При уходе из параметра длинным нажатием
- Что именно лежит в ячейке порога?
- Шкала 200-650 разбита на десять делений по 50 единиц; в ячейке число 0-9, которое читается в дампе глазами. Номер деления шкалы с шагом 50
- Оператор держал кнопку 400 мс и отпустил. Что сделает скетч?
- Короткое нажатие — шаг по шкале без записи; запись происходит только по длинному удержанию, вместе с переходом к следующему параметру. Шагнёт по шкале, без записи
- В ячейке при старте читается 255. Что сделает загрузка?
- 255 — признак свежей памяти. Загрузка подставляет дефолтное деление, а строка в мониторе отделяет первый запуск от штатного старта. Возьмёт дефолтное деление и напечатает об этом
- Почему клик считается по отпусканию, а не по нажатию?
- Длинное нажатие начинается с того же фронта, что и короткое. Счёт по отпусканию с проверкой длительности разделяет их без второго таймера. Иначе каждое длинное удержание сначала срабатывает кликом и меню двоится