Гирлянда на кухонной полке работала месяц, и всё это время меня раздражала одна мелочь: после любого моргания света она просыпалась на первом эффекте. Вечер перестроен под комету, выключил на ночь, утром включил — и снова радуга с нуля, листай заново. Кнопка и так стояла, листание работало, не хватало одной ячейки: записать номер эффекта в энергонезависимую память и читать его при старте. В этом уроке добавляем именно её — и попутно разбираем три ловушки, из-за которых такие сохранялки умирают за первые месяцы жизни.
План такой: кнопка на D2 листает четыре эффекта по фронту — радуга, свеча, комета, тишина; номер пишется в ячейку ноль вызовом EEPROM.update по событию листания; при старте скетч читает ячейку и включает тот же эффект. Почему update, а не write — отдельная глава: у ячейки ресурс около ста тысяч циклов записи, и разница между «пишем всегда» и «пишем при изменении» — это разница между часом и годами работы. И третья глава — мусор: свежая плата отвечает из ячейки байтом 255, и без проверки диапазона скетч попробует включить эффект номер двести пятьдесят пять.
Кнопочная механика здесь нарочно минимальная — только фронт, без различения клика и удержания: полный пульт с антидребезгом и яркостью на удержании разобран в уроке про кнопочный пульт гирлянды. Соль этого урока в памяти, и всё, что не относится к ней, я выкинул без жалости. Эффекты взяты готовые — свеча и радуга из соседних уроков, чтобы леса не заслоняли здание.
Фронт листает, событие пишет
Листание по фронту — пара «прошлое состояние — текущее»: эффект сменяется в момент, когда кнопка переходит из HIGH в LOW, и один клик даёт ровно один шаг. По уровню нельзя: пока палец держит кнопку, скетч насчитает десятки переключений. Антидребезг в симуляторе не виден — виртуальная кнопка даёт чистый фронт, — но на живой плате контакты стучат 5-20 миллисекунд, и фильтр на 30 мс обязателен; механика стука и пороги разобраны в том же уроке про пульт. Здесь я оставляю честную оговорку в коде-комментарии и не нагружаю скетч.
Запись в память привязана к тому же фронту, и это ключевая дисциплина всего урока: EEPROM получает номер эффекта только при его смене — десятки раз за вечер, а не тысячи раз за минуту. Классическая беда — перенести запись в loop() без условия: скетч честно пишет «режим 2» тридцать раз в секунду, и ресурс ячейки выгорает, о чём рассказывает отдельная глава ниже. Тот же принцип записи по событию, а не по кадру, работает и в ночнике с тремя режимами, и в любой сохранялке настроек: событие «пользователь что-то выбрал» — единственный законный повод потратить цикл записи.
Чтение при старте дополняется проверкой. Свежая плата, где ни разу не писали, отвечает байтом 255 из каждой ячейки — физически это стёртое состояние флеш-подобной памяти. Без проверки скетч возьмёт 255 как номер эффекта, промотает все if и оставит ленту в тьме — а вы полчаса будете искать обрыв данных, которого нет. Проверка диапазона — одна строка: номер больше трёх, значит памяти ещё не было, ставим нулевой эффект и сразу фиксируем его записью. Та же дисциплина с дефолтами подробно разобрана в уроке про магическое число.
#include <FastLED.h>#include <EEPROM.h> #define LED_PIN 6#define NUM_LEDS 16#define MODES 4 // радуга, свеча, комета, тишина CRGB leds[NUM_LEDS];int mode = 0;int hue = 0;int head = 0;int prevBtn = HIGH;unsigned long lastFrame = 0; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(150); FastLED.clear(); FastLED.show(); pinMode(2, INPUT_PULLUP); Serial.begin(115200); int saved = EEPROM.read(0); if (saved >= MODES) { // свежая плата отвечает 255: памяти ещё не было saved = 0; } mode = saved; EEPROM.update(0, mode); // фиксируем дефолт один раз, дальше пишем по событию Serial.print("start mode: "); Serial.println(mode);} void loop() { int btn = digitalRead(2); if (prevBtn == HIGH && btn == LOW) { // фронт: один клик — один шаг mode = (mode + 1) % MODES; EEPROM.update(0, mode); // пишем по событию листания, не по кадру Serial.print("mode: "); Serial.println(mode); } prevBtn = btn; if (millis() - lastFrame < 30) { return; } lastFrame = millis(); if (mode == 0) { // радуга hue = (hue + 3) % 256; fill_rainbow(leds, NUM_LEDS, hue, 16); } if (mode == 1) { // свеча fadeToBlackBy(leds, NUM_LEDS, 30); if (random(0, 100) < 25) { leds[random(0, NUM_LEDS)] = CHSV(30, 150, random(120, 220)); } } if (mode == 2) { // комета fadeToBlackBy(leds, NUM_LEDS, 45); leds[head] = CHSV(0, 200, 255); head = (head + 1) % NUM_LEDS; } if (mode == 3) { // тишина FastLED.clear(); } FastLED.show();}Обратите внимание, где в скетче два вызова update. Первый — в setup(), после проверки диапазона: он фиксирует дефолт на свежей плате, чтобы следующий старт уже читал валидный номер. Второй — внутри фронта кнопки, и это единственное место в loop(), где EEPROM трогается вообще. Если позже добавите в гирлянду яркость на удержании, как в пульте из соседнего урока, — она запишется тем же способом, по событию отпускания, и в соседней ячейке с адресом 1. Порядок адресов и подрастание настроек — тема урока про EEPROM.put и get: структура вместо россыпи read/write экономит нервы, как только настроек становится больше двух.
Каждая ветка эффекта начинается с очистки или затухания — это старое правило сцен из урока про ёлку с кнопкой, и здесь оно же делает переключение чистым: после кометы радуга не вспыхивает поверх огарков. Режим «тишина» — единственный, кто вызывает clear() каждый кадр: можно было бы гасить один раз по флагу, но для урока яснее выглядит одинаковость веток. На скорость не влияет: clear() — это просто запись нулей в шестнадцать ячеек буфера, доли микросекунды.
update против write: арифметика ресурса
У ячейки EEPROM ATmega328P ресурс около ста тысяч циклов записи — цифра из даташита, и она ни при чём к часам или дням: считать нужно не время, а количество записей. Проставьте write в каждый кадр при тридцати кадрах в секунду — лимит выбирается за час с небольшим, и ячейка начнёт молча терять записанное. Тот же write по событию листания — пара сотен записей за зиму, и ресурса хватит на десятилетия. Разница между «работает год» и «умирает за час» сидит целиком в месте вызова, а не в выборе функции.
EEPROM.update — write с одной добавкой: перед записью он читает ячейку и физически пишет только тогда, когда значение отличается. Повторная запись того же номера эффекта с update не тратит ни одного цикла — ячейка бездействует. Зачем это в нашем скетче, если запись и так по событию? Подстраховка на будущее: при добавлении второй сохранялки в тот же loop() легко получить повторные записи одного значения из разных веток, и update делает такой промах бесплатным. На живой плате update ещё и быстрее в случае совпадения — чтение занимает микросекунды против 3,3 миллисекунды полной записи по даташиту.
И про мусор сверх 255-й ячейки: диапазонная проверка при старте страхует не только свежую плату, но и будущие версии скетча. Когда-нибудь вы добавите пятый эффект, потом удалите третий, а в памяти останутся записанные старыми версиями номера — без проверки скетч включит несуществующий режим и погаснет в тьму. Правило простое: любое число, прочитанное из EEPROM, валидно, только если попало в текущий диапазон; всё остальное — дефолт и немедленная запись дефолта. Так скетч переживает и смену количества эффектов, и чужую плату, и сбойную запись.
Сама память на Uno скромная: 1024 байта, адреса от нуля, один байт на ячейку — любой номер и любой уровень яркости влезают с запасом. Рядом с номером эффекта в ячейке ноль легко живут соседние настройки: яркость в адресе один, скорость анимации в адресе два, — лишь бы каждая оставалась байтом и имела свою ячейку. Кладут туда и счётчики включений, и последний выбранный цвет. Как только захочется хранить структуру больше байта, приходит EEPROM.put и get — они разобраны в уроке про ночник, ссылка ниже.
Память в симуляторе
Листайте кнопкой и смотрите Serial: каждый шаг печатает mode, и в момент смены происходит единственная запись в память. Честная оговорка про симулятор: его EEPROM пуст при каждом запуске скетча и пустая ячейка отвечает нулём, тогда как живая плата отвечает 255 — обе ситуации закрывает одна проверка диапазона, но увидеть «режим вернулся после перезагрузки» можно только на настоящей плате: запишите режим, выдерните вилку, включите — Serial напечатает тот же start mode.
Второе, на что стоит посмотреть в симуляторе, — поведение эффектов при листании назад по кругу: после тишины снова радуга, и она выстраивается с чистого буфера без огарков. Если позже добавите эффект, рисующий поверх прежнего кадра, — придётся решать, затухание ему нужно условное или нет; эта вилка разобрана в уроке про пульт, там же — идея листания в обе стороны второй кнопкой. А как только гирлянда доживёт до корпуса, ячейка с номером станет её характером: прибор включается не «как в скетче», а «как его выключили» — мелочь, которая делает самоделку прибором.
Куда развивать, когда база надоест: вторая кнопка на D3 листает назад — к радуге не крутиться через весь круг; яркость эффектов переезжает в ячейку один тем же update по событию удержания; для ёлочной гирлянды стоит добавить автоцикл — нет нажатий минуту, значит включаем перебор, и ячейка помнит, с чего перебор начинать. Всё это — те же три слова: событие, диапазон, update. Полный список идей сохранения настроек живёт в разделе про EEPROM — от дефолтов до счётчиков износа.
| Способ сохранения | Когда пишет | Что будет с ресурсом |
|---|---|---|
| write из каждого кадра | 30 раз в секунду | 100 000 циклов выгорают за час; кадр толстеет на 3,3 мс записи |
| write по событию | десятки раз за вечер | Годы работы; повтор того же значения всё равно стирает-пишет |
| update по событию | десятки раз за вечер | Тот же ресурс, но повтор значения не тратит ни цикла |
| put/get структуры | по событию | Растёт вместе с настройками: механика в уроке про put и get |
- 1. Что ответит ячейка EEPROM на плате, где ещё ни разу не писали?
- 2. Почему запись номера эффекта живёт в обработчике фронта, а не в loop()?
- 3. Чем EEPROM.update отличается от EEPROM.write?
- 4. Скетч знает четыре эффекта, в EEPROM записана пятёрка от старой версии. Что сделает проверка диапазона?
- 5. Сколько длится одна полная запись ячейки EEPROM по даташиту?
- Фронт кнопки «прошлое — текущее»: один клик — один шаг и одна запись в память
- EEPROM.update(0, mode) по событию; write из каждого кадра выжигает 100 000 циклов за час
- update физически пишет только при изменении: повтор того же значения бесплатен
- Свежая плата отвечает 255: проверка диапазона при старте плюс запись дефолта обязательны
- В симуляторе EEPROM пуст при каждом запуске; проверка памяти — выдернуть вилку и включить снова
Частые вопросы по теме урока
- Что ответит ячейка EEPROM на плате, где ещё ни разу не писали?
- Стёртая ячейка читается как 255. Без проверки диапазона скетч попробует включить эффект номер 255 и уйдёт во тьму. 255 — стёртое состояние памяти
- Почему запись номера эффекта живёт в обработчике фронта, а не в loop()?
- Ресурс — около 100 000 циклов на ячейку: при 30 кадрах в секунду лимит выбирается за час. Пишем только при смене эффекта. Запись по событию тратит циклы десятками за вечер; из loop — тридцать раз в секунду, ресурс выгорает за час
- Чем EEPROM.update отличается от EEPROM.write?
- Повторная запись того же значения с update не тратит ни одного цикла ресурса; write стирает и пишет всегда. Читает ячейку и физически пишет только при изменении значения
- Скетч знает четыре эффекта, в EEPROM записана пятёрка от старой версии. Что сделает проверка диапазона?
- Любое число вне текущего диапазона — дефолт и немедленная запись. Так скетч переживает смену количества эффектов. Поставит дефолт и запишет его в ячейку
- Сколько длится одна полная запись ячейки EEPROM по даташиту?
- 3,3 мс на запись: write из каждого кадра не только жжёт ресурс, но и отъедает у кадра больше времени, чем show() на 16 пикселях. Около 3,3 миллисекунды