Ночник в детской я собрал за вечер: потенциометр задаёт яркость, плата управляет светодиодом. Всё работало до первого отключения... и после него тоже работало — только каждый раз с завода. Жена выставила уютные 30%, я выключил удлинитель, включил снова — и ночник ослепил всех на половину яркости. «Кто трогал настройки?» — спрашивал я у платы. Никто не трогал. Плате просто негде было помнить: переменная с яркостью жила в оперативной памяти и умирала вместе с питанием.
Решение я знал: EEPROM — энергонезависимая память внутри самого чипа. Вписал сохранение яркости... и тут же изобрёл вторую ошибку: вызвал EEPROM.write() прямо в loop() без всяких условий. Каждый проход цикла — запись в ячейку, десятки раз в секунду. Через сутки с небольшим ячейка выдохлась: значение перестало меняться, потенциометр крутился вхолостую. Ресурс EEPROM — примерно 100000 циклов записи на ячейку, и это не теория: при записи десять раз в секунду стопроцентный износ наступает меньше чем за три часа.
Обе ошибки лечатся двумя приёмами: писать только когда значение действительно изменилось, и использовать update() вместо write(). В этом уроке разберём устройство EEPROM на Uno — 1024 байта, адреса, скорость, ресурс — и соберём ночник, который помнит выставленную яркость через сколько угодно выключений.
Симулятор поддерживает EEPROM.read, EEPROM.write, EEPROM.update и EEPROM.length(), причём значения сохраняются между запусками — как у настоящей платы. Так что эксперимент «выключил — включил — настройки на месте» можно проделать прямо здесь, не вставая со стула.
Три памяти Arduino: Flash, SRAM и EEPROM
В Arduino Uno три вида памяти, и у каждого своя работа. Flash, 32 КБ, — там лежит сам скетч; он сохраняется десятилетиями, но писать в него из программы не принято и небезопасно. SRAM, 2 КБ, — оперативная память: переменные, стек, буферы; молниеносная и полностью обнуляется при потере питания. EEPROM, 1 КБ — те самые 1024 байта, — отдельная область, которая помнит данные без питания годами и доступна программе для чтения и записи.
Числа у разных плат разные, и это стоит знать до покупки: у Uno и Nano — 1024 байта EEPROM, у Mega — 4096, у Leonardo — 1024. Проверить можно программно: EEPROM.length() вернёт точное значение, и код не придётся править при переносе на другую плату. Для сравнения: у ESP32 отдельной EEPROM нет, там она эмулируется во флеше — но это тема другого раздела.
1024 байта — это адреса от 0 до 1023. В каждой ячейке живёт один байт — число от 0 до 255. Больше в одну ячейку не влезет физически: попытка записать 700 молча сохранит остаток от деления на 256, то есть 188. Всё многобайтовое — int, long, float — приходится раскладывать по соседним ячейкам, и мы сделаем это руками в следующем уроке.
Что реально живёт в EEPROM: настройки пользователя (яркость, громкость, целевая температура), калибровочные коэффициенты (сдвиг нуля датчика), счётчики событий, последний выбранный режим. Что не живёт: журналы измерений каждую секунду — их место на SD-карте, и большие массивы — 1024 байта это примерно полторы страницы текста, а не хранилище.
#include <EEPROM.h> const int addrBright = 0; // ячейка с яркостью void setup() { Serial.begin(9600); pinMode(9, OUTPUT); int saved = EEPROM.read(addrBright); if (saved == 255) { // новая EEPROM: пусто saved = 128; // разумный заводской дефолт } analogWrite(9, saved); Serial.print("Восстановлена яркость: "); Serial.println(saved);} void loop() { int pot = analogRead(A0) / 4; // 0..1023 превращаем в 0..255 int saved = EEPROM.read(addrBright); if (abs(pot - saved) > 2) { // ручку реально крутили EEPROM.update(addrBright, pot); analogWrite(9, pot); Serial.print("Сохранена яркость: "); Serial.println(pot); } delay(100);}Смотрим на setup(). EEPROM.read(0) возвращает байт из ячейки с адресом 0. На совершенно новой плате там лежит 255: пустая EEPROM — это все единичные биты. Поэтому перед использованием значение проверяют: если 255 — это первый запуск, кладём разумный дефолт 128. Без этой проверки ночник после прошивки засветится на полную яркость.
В loop() сравниваем текущее положение ручки (analogRead даёт 0–1023, делим на 4 — получаем 0–255, ровно байт) с сохранённым. Записываем только если разница больше двух: гистерезис отсекает дрожание ручки и холостые записи. abs() — встроенная функция модуля, в симуляторе она тоже есть.
Само сохранение — EEPROM.update(адрес, значение). Update отличается от write тем, что сначала читает ячейку и пишет только при отличии. Для нас это двойная страховка: значения мы и так сравнили руками, а update подстрахует, если где-то в другом месте кода проскочит повторная запись с тем же значением.
analogWrite(9, saved) в setup() — момент истины: светодиод загорается сразу с той яркостью, которую пользователь выставил в прошлый раз. Никакого «заводского полтинника», никакой потери настроек. Один байт — одна ячейка — одна настройка: так выглядит идеальный житель EEPROM.
write() против update(): срок жизни ячейки
Каждая запись в ячейку — физический процесс: заряд перекачивается через тонкий оксидный слой, и тот понемногу деградирует. Производитель ATmega328P гарантирует около 100000 циклов записи на ячейку. Чтение ресурса не тратит вовсе — читайте хоть миллион раз в секунду. Убивает именно запись, причём каждая, даже запись того же самого значения.
Прикинем на числах, потому что от них зависит архитектура проекта. Запись раз в секунду: 100000 секунд — это 27,8 часа, меньше полутора суток. Раз в минуту — около 69 дней. Раз в час — почти 11,4 года. Раз в день — 274 года. Видите, как одна цифра периода меняет срок жизни на порядки? Ресурс EEPROM — это всегда вопрос частоты записи, а не вопрос «хватит или нет».
Моё рабочее правило: по умолчанию пишу update() — и только когда значение гарантированно изменилось и лишняя проверка бессмысленна (например, сразу после сравнения, как в нашем ночнике), позволяю себе write(). Условие простое: если сомневаетесь — update, ошибки он не сделает никогда.
Про скорость тоже честно: одна запись занимает около 3,3 мс, и всё это время процессор занят ею. Для сохранения настройки раз в минуту — незаметно. Для цикла, который должен тикать каждые 100 микросекунд, — катастрофа. Ещё одна причина писать редко и по делу.
Что хранить в EEPROM, а что — нет
Железный список «да»: настройки, которые меняет человек (яркость, громкость, температура на термостате), калибровки (сдвиг нуля весов, коэффициент датчика), счётчики тихих событий (включения платы, циклы фильтра), последний режим работы. Всё это меняется редко — от раза в минуту до раза в месяц, — и каждая ячейка переживёт годы такой работы.
Список «нет» такой же железный: потоковые данные (логи и показания каждую секунду — их место на SD-карте), большие массивы (1024 байта — это блокнот настроек, а не архив), секреты (пароли из EEPROM считываются программатором за минуту — это не сейф) и всё, что требует записи чаще раза в минуту.
Из практики: я держу в EEPROM «служебный блок» в начале памяти — контрольный байт, версию настроек, потом сами значения. При смене прошивки, если раскладка адресов меняется, версия подскажет, что старые настройки надо сбросить, а не читать наугад. И заведите карту памяти — комментарий в шапке скетча или блокнот со списком «адрес — что лежит»: после полугода доработок без неё скетч не прочтёт сам себя.
И про симулятор: EEPROM здесь ведёт себя как настоящая. Запустите скетч, покрутите потенциометр A0, остановите выполнение и запустите снова — в Serial придёт восстановленная яркость, а не заводская. Память симулятора сохраняется между запусками, ровно как у реальной платы между отключениями питания.
- Замените update() на write() и добавьте счётчик: печатайте в Serial каждый факт записи. Убедитесь, что write дёргается чаще, — именно так тратится ресурс впустую.
- Уберите проверку if (saved == 255) и выставьте A0 в максимум перед запуском: подумайте, что вернёт EEPROM.read на чистой плате и что из этого сделает analogWrite.
- Прочитайте первые десять ячеек в цикле for (int i = 0; i < 10; i++) и напечатайте их значения до первого сохранения: почти везде будут 255.
- Реализуйте magic-байт из совета выше: при первом запуске пишите дефолт 128 в ячейку 0 и 170 в ячейку 100, при следующих — только проверяйте контрольное число.
- EEPROM на Uno — 1024 байта (адреса 0–1023) внутри ATmega328P; у Mega — 4096; размер возвращает EEPROM.length()
- Байт = 0–255; пустая новая EEPROM читается как 255 — проверяйте и подставляйте дефолты
- EEPROM.read(addr) — чтение (ресурс не тратит); EEPROM.write(addr, val) — запись всегда; EEPROM.update(addr, val) — запись только при отличии
- Ресурс ~100000 циклов записи на ячейку: раз в секунду — 27,8 часа, раз в минуту — 69 дней, раз в час — 11,4 года
- Пишите по событию или при изменении значения; одна запись занимает около 3,3 мс
- Настройки и калибровки — в EEPROM; потоковые логи — на SD-карте
- 1. Сколько EEPROM у Arduino Uno?
- 2. Что вернёт EEPROM.read(50) на совершенно новой плате?
- 3. Чем EEPROM.update() отличается от EEPROM.write()?
- 4. Ячейка пишется раз в секунду. Когда она исчерпает ресурс 100000 циклов?
- 5. Какое значение влезает в одну ячейку EEPROM без ухищрений?
- 6. Что нужно сделать с аналоговым значением 0–1023 перед записью в EEPROM?
Один байт — уже настройка, которая переживает розетку. Но рано или поздно понадобится число побольше: счётчик включений, пройденные метры, минуты наработки. В следующем уроке будем хранить int в двух соседних ячейках — разрежем его на старший и младший байты через деление и остаток, соберём обратно и заодно напишем счётчик включений, который покажет, сколько раз вашей плате пропадало питание.
Пока — заведите привычку: перед тем как добавить в скетч ещё один EEPROM.write, спросите себя, как часто это будет происходить. Если ответ «каждый проход loop» — вернитесь и придумайте событие. Ваши ячейки скажут спасибо лет через десять.
Частые вопросы по теме урока
- Сколько EEPROM у Arduino Uno?
- Uno и Nano — 1024 байта (1 КБ). 4096 — у Mega, 32 КБ — это Flash со скетчем. Точный размер вернёт EEPROM.length(). 1024 байта
- Что вернёт EEPROM.read(50) на совершенно новой плате?
- Пустая EEPROM — все единичные биты, то есть 255. По этому признаку ловят первый запуск. 255
- Чем EEPROM.update() отличается от EEPROM.write()?
- write() перезаписывает ячейку всегда и тратит цикл записи; update() сравнивает и пишет только по делу. Пишет только если новое значение отличается от текущего — экономит ресурс ячейки
- Ячейка пишется раз в секунду. Когда она исчерпает ресурс 100000 циклов?
- 100000 секунд — примерно 27,8 часа. Отсюда правило: в EEPROM пишут по событию, а не по секундному расписанию. Через 27,8 часа
- Какое значение влезает в одну ячейку EEPROM без ухищрений?
- Ячейка хранит один байт. Многобайтовые типы раскладывают по соседним ячейкам вручную — этому посвящён следующий урок. Байт от 0 до 255
- Что нужно сделать с аналоговым значением 0–1023 перед записью в EEPROM?
- Ячейка принимает 0–255. analogRead 1023 / 4 = 255 — аккуратное сжатие без потери, заметной для яркости. Разделить на 4 (или сжать через map) до диапазона 0–255