Обогреватель в моём гараже держит плюс восемь всю зиму, и порог срабатывания регулярно крутит каждый, кто заходит: сосед занёс рассаду — поставил пятнадцать, приехал в июне тесть — выкрутил тридцать. В феврале я приехал к луже и запаху прелого картона: кто-то оставил порог на плюс четыре, датчик видел шесть, реле честно молчало. Сидеть с ноутбуком в гараже и перешивать скетч ради одной цифры — самообман. Проблема не в скетче: у прибора просто нет кнопки «верни как было с завода».
Тупик, в который я заехал первым делом, — сброс на короткое нажатие. Работало ровно до дня, когда я переносил плату на новую DIN-рейку и положил её кнопкой на стол: одно касание стёрло неделю настроек. После этого сформулировал правило: сброс — настолько разрушительное действие, что оно обязано требовать намерения. Намерение — это три секунды удержания без расслабления, а не щелчок мимо.
Здесь соберём заводской сброс целиком: заводские значения лежат константами в скетче, удержание кнопки три секунды отсчитывает millis, бип подтверждает факт записи, а прибор сразу живёт по новым-старым цифрам. Разберём три пути мягкого рестарта и защиту от случайного зажатия — от первой секунды отсчёта до комбинации кнопок.
Отдельная прелесть схемы: дефолты — часть скетча, поэтому обновление заводских цифр — это правка строки и перезаливка. А вот сброс возвращает именно скетчевые дефолты, даже если вы поменяли их со дня последней прошивки: пользователь получает то, что вы считаете правильным сегодня, а не то, что было зашито полгода назад.
В симуляторе EEPROM не эмулируется — модель памяти это массив прямо в скетче. Логика сброса при этом честная: накрутите значения тумблером в мусор, зажмите кнопку на три секунды, услышите бип и посмотрите, как модельная память возвращается к заводским цифрам.
Дефолты живут в скетче, а не в памяти
Заводские значения на самом деле не заводские: их придумали вы, и место им — в прошивке. Я держу их парой массивов: адреса ячеек и их значения, и две колонки держатся параллельно — добавить настройку значит добавить её в оба массива, иначе адреса разъедутся. Смена дефолта — правка одной строки и перезаливка скетча, а то, что пользователь накрутил поверх, лежит в EEPROM независимо. Отличать чистую память от накрученной при первом запуске мы договорились в уроке про первые настройки: там magic-байт, здесь — та же логика, только вместо молчаливой подстановки — громкая процедура сброса.
Цена вопроса в циклах — приятная. Пять настроек — пять ячеек, один сброс — пять циклов записи из ста тысяч на каждую ячейку. Даже тысяча сбросов за год съест пять тысяч циклов, то есть около пяти процентов ресурса. По времени то же самое: пять ячеек по 3,3 мс — семнадцать миллисекунд занятого процессора один раз в голубой луне, замерить без осциллографа сложно. Так что износ тут не аргумент; аргумент — доверие: прибор, который молча забывает настройки от случайного касания, пользователь разберёт и уберёт в ящик. Настоящая цена сброса измеряется секундами удержания, а не циклами — арифметику ресурса я считал в уроке про износ.
Почему я записываю дефолты явно, а не стираю magic-байт и не жду, что setup() подставит заводское сам? Вариант со стиранием короче на строчку, но распыляет ответственность: одно место стирает, другое где-то в setup подставляет. Явная функция applyDefaults собирает все заводские цифры в одном месте — через полгода читаю её как документацию на собственный прибор.
Три секунды, бип и мягкий рестарт
#include <EEPROM.h> const int knopka = 2; // INPUT_PULLUP: зажата = LOWconst int zummer = 8; // бип подтвержденияconst int led = 12; // индикатор прогресса удержания const int adr[3] = {10, 11, 12}; // порог, гистерезис, режимconst byte defolt[3] = {80, 10, 1}; // заводские значения unsigned long tNazhatiya = 0;bool sbrosheno = false; void applyDefaults() { for (int i = 0; i < 3; i++) { EEPROM.update(adr[i], defolt[i]); // пишет только отличия }} void setup() { Serial.begin(9600); pinMode(knopka, INPUT_PULLUP); pinMode(zummer, OUTPUT); pinMode(led, OUTPUT); Serial.println("start");} void loop() { if (digitalRead(knopka) == LOW) { // первая секунда работы не считается: придавленная при // подаче питания кнопка не должна стирать настройки if (tNazhatiya == 0 && millis() > 1000) { tNazhatiya = millis(); } long derzhu = 0; if (tNazhatiya > 0) { derzhu = millis() - tNazhatiya; } if (derzhu > 1000) { digitalWrite(led, HIGH); // секунда прошла - прибор заметил } if (derzhu >= 3000 && !sbrosheno) { applyDefaults(); tone(zummer, 1000, 400); // бип ПОСЛЕ записи, не до Serial.println("defaults restored"); sbrosheno = true; // рестарт: обновите рабочие переменные или watchdog (см. ниже) } } else { tNazhatiya = 0; // отпустил - отсчёт сгорает sbrosheno = false; digitalWrite(led, LOW); }}Весь тайминг держится на одной паре переменных. tNazhatiya запоминается на фронте нажатия и сгорает при отпускании: продержал две секунды и устал — отсчёт пошёл заново, память не тронута. Светодиод загорается после первой секунды: пользователь видит, что прибор его заметил, но ещё ждёт. Отсчёт без delay — привычный millis-подход, loop не замирает и продолжает крутить остальную логику; фронт же ловится проверкой tNazhatiya == 0, а если хочется строгости по дребезгу, добавляю приём из урока про антидребезг.
Бип ставьте после applyDefaults, а не до: звук до записи — обещание, звук после — подтверждение. Между ними около пяти миллисекунд на три ячейки: update пропускает совпадающие значения, а каждая реальная запись занимает свои 3,3 мс. Пользователь такой паузы не заметит, зато подтверждение становится абсолютным. Бипа в проекте нет — подтверждайте миганием: три вспышки после сброса читаются не хуже.
И ещё одна вещь, о которую я спотыкался не раз: после сброса прибор обязан сразу жить по заводским цифрам. Если рабочие копии настроек лежат в переменных, applyDefaults должна обновить и их — иначе прибор до рестарта будет греть по старому порогу, хотя в EEPROM уже заводские значения. Симптом такого «полусброса» я ловил на приборе с шестью настройками: пять переприсвоил, шестую — гистерезис — забыл, и пользователь неделю наблюдал реле, которое включается вплотную к порогу, хотя по документации зазор должен быть градус. Достаточно одного такого письма от пользователя, чтобы навсегда повесить переприсвоение внутрь той же функции, что пишет память.
После записи дефолтов логике прибора нужно перечитать настройки, и здесь три пути. Первый, мой любимый: никаких рестартов — applyDefaults обновляет и EEPROM, и рабочие переменные, прибор продолжает работать, пользователь слышит бип. Второй — рестарт через watchdog: взводим собаку на полсекунды и уходим в глухой цикл, через 500 мс плата стартует как после выключения, setup() читает уже заводские ячейки. Третий — прыжок на нулевой адрес через указатель на функцию: на форумах его любят, я нет — стек и периферия остаются как были, и половина библиотек после такого «рестарта» живёт странно. Watchdog для того и придуман, тайминги его я описывал в уроке про watchdog и сон.
Кстати, обычный while(true) без собаки — это не рестарт, а повешенный прибор: loop не вернётся, setup не перепрочтётся, и только Reset или выдёргивание вилки его спасут. Watchdog именно за тем и стоит в связке: он превращает намеренное зависание в полноценный старт с обнулением всей оперативной памяти — то, что нужно после сброса настроек.
Watchdog-вариант помещается в две строки — держите их под рукой для приборов с длинным списком настроек, где переприсваивать каждую переменную руками лень и рискованно забыть одну из них.
#include <avr/wdt.h> // вместо рестарта из середины loop():wdt_enable(WDTO_500MS); // собака на 500 мсwhile (true) { } // ждём укус - плата перезапустится целиком| Способ сброса | Защита от случайности | Когда я его беру |
|---|---|---|
| Короткое нажатие кнопки | никакая: стирается от любого касания, включая перенос платы | ни для настроек, только для режима-галочки |
| Удержание одной кнопки 3 с | хорошая: случайно задеть сложно, устал руку раньше — отсчёт сгорел | кнопка одна, сброс редкий, панель пустая |
| Комбинация двух кнопок 3 с | очень высокая: двумя пинами мимо не попадёшь | на панели уже есть кнопки — комбинация достаётся бесплатно |
| Сброс командой из приложения | высокая, но нужна связь и подтверждение в интерфейсе | прибор с Wi-Fi и настройками через меню |
- 1. Сколько циклов записи тратит один заводской сброс прибора с пятью настройками?
- 2. Пользователь держал кнопку две секунды и отпустил. Что сделает скетч?
- 3. Почему бип подтверждения ставят после applyDefaults, а не до?
- 4. Плата лежит в корпусе с придавленной кнопкой сброса, проверка кнопки сделана в setup(). Что случится при подаче питания?
- 5. Какой вариант перезапуска после сброса я советую по умолчанию?
- Заводские значения — константные массивы в скетче; сброс — функция applyDefaults: пишет их через update и обновляет рабочие переменные
- Удержание три секунды: таймер на millis стартует на фронте нажатия и сгорает при отпускании — случайное касание не записывает ничего
- Первая секунда после подачи питания не считается: придавленная при включении кнопка не должна стирать настройки
- Подтверждение после записи: бип или мигание говорят о факте, а не о намерении
- Рестарт: обычно хватает переприсвоения переменных; запасной путь — watchdog на 500 мс и глухой цикл, прыжок на адрес 0 я не использую
Сброс готов, но в гаражном приборе он — только половина выживания. Вторая половина случается, когда никто не трогал ни кнопок, ни настроек: мигнуло питание — и термостат проснулся с пустой головой и заводским порогом. Как собрать настройки прибора в одну структуру, чтобы переживали не только выключение, но и сбой посреди работы, — следующий урок про термостат с памятью.
Частые вопросы по теме урока
- Сколько циклов записи тратит один заводской сброс прибора с пятью настройками?
- Сброс — обычные записи через update: пять ячеек, пять циклов против ста тысяч на ячейку. Даже тысяча сбросов съедает около пяти процентов ресурса, а по времени это семнадцать миллисекунд занятого процессора. Около пяти: по одному update на каждую ячейку с отличающимся значением
- Пользователь держал кнопку две секунды и отпустил. Что сделает скетч?
- Отпускание обнуляет таймер удержания. Намерение не подтвердилось — память не тронута, ничего не записано и не подтверждено. Держать придётся все три секунды заново. Ничего: отсчёт сгорел, чтобы сбросить, надо держать заново
- Почему бип подтверждения ставят после applyDefaults, а не до?
- Запись трёх ячеек занимает около пяти миллисекунд. Бип после записи подтверждает факт; бип до — только намерение. Между обещанием и его исполнением всегда может пролезть отключение питания. Запись занимает миллисекунды, и бип до неё обещал бы то, чего ещё нет
- Плата лежит в корпусе с придавленной кнопкой сброса, проверка кнопки сделана в setup(). Что случится при подаче питания?
- Зажатая при старте кнопка проходит проверку в setup() — и стирание срабатывает при каждом включении. Поэтому сброс держат в loop() с таймером удержания и отсрочкой первой секунды. Настройки сотрутся при каждом включении
- Какой вариант перезапуска после сброса я советую по умолчанию?
- Рестарт чаще всего не нужен: applyDefaults обновляет и EEPROM, и рабочие переменные, и прибор продолжает работу без единого разрыва. Watchdog — запасной путь для длинных списков настроек, где легко забыть одну переприсвоенную цифру. Никакого рестарта: обновить дефолты и рабочие переменные и продолжать жить