EEPROM на Arduino: сохранение настроек после сброса питания
Каждый, кто первый раз собрал будильник или термостат на Arduino, ловил один и тот же облом: переменная с настройками живёт до первого сброса питания. Выключили адаптер — и будильник забыл и будить, и время, и всё, что вы ему задали. Причина простая: обычные переменные живут в ОЗУ, а ОЗУ на ATmega328P держит данные, только пока есть напряжение. Для «памяти, которая переживает выключение», на плате есть отдельная область — EEPROM, и у неё своя библиотека со своими граблями.
В нашем симуляторе EEPROM — это память самой платы Uno, не отдельная деталь из палитры «Макетка»: периферия с макетки подключается кликом, а энергонезависимую память скетч пишет и читает сам. Рантайм исполняет чтение, запись и update по-настоящему — прогон скетчей ниже снят с живого рантайма. Ниже — карта памяти, скетч, ресурс ячеек и честная граница: что в браузере проверить нельзя ни при каком старании.
Что такое EEPROM и сколько его на плате
EEPROM — electrically erasable programmable read-only memory: байт-адресуемая память, которая не стирается ни выключением, ни перезаливкой скетча. На Uno (ATmega328P) её 1 КБ — 1024 байта с адресами 0-1023, один вызов читает или пишет один байт. Для сравнения: флешь со скетчем — 32 КБ, ОЗУ с переменными — 2 КБ. EEPROM медленный (запись занимает около 3,3 мс на байт) и дорогой по ресурсу, поэтому его роль — редкие события: калибровка, выбранный режим, счётчик пусков, адрес устройства на шине.
- 1 КБ на Uno, адреса 0-1023: за границей нет защиты — запись по адресу 1024 затрёт неизвестно что или молча испортится.
- Ресурс ячейки — 100 000 циклов записи по даташиту ATmega328P; чтение ресурс не расходует вовсе.
- Без питания данные держат порядка 100 лет при 25 °C — не боятся ни розетки, ни батарейки.
- update(addr, val) пишет байт только если он отличается от лежащего — главный способ беречь ресурс.
- Перезаливка скетча EEPROM не трогает: настройки переживают новую прошивку — в этом его смысл.
Скетч: сохранить настройку и прочитать её обратно
#include <EEPROM.h> const byte ADDR_MODE = 7; // ячейка «режим устройства» void setup() { Serial.begin(9600); EEPROM.update(ADDR_MODE, 2); // пишет, только если там не 2 Serial.print("mode = "); Serial.println(EEPROM.read(ADDR_MODE)); // 2 — и после сброса питания тоже EEPROM.write(8, 0xAB); // прямая запись байта, без проверки Serial.print("eeprom size: "); Serial.println(EEPROM.length()); // 1024 на Uno} void loop() {}Прогон в симуляторе подтверждает ровно это поведение: update(7, 123) записал байт, повторный запуск скетча без записи прочитал из ячейки те же 123, length() вернул 1024. Кнопки «Запуск» и «Стоп», новая заливка скетча — память живёт; закрыли вкладку браузера — сессия кончилась, на новом сеансе плата чистая. На живой плате всё то же самое, только «новый сеанс» случается раз в годы, а не при каждом F5.
Честная граница: что исполняет симулятор
| Проверка | Симулятор | Только железо |
|---|---|---|
| read / write / update / length | исполняется по-настоящему | то же самое |
| put / get int и float | исполняется: put пишет 4 байта little-endian, трогает только изменившиеся | то же самое |
| put / get структур и массивов | частично: массив байтов — да, структуры — нет | пишет любой тип через sizeof |
| Ресурс 100 000 циклов | не эмулируется: память не изнашивается | да — износ ячеек реален |
| Держит данные без питания | нет: закрытие вкладки стирает сессию | да — годы по даташиту |
| Медленная запись 3,3 мс/байт | мгновенно | да — задержка на каждый байт |
Отдельно про put и get — с недавних пор их рантайм исполняет для чисел. put(addr, value) кладёт целое или дробное как 4 байта little-endian (int в браузере 32-битный, как long на плате) и пишет только изменившиеся байты — ресурс ячеек бережётся точно так же, как от update. get читает записанное обратно: скаляр возвращается присваиванием v = EEPROM.get(0, v); — это валидный C++, у живой библиотеки get возвращает ссылку на переменную; массив байтов get заполняет по-настоящему, поэлементно. Чего в браузере по-прежнему нет: структур (struct) интерпретатор не знает, их put/get с байтами полей — на реальной плате. Полный разбор put/get с CRC и первый пуск устройства — в уроке про настройки устройства, а карта всех 1024 байт — в уроке про память ATmega328.
Ресурс и грабли
Сто тысяч звучит как «вечность», пока не поделить. Запись раз в секунду убивает ячейку за 100 000 секунд — около 28 часов непрерывной работы. Та же ячейка с записью раз в минуту живёт почти два месяца, раз в час — пять лет. Отсюда два правила: update вместо write (не писать то же значение) и журнал с переносом точки записи — wear leveling, когда счётчик не сидит на одной ячейке, а кочует по кольцу. Разбор с формулами — в уроке про износ и wear leveling.
- EEPROM.write внутри loop() без условия: тысяча записей в секунду — и ячейка мертва к вечеру. Пишите по событию и через update.
- int занимает 2 байта, long и float — 4: разложите на байты сами (highByte/lowByte) или это делайте на живой плате через put.
- Адрес за 1023 — чекер границ в C++ не поймает: держите константы адресов в одном месте.
- Мусор от предыдущей жизни: EEPROM новой платы не пустая, там 0xFF или что-то чужое — ставьте magic number первого пуска.