Кнопку «сбросить настройки» я в первый раз повесил вплотную к кнопке режима. Через неделю позвонил заказчик: конфиг слетел, все уставки заводские. Разбор показал честную хронику: плата жила в тесной нише, кабель режима задевал кнопку сброса, и одно случайное касание за полсекунды вынесло месяц ручной работы. Переезд кнопки не спас бы надолго — спасла пауза: сброс теперь случается только после пяти секунд непрерывного удержания, и за пять секунд рука успевает подумать. Случайное касание даже не фиксируется как попытка.
Пять секунд — не магия, а компромисс, который я вывел на трёх устройствах: три секунды люди прожимают сами того не замечая, когда перетаскивают плату или чистят корпус, а десять — уже «наверное, сломалось». Пять коротки для намеренного и длинны для случайного. За эти секунды устройство обязано показывать, что процесс пошёл: мигающий светодиод — ваш соучастник, он превращает «сломалось» в «стоп, я же жму не туда».
Подход с удержанием у нас уже встречался: в уроке про кнопку возврата к дефолтам сброс срабатывал на трёх секундах, и для многих устройств этого хватает. Здесь я поднимаю планку и разбираю вопрос целиком: почему именно пять, что писать при сбросе, а что обходить стороной, и как сделать так, чтобы кнопка не стала дырой в устройстве размером с месяц работы.
Удержание как доказательство намерения
Механика на millis(): нажатие фиксируется фронтом, от него стартует таймер, отпускание в любой момент сгорает и не оставляет следов — приём тот же, что в уроке про millis вместо delay, только служит он не ритму, а безопасности. Порог входа в процесс — полсекунды: дребезг и короткое касание таймер не запускают вообще, механику контактов мы разбирали в уроке про антидребезг. Прогресс размечен: с полсекунды LED мигает медленно, с трёх секунд — вдвое чаще, на пятой блок перезаписывается и LED замирает включённым до отпускания кнопки.
Почему millis, а не delay и не прерывание — отдельный разговор. delay() превращает весь loop() в паузу: прогресс-мигание и отклик на отпускание вешать не на что. Прерывание на фронте кнопки даст метку старта, но длительность всё равно меряет millis() в loop, а внутри обработчика прерываний delay и Serial недоступны — не место там и для записи EEPROM, которая занимает миллисекунды и перебиваться не любит. Поэтому вся машина сброса — обычный код в loop(): таймер, пороги, LED — и ни одной паузы, которая могла бы замаскировать отпускание кнопки.
Пороги легко подстраивать под характер устройства, и именно поэтому они живут в константах, а не размазаны по коду. Для кофемашины с кнопкой на лицевой панели я оставил пять секунд; для поливного контроллера в амбаре поставил восемь — случайные руки туда не добираются, а намеренный сброс и так требует похода с фонарём. Нижний порог полсекунды поднимайте осторожно: выше секунды начинает казаться, что устройство игнорирует кнопку, и люди жмут по три раза подряд, создавая ровно тот хаос, от которого вас защищает пауза.
Два гвоздя, которые я забиваю в каждый такой сброс. Первый: первая секунда после подачи питания не считается — придавленная при включении кнопка не имеет права стирать настройки, иначе сборка в корпусе станет рулеткой. Второй: после записи блок подтверждается — LED замирает включённым, зуммер пикает, что угодно, но молчание после пяти секунд нажатия выглядит как «то ли сработало, то ли нет», и люди жмут ещё раз. Уже записанный блок перепишется во второй раз — не страшно, но циклы ячеек тратятся зря, и это наша вина, а не их.
Что писать при сбросе, а что не трогать
Сброс — это запись блока по той же раскладке, что и первый запуск: поля по заводским значениям, версия на место, сумма свежая. Функция заливки у меня одна на оба случая: и чистая память, и кнопка сброса вызывают её — меньше шансов, что заводские значения разъедутся между «первым включением» и «сбросом». Рабочие переменные после записи переприсваиваются из блока, рестарт не нужен: плата продолжает работать с заводскими прямо в этой сессии.
Не трогаю при сбросе служебное: счётчик включений, моточасы, заводской номер платы. Это история устройства, она копилась годами и никак не связана с тем, что пользователь хочет вернуть настройки. Полная зачистка через eeprom_clear — ноль во все 1024 ячейки, около 3,4 с блокирующей записи и килобайт потраченных циклов, арифметика износа собрана в отдельном уроке — остаётся для одного случая: плата уходит в чужие руки, и вы не хотите отдавать вместе с ней свою историю. Даже тогда я сначала руками вычищаю только блоки с настройками, а счётчики обнуляю осознанно, по одному.
Полезная мелочь: рядом с конфиг-блоком держите ячейку со счётчиком сработавших сбросов — вне блока, чтобы заливка его не задевала. Инкремент стоит один цикл записи и отвечает на неудобный вопрос «кто жмёт кнопку» без установки камеры: ноль за месяц — тишина, десятка за неделю — ищите сквозняк или соседа. Счётчик тихих событий мы уже складировали в уроке про счётчик включений — приём переезжает целиком, меняется только событие.
И про сами дефолты: они обязаны быть безопасными, а не нулевыми — тот же аргумент, что при первом запуске. Термостат вернётся к 21 градусу, реле — в выключенное состояние, порог — в середину диапазона. После сброса устройству положено быть скучным: работать по заводскому сценарию, который вы согласились бы оставить на неделю без присмотра. Моя проверка простая: если заводское состояние устройства пугает, дефолты подобраны плохо.
#include <EEPROM.h> const int KNOPKA = 2; // кнопка на GND, INPUT_PULLUPconst int LED = 12;const int RELE = 7; // Раскладка конфиг-блока та же, что в уроках про маркер и CRC:// 2 уставка, 3 гистерезис, 4 режим, 5 CRC8. Маркер и версия живут// в ячейках 0 и 1 и заливаются одной функцией с чистой памятью. int startHod = 0; // millis старта удержания, 0 — не нажатоint sdelano = 0; int crc8(int first, int last) { int c = 0; for (int a = first; a <= last; a++) { c = c ^ EEPROM.read(a); for (int b = 0; b < 8; b++) { if ((c & 128) > 0) { c = (c << 1) ^ 7; } else { c = c << 1; } c = c & 255; } } return c & 255;} void sbrosBlok() { EEPROM.update(2, 210); EEPROM.update(3, 2); EEPROM.update(4, 1); EEPROM.update(5, crc8(2, 4)); Serial.println("zapisano: defolty + svezhaya summa");} void setup() { Serial.begin(9600); pinMode(KNOPKA, INPUT_PULLUP); pinMode(LED, OUTPUT); pinMode(RELE, OUTPUT); digitalWrite(RELE, HIGH); // нагрузка включена: так и было до сброса Serial.println("derzhite knopku 5 sekund: sbros nastroek");} void loop() { int t = millis(); int nazhato = (digitalRead(KNOPKA) == LOW); if (nazhato && startHod == 0 && t > 1000) { startHod = t; // первая секунда после подачи питания не считается } if (startHod > 0) { int dlit = t - startHod; if (dlit >= 5000) { if (sdelano == 0) { sbrosBlok(); digitalWrite(RELE, LOW); // нагрузка — в заводское состояние sdelano = 1; } digitalWrite(LED, HIGH); // подтверждение: держится до отпускания } else if (dlit >= 3000) { digitalWrite(LED, (t / 100) % 2); // быстрее: почти готово } else if (dlit >= 500) { digitalWrite(LED, (t / 400) % 2); // медленно: нажатие принято } } if (nazhato == 0) { startHod = 0; // отпускание сгорает: ничего не записано if (sdelano == 0) { digitalWrite(LED, LOW); } } delay(10);}| Действие | Что трогает в памяти | Когда беру |
|---|---|---|
| Переприсвоить рабочие переменные | Ничего: EEPROM не пишется | Вернуть настройки текущей сессии без записи |
| Перезаписать блок дефолтами со свежей суммой | Только ячейки конфига, 4-6 циклов записи | Пользователь просит «как из коробки» |
| eeprom_clear: ноль во все ячейки | Весь килобайт, 1024 цикла, около 3,4 с блокировки | Плата уходит в чужие руки вместе с историей |
| Watchdog-рестарт после записи | Ничего сверх уже записанного | Рабочие переменные размазаны по функциям |
Тренажёр удержания
В симуляторе EEPROM заменён переменными — говорю прямо, — зато вся механика удержания настоящая: таймер на millis, пороги полсекунды, три секунды и пять, LED-прогресс. Кнопка на D2, светодиод на D12, реле на D7 играет роль нагрузки: после сброса она обязана вернуться в заводское выключенное состояние. Держите кнопку пять секунд и следите за частотой вспышек — она удваивается на середине, и это сделано специально: рука должна чувствовать, что процесс движется, а не завис.
Отпустите кнопку на третьей секунде — в мониторе не появится никакой записи о сбросе, и это правильно: таймер сгорел, ни один байт не пострадал. Повторите с полным удержанием — блок перезапишется заводскими значениями, реле щёлкнет в выключенное состояние, LED подтвердит факт до отпускания. На настоящей плате вместо переменных — EEPROM.update из code-блока выше, пороги и логика совпадают один в один.
Отдельно про железо: кнопка подключается между D2 и GND с INPUT_PULLUP, и без подтяжки неподтянутый пин читает наводки из воздуха — симулятор прощает, плата нет. Перед первым запуском проверьте обратное: отпущенная кнопка обязана давать HIGH, иначе машина сброса стартует сразу после подачи питания, и защита первой секунды останется единственным барьером между кнопкой и вашими настройками.
- Намерение отличает от случайности длительность: пять секунд непрерывного удержания, полсекунды порог входа, отпускание сгорает
- Прогресс обязателен: медленное мигание с полсекунды, вдвое быстрее с трёх секунд, включённый LED после записи
- Первая секунда после подачи питания игнорируется: придавленная кнопка не стирает настройки
- Сброс перезаписывает только конфиг-блок: поля по заводским, версия, свежая сумма; счётчики и номер платы не трогаются
- eeprom_clear — ноль во все 1024 ячейки, около 3,4 с и килобайт циклов: это зачистка перед передачей платы, а не сброс
- 1. Случайное касание кнопки сброса длится меньше полсекунды. Что записывает скетч из урока?
- 2. Почему первая секунда после подачи питания игнорируется?
- 3. Пользователь держал кнопку четыре секунды и отпустил. Что произойдёт?
- 4. Что из перечисленного НЕ перезаписывается при фабричном сбросе?
- 5. Зачем после записи блока мигать LED или пикать зуммером?
Маркер, сумма и сброс — три приёма, которые вместе закрывают почти всё, что случается с килобайтом настроек: целостность, подлинность раскладки и путь назад. Собирает их в одну систему итоговый урок раздела про настройки, которые живут годами — там же чек-лист раскладки памяти. Домашнее задание: добавьте в свой сброс второй уровень подтверждения — отпустить и нажать снова в течение двух секунд; перепишите таймеры, поживите с этим неделю и решите, стоит ли оно того. У меня после такой недели осталось одно нажатие в пять секунд: лишний шаг пользователи не прощают даже из благих соображений.
Частые вопросы по теме урока
- Случайное касание кнопки сброса длится меньше полсекунды. Что записывает скетч из урока?
- Порог входа в полсекунды отсекает дребезг и касания: таймер даже не стартует, EEPROM не трогается ни одним байтом. Ничего: процесс запускается только после полсекунды удержания
- Почему первая секунда после подачи питания игнорируется?
- Сборка в тесном корпусе легко придавливает кнопку: без этой секунды каждое включение могло бы обнулять конфиг. Таймер просто не стартует раньше. Придавленная при включении кнопка не должна стирать настройки
- Пользователь держал кнопку четыре секунды и отпустил. Что произойдёт?
- Запись привязана к пятой секунде непрерывного удержания, отпускание обнуляет попытку: ни одного байта не записано, LED погаснет. Ничего не записано: таймер сгорел при отпускании
- Что из перечисленного НЕ перезаписывается при фабричном сбросе?
- Счётчики и заводской номер — история устройства, а не настройки пользователя. Сброс перезаписывает только конфиг-блок: поля, версию, свежую сумму. Счётчик моточасов платы
- Зачем после записи блока мигать LED или пикать зуммером?
- Молчание после пяти секунд выглядит как сбой — начинаются повторные нажатия и лишние записи. Подтверждение отдаёт факт, а не намерение. Сказать пользователю о факте: без подтверждения люди жмут ещё раз