Основы · 14 мин чтения

EEPROM в Arduino: запись и чтение данных (read, write, update, put, get)

EEPROM в Arduino простыми словами: 1024 байта на Uno, команды read, write, update, put и get, ресурс в 100 000 записей, сохранение счётчиков и паролей, Preferences на ESP32.

EEPROM в Arduino — это небольшая постоянная память, которая хранит данные даже после отключения питания платы. В микроконтроллере ATmega328P на Arduino Uno, Nano и Pro Mini есть 1024 байта EEPROM, а работать с ней можно командами EEPROM.read(), EEPROM.write(), EEPROM.update(), EEPROM.put() и EEPROM.get() из библиотеки EEPROM, которая идёт вместе с Arduino IDE. ОЗУ, где живут переменные, очищается при каждом перезапуске, а настройка, счётчик или пароль, записанные в EEPROM, остаются на месте, когда плата снова включается.

В этом руководстве разберём, как устроена EEPROM, сколько её на каждой плате, чем отличаются команды и самый частый вопрос: что на практике значит ресурс примерно в 100 000 записей. Затем пошагово соберём счётчик, который помнит своё значение после отключения питания, структуру настроек, которая за один раз сохраняет пароль и параметры, и кольцевой буфер, увеличивающий ресурс записи в 16 раз. В последних разделах узнаешь, как сделать то же самое на ESP32, где настоящей EEPROM нет, с помощью библиотеки Preferences, а на Raspberry Pi Pico — через файловую систему MicroPython.

Arduino Uno: внутри микроконтроллера ATmega328P есть 1 КБ EEPROM
EEPROM на Arduino Uno — не отдельная микросхема: она находится внутри ATmega328P и хранит 1024 байта постоянных данных по адресам 0–1023.

Что такое EEPROM и как она работает?

EEPROM расшифровывается как Electrically Erasable Programmable Read-Only Memory (электрически стираемое перепрограммируемое ПЗУ). Каждая ячейка хранит информацию в виде электрического заряда, запертого в изолированном затворе, и этот заряд никуда не девается при отключении питания. Представь EEPROM на Uno как шкаф с 1024 ящиками: у каждого ящика есть адрес от 0 до 1023, и в него помещается ровно 1 байт, то есть число от 0 до 255. Ячейка, в которую ни разу не писали, читается как 255 (0xFF). Чтение почти мгновенное и не ограничено; запись занимает около 3,3 мс на байт и понемногу изнашивает ячейку. В даташите ATmega328P для каждой ячейки EEPROM указано не менее 100 000 циклов записи/стирания и 100 лет хранения данных при 25 °C.

ХарактеристикаFlash (память программ)SRAMEEPROM
Объём на Uno32 КБ (0,5 КБ занимает загрузчик)2 КБ1 КБ (1024 байта)
Что хранится?Загруженный скетчПеременные, массивыНастройки, счётчики, рекорды
При отключении питанияСохраняетсяСтираетсяСохраняется
Можно писать во время работы скетча?Обычно нет (через PROGMEM только чтение)Да, без ограниченийДа, через библиотеку EEPROM
Ресурс записиОколо 10 000Без ограниченийОколо 100 000 на ячейку
Скорость записиТолько при загрузке скетчаОдин тактОколо 3,3 мс на байт

Короче говоря, SRAM — это маркерная доска, а EEPROM — тетрадь. На доске можно писать и стирать сколько угодно, но когда гаснет свет, всё исчезает; написанное в тетради остаётся, но одну и ту же страницу нельзя стирать и переписывать бесконечно. Поэтому EEPROM идеальна для информации, которая занимает мало места и меняется редко: калибровочные значения, пользовательские настройки, яркость или громкость, рекорд в игре, код двери, зарегистрированные RFID-карты или последнее состояние лампы. Для больших и часто записываемых данных, например журнала температуры с записью каждую секунду, лучше подходит модуль SD-карты. Если интересно, почему ОЗУ так быстро заканчивается, прочитай статью Arduino: не хватает памяти.

Сколько EEPROM на каждой плате?

ПлатаМикроконтроллерEEPROMПримечание
Arduino Uno R3, Nano, Pro MiniATmega328P1024 байтаНастоящая EEPROM
Arduino Mega 2560ATmega25604096 байтНастоящая EEPROM
Arduino Leonardo, MicroATmega32U41024 байтаНастоящая EEPROM
Arduino Nano EveryATmega4809256 байтНастоящая EEPROM, но маленькая
Arduino Uno R4 Minima, WiFiRenesas RA4M18 КБФлеш для данных; библиотека EEPROM работает с теми же командами
ESP32ESP32 (Xtensa)НетРаздел NVS во флеш-памяти; рекомендуется Preferences
ESP8266 (NodeMCU, Wemos D1)ESP8266НетЭмуляция во флеш-памяти; нужны EEPROM.begin() и commit()
Raspberry Pi PicoRP2040НетВ MicroPython — файл, в ядре Arduino — эмуляция EEPROM во флеш-памяти

Если код будет переезжать с платы на плату, не прописывай размер EEPROM числом: EEPROM.length() возвращает реальный объём EEPROM платы (1024 на Uno, 4096 на Mega), так что один и тот же код очистки или вывода содержимого работает везде. Остальные подробности о памяти и пинах Uno есть в статье Пины Arduino Uno, а про выбор платы читай Какую Arduino купить?.

Команды EEPROM: read, write, update, put и get

Библиотека EEPROM входит в пакет платы, так что устанавливать ничего не нужно: достаточно добавить в начало скетча строку #include <EEPROM.h>. Адреса начинаются с 0 и на Uno заканчиваются на 1023. В таблице ниже кратко описано, что делает каждая команда и изнашивает ли она ячейки:

КомандаЧто делаетРазмерИзнос
EEPROM.read(адрес)Читает один байт (0–255)1 байтНет, чтение бесплатно
EEPROM.write(адрес, значение)Перезаписывает байт при каждом вызове1 байтТратит цикл при каждом вызове
EEPROM.update(адрес, значение)Сначала читает, пишет только если значение отличается1 байтТолько когда значение меняется
EEPROM.put(адрес, данные)Записывает int, float, массив или struct побайтно; внутри использует updatesizeof(данные)Только изменившиеся байты
EEPROM.get(адрес, переменная)Читает записанное через put обратно в том же типеsizeof(переменная)Нет
EEPROM[адрес]Доступ как к массиву; чтение бесплатно, присваивание через = работает как write1 байтПри каждом присваивании
EEPROM.length() (размер)Возвращает общий объём EEPROM-Нет
EEPROM.write() и EEPROM.read() работают только с одним байтом. Если записать через write число больше 255, например 300, скетч скомпилируется без ошибки, но в ячейку попадёт только младший байт: 300 − 256 = 44. Для многобайтовых данных — int, long, float и структур — всегда используй EEPROM.put() и EEPROM.get(). Точно так же присваивание EEPROM[5] = x ведёт себя как write, а не как update.

Первый скетч: запись в EEPROM и чтение из неё

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3void setup() {
4  Serial.begin(9600);
5  Serial.print(F("EEPROM boyutu / size: "));
6  Serial.println(EEPROM.length());            // Uno'da 1024 / 1024 on the Uno
7
8  byte first = EEPROM.read(0);                // 0. adresi oku / read address 0
9  Serial.print(F("Adres 0 / address 0: "));
10  Serial.println(first);                      // ilk seferde 255 / 255 the first time
11
12  EEPROM.update(0, 42);                       // farklıysa yazar / writes only if different
13  EEPROM.write(1, 7);                         // her seferinde yazar / writes every time
14
15  int number = 300;
16  EEPROM.put(10, number);                     // int = 2 bayt: 10 ve 11 / int = 2 bytes: 10 and 11
17  float temperature = 23.75;
18  EEPROM.put(12, temperature);                // float = 4 bayt: 12-15 / float = 4 bytes: 12-15
19
20  int readNumber;
21  float readTemperature;
22  EEPROM.get(10, readNumber);                 // put ile aynı tür / same type as put
23  EEPROM.get(12, readTemperature);
24  Serial.println(readNumber);                 // 300
25  Serial.println(readTemperature);            // 23.75
26
27  EEPROM.update(20, 300);                     // HATA: yalnız alt bayt (44) kalır / BUG: only the low byte (44) is kept
28  Serial.println(EEPROM.read(20));            // 44
29}
30
31void loop() {}

Загрузи скетч и открой монитор порта на скорости 9600 бод. При первом запуске по адресу 0 читается 255, потому что в эту ячейку ещё ни разу не писали. Теперь нажми кнопку сброса на плате или отключи и снова подключи USB-кабель: на этот раз читается 42. Это и есть доказательство того, что EEPROM сохраняет данные. А строка с write() для адреса 1 перезаписывает ячейку при каждом запуске, хотя значение всегда равно 7; с update() запись произошла бы только в первый раз. Значение 300 в последней строке показывает ловушку из предупреждения выше, и на экране появляется 44. Если в «Файл › Настройки» параметр «Предупреждения компилятора» (Compiler warnings) стоит не на «Нет», компилятор отметит эту строку предупреждением large integer implicitly truncated to unsigned type (большое целое неявно обрезано до беззнакового типа).

При работе с put() и get() нужно знать, сколько байтов занимает каждое значение, иначе две переменные запишутся друг поверх друга и одна из них испортится. В примере int занимает адреса 10 и 11, поэтому float начинается с адреса 12. Вот размеры типов данных на Uno:

Тип данныхРазмер на UnoДиапазонПример использования
byte, bool, char (1 байт)1 байт0–255, true/false или один символСостояние вкл/выкл, выбор режима
int (целое)2 байтаот −32 768 до 32 767Калибровочное смещение
unsigned int, uint16_t (беззнаковое)2 байта0–65 535Рекорд в игре
long, unsigned long (длинное)4 байтаОколо ±2,1 млрд или 0–4,29 млрдДолговременный счётчик
float (дробное)4 байтаТочность 6–7 знаковКалибровочный коэффициент датчика
char массив[10]10 байт9 символов плюс завершающий нульПароль, имя устройства
Заведи в начале скетча карту адресов, например: const int ADDR_MODE = 0; const int ADDR_SCORE = 1; const int ADDR_CAL = 3;. Следующий адрес всегда считай как предыдущий адрес + sizeof(предыдущих данных) и оставляй между полями несколько свободных байтов, чтобы потом можно было добавить новые.

Ресурс записи EEPROM: что значит предел в 100 000?

100 000 записей звучит солидно, но неудачно написанный скетч может израсходовать этот ресурс за несколько минут. Формула срока службы простая: срок службы = 100 000 ÷ частота записи в одну и ту же ячейку. Главное: предел действует для каждой ячейки отдельно, а не для всей EEPROM; если раз за разом писать по одному адресу, изнашивается только эта ячейка. Чтение на ресурс вообще не влияет. В таблице показано, сколько проживёт ячейка, если писать по одному адресу с разной частотой:

Как часто пишется один адресПримерный срок службы
write() в loop() без задержки (около 300 раз в секунду)5–6 минут
Каждую секундуОколо 28 часов
Каждую минутуОколо 69 дней
Раз в 10 минутОколо 1,9 года
Раз в часОколо 11 лет
10 раз в деньОколо 27 лет

100 000 — это минимум, который гарантирует производитель; многие микросхемы выдерживают гораздо больше, но рассчитывать на это при проектировании не стоит. Изношенная ячейка не умирает внезапно: сначала она перестаёт надёжно держать записанное значение, и некоторые биты начинают меняться сами. В таком случае данные можно перенести на другой адрес, ведь остальные ячейки это не затрагивает. Как продлить срок службы:

  • Используй update() и put(): обе команды перед записью читают ячейку и вообще ничего не пишут, если значение то же самое.
  • Пиши только при изменении: сохраняй настройку, когда пользователь действительно её меняет, а не на каждом проходе loop().
  • Откладывай сохранение: если значение меняется много раз подряд, запиши его один раз через несколько секунд после последнего изменения; с millis() это делается легко.
  • Пиши по событиям: сохраняй рекорд, когда игра закончилась, а настройки меню — при выходе из меню.
  • Распределяй записи: часто меняющийся счётчик пиши по очереди в несколько адресов, а не в один (см. пример с кольцевым буфером ниже).
  • При необходимости бери внешнюю память: для очень частых сохранений подойдёт внешняя EEPROM на 1 млн циклов, FRAM или SD-карта.

Пример 1: счётчик, который не забывает значение без питания

В первом примере соберём счётчик, который увеличивается при каждом нажатии кнопки и после выключения и включения платы продолжает с того же места. Подключи один вывод кнопки к D2, а другой к GND; используется встроенная подтяжка, так что отдельный резистор не нужен (подробности в статье Как подключить кнопку к Arduino, расположение пинов — в утилите распиновки). Хитрость в том, что скетч сохраняет счётчик не при каждом нажатии, а через 5 секунд после последнего нажатия. Поэтому, даже если нажать кнопку 20 раз подряд, в EEPROM запись произойдёт только один раз.

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3const byte BUTTON_PIN = 2;                     // buton D2 ile GND arasında / button between D2 and GND
4const int  COUNT_ADDR = 0;                     // sayaç 0-3. baytlarda / counter in bytes 0-3
5const unsigned long SAVE_DELAY = 5000;         // son basıştan 5 sn sonra yaz / write 5 s after the last press
6
7unsigned long count = 0;
8bool dirty = false;                            // kaydedilmemiş değişiklik / unsaved change
9unsigned long lastPress = 0;
10unsigned long lastChange = 0;
11bool lastReading = HIGH;
12bool stableState = HIGH;
13
14void setup() {
15  Serial.begin(9600);
16  pinMode(BUTTON_PIN, INPUT_PULLUP);
17  EEPROM.get(COUNT_ADDR, count);
18  if (count == 0xFFFFFFFF) count = 0;          // boş EEPROM / blank EEPROM
19  Serial.print(F("Kayitli sayac / stored count: "));
20  Serial.println(count);
21}
22
23void loop() {
24  bool reading = digitalRead(BUTTON_PIN);
25  if (reading != lastReading) {                // pin değişti, sayacı başlat / pin changed, restart timer
26    lastChange = millis();
27    lastReading = reading;
28  }
29  if (millis() - lastChange > 30 && reading != stableState) {   // 30 ms sabit kaldı / stable for 30 ms
30    stableState = reading;
31    if (stableState == LOW) {                  // yeni basış / new press
32      count++;
33      dirty = true;
34      lastPress = millis();
35      Serial.println(count);
36    }
37  }
38
39  if (dirty && millis() - lastPress > SAVE_DELAY) {   // sessiz dönemden sonra yaz / write after a quiet period
40    EEPROM.put(COUNT_ADDR, count);
41    dirty = false;
42    Serial.println(F("EEPROM'a yazildi / saved to EEPROM"));
43  }
44}

Переменная dirty означает, что в ОЗУ есть изменение, которое ещё не сохранено. Счётчик имеет тип unsigned long, поэтому занимает 4 байта (адреса 0–3) и может досчитать до более чем 4 млрд нажатий. В чистой EEPROM эти четыре байта равны 0xFF, поэтому при первом запуске читается 4 294 967 295; увидев это значение, код начинает счёт с нуля. Так как put() записывает только изменившиеся байты, при росте счётчика чаще всего изнашивается только младший байт. Если питание пропадёт в течение 5 секунд до сохранения, последние нажатия потеряются; это небольшая цена за защиту EEPROM.

Пример 2: настройки и пароль в структуре struct

Вместо того чтобы записывать несколько настроек по отдельным адресам, аккуратнее и надёжнее собрать их в struct и сохранить одним вызовом put(). В этом примере хранятся яркость светодиода и пароль из 4–6 символов. Первые два поля структуры очень важны: magic («магическое число») позволяет понять, действительно ли в EEPROM записаны настройки именно этого скетча, а version не даёт прочитать старые несовместимые данные после того, как в структуру позже добавится новое поле. Если хотя бы одно из них не совпадает, скетч записывает настройки по умолчанию.

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3const uint16_t MAGIC = 0xB6A1;                 // "ayar yazıldı" işareti / "settings written" marker
4const byte VERSION = 1;                        // yapı değişince artır / bump when the struct changes
5const int  SETTINGS_ADDR = 0;
6const byte LED_PIN = 9;                        // PWM pini / PWM pin
7
8struct Settings {
9  uint16_t magic;
10  byte version;
11  char password[7];                            // en çok 6 karakter + '\0' / up to 6 chars + '\0'
12  byte brightness;                             // LED parlaklığı 0-255 / LED brightness 0-255
13};
14
15Settings cfg;
16
17void writeDefaults() {
18  cfg.magic = MAGIC;
19  cfg.version = VERSION;
20  strcpy(cfg.password, "1234");
21  cfg.brightness = 128;
22  EEPROM.put(SETTINGS_ADDR, cfg);
23}
24
25void setup() {
26  Serial.begin(9600);
27  pinMode(LED_PIN, OUTPUT);
28  EEPROM.get(SETTINGS_ADDR, cfg);
29  if (cfg.magic != MAGIC || cfg.version != VERSION) {   // ilk açılış ya da eski yapı / first boot or old layout
30    writeDefaults();
31    Serial.println(F("Varsayilan ayarlar yazildi / defaults written"));
32  }
33  analogWrite(LED_PIN, cfg.brightness);
34  Serial.print(F("Ayar boyutu / settings size: "));
35  Serial.println(sizeof(Settings));            // Uno'da 11 bayt / 11 bytes on the Uno
36  Serial.println(F("Komutlar / commands: b0-255   p<eski>,<yeni>   ?"));
37}
38
39void loop() {
40  if (!Serial.available()) return;
41  String line = Serial.readStringUntil('\n');
42  line.trim();
43
44  if (line.startsWith("b")) {                  // örnek / example: b200
45    cfg.brightness = constrain(line.substring(1).toInt(), 0, 255);
46    analogWrite(LED_PIN, cfg.brightness);
47    EEPROM.put(SETTINGS_ADDR, cfg);            // yalnız değişen bayt yazılır / only the changed byte is written
48    Serial.println(F("Parlaklik kaydedildi / brightness saved"));
49  } else if (line.startsWith("p")) {           // örnek / example: p1234,5678
50    int comma = line.indexOf(',');
51    if (comma < 0) {
52      Serial.println(F("Bicim / format: p1234,5678"));
53      return;
54    }
55    String oldPw = line.substring(1, comma);
56    String newPw = line.substring(comma + 1);
57    if (oldPw != cfg.password) {
58      Serial.println(F("Eski sifre yanlis / wrong old password"));
59    } else if (newPw.length() < 4 || newPw.length() > 6) {
60      Serial.println(F("Yeni sifre 4-6 karakter olmali / new password must be 4-6 chars"));
61    } else {
62      newPw.toCharArray(cfg.password, sizeof(cfg.password));
63      EEPROM.put(SETTINGS_ADDR, cfg);
64      Serial.println(F("Sifre degisti / password changed"));
65    }
66  } else if (line == "?") {
67    Serial.print(F("Parlaklik / brightness: "));
68    Serial.println(cfg.brightness);
69  }
70}

Подключи светодиод к D9 через резистор (подойдёт 220 Ω, номинал можно проверить в калькуляторе резистора для светодиода); D9 — пин с ШИМ. Открой монитор порта на скорости 9600 бод, выбери конец строки New Line («Новая строка») и попробуй команды: b200 ставит яркость 200, p1234,5678 меняет пароль с 1234 на 5678, а ? показывает текущую яркость. После выключения и включения платы светодиод загорается с той же яркостью, а новый пароль продолжает работать. На Uno структура занимает 11 байт; put() записывает только изменившиеся байты, поэтому изменение яркости изнашивает одну-единственную ячейку. Ту же структуру можно приспособить для кодового замка с клавиатурой или хранить в массиве 4-байтовые идентификаторы разрешённых карт для RFID RC522. Если команды через последовательный порт пока в новинку, прочитай статью Монитор порта Arduino.

Никогда не клади в EEPROM объект String. String хранит не сам текст, а его адрес в ОЗУ и длину; при вызове EEPROM.put(0, name) в EEPROM уходит не текст, а несколько байтов указателя, и после перезапуска читается мусор. Текст всегда храни в массиве char фиксированного размера, как в примере. Кроме того, EEPROM не шифруется: любой, кто подключит к плате программатор, сможет её прочитать. Такой подход годится для любительских замков, но не для задач, где нужна настоящая безопасность.

Как умножить ресурс записи: кольцевой буфер (wear leveling)

Если часто обновляемый счётчик всё время писать по одному адресу, эта единственная ячейка быстро изнашивается. В кольцевом буфере значение записывается по очереди в разные слоты: одно сохранение идёт в один слот, следующее — в следующий, а после последнего слота запись возвращается к началу. Каждый слот записывается лишь раз на 16 сохранений, поэтому ресурс вырастает в 16 раз, примерно до 1,6 млн сохранений. При запуске читаются все слоты, чтобы найти самое свежее значение; так как счётчик только растёт, самое большое значение и есть самое новое.

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3const int  RING_START = 100;                   // halka 100. adresten başlar / ring starts at address 100
4const byte SLOTS = 16;                         // 16 yuva x 4 bayt = 64 bayt / 16 slots x 4 bytes = 64 bytes
5
6uint32_t counter = 0;
7byte slot = 0;
8
9void loadCounter() {
10  for (byte i = 0; i < SLOTS; i++) {
11    uint32_t v;
12    EEPROM.get(RING_START + i * 4, v);
13    if (v != 0xFFFFFFFF && v >= counter) {     // en büyük değer en yenisidir / the largest value is the newest
14      counter = v;
15      slot = i;
16    }
17  }
18}
19
20void saveCounter() {
21  slot = (slot + 1) % SLOTS;                   // her kayıtta sıradaki yuva / next slot on every save
22  EEPROM.put(RING_START + slot * 4, counter);
23}
24
25void setup() {
26  Serial.begin(9600);
27  loadCounter();
28  counter++;                                   // örnek: her açılışı say / example: count every boot
29  saveCounter();
30  Serial.print(F("Acilis sayisi / boot count: "));
31  Serial.println(counter);
32  Serial.print(F("Kullanilan yuva / slot used: "));
33  Serial.println(slot);
34}
35
36void loop() {}

Пример увеличивает счётчик при каждом запуске; вместо этого можно считать нажатия кнопки, события датчика или моточасы. Чем больше слотов, тем пропорционально больше ресурс: 64 слота занимают 256 байт и выдерживают около 6,4 млн сохранений. Если плата раньше работала с другими скетчами, в области кольца могли остаться старые данные, поэтому перед первым использованием очисти EEPROM кодом ниже. В более сложных проектах можно добавить контрольную сумму CRC, как во встроенном примере Arduino eeprom_crc, чтобы ловить испорченные данные.

Как очистить EEPROM? Стирается ли она при загрузке скетча?

При обычной загрузке по USB-кабелю записывается только память программ, а данные в EEPROM сохраняются. Поэтому, когда загружается новый проект, настройки предыдущего остаются в EEPROM, и новый код может прочитать значения, которых не ожидает. На Uno EEPROM стирается только при работе с ISP-программатором через Скетч › Загрузить через программатор (Upload Using Programmer) или Инструменты › Записать загрузчик (Burn Bootloader): эти операции стирают всю микросхему, а бит EESAVE, защищающий EEPROM, в fuse-настройках Uno выключен. Чтобы очистить EEPROM вручную, загрузи один раз этот скетч:

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3void setup() {
4  for (uint16_t i = 0; i < EEPROM.length(); i++) {
5    EEPROM.update(i, 255);                     // fabrika durumu 0xFF / factory state 0xFF
6  }
7  pinMode(LED_BUILTIN, OUTPUT);
8  digitalWrite(LED_BUILTIN, HIGH);             // LED yandı: bitti / LED on: done
9}
10
11void loop() {}

Пример Файл › Примеры › EEPROM › eeprom_clear из Arduino IDE записывает во все ячейки 0, а здесь ячейки возвращаются в заводское состояние 255. Так код, который проверяет «магическое число» или 0xFF, снова распознаёт EEPROM как пустую. Благодаря update() ячейки, где уже 255, вообще не перезаписываются. Запись всех 1024 ячеек на Uno занимает не больше 3,4 секунды; когда загорится встроенный светодиод, всё готово. После этого загрузи свой основной скетч.

EEPROM на ESP32: библиотека Preferences

Настоящей EEPROM у ESP32 нет. Постоянные данные хранятся в разделе NVS (Non-Volatile Storage, энергонезависимое хранилище) внешней флеш-памяти; в стандартной таблице разделов эта область занимает 20 КБ. Espressif рекомендует библиотеку Preferences, которая входит в пакет плат ESP32. Preferences хранит данные не по адресам, а по именам: пространство имён (namespace) и ключ (key), так что помнить, по какому адресу лежит счётчик, не нужно. NVS каждый раз пишет новое значение в следующую свободную запись на странице и помечает старую как стёртую, поэтому выравнивание износа происходит автоматически. Имена пространств и ключей могут быть не длиннее 15 символов и чувствительны к регистру. Если ESP32 пока в новинку, начни со статьи Что такое ESP32.

C++ (Arduino IDE)
1#include <Preferences.h>
2
3Preferences prefs;
4
5void setup() {
6  Serial.begin(115200);
7  prefs.begin("bluegrays", false);             // ad alanı; false = okuma + yazma / namespace; false = read + write
8
9  uint32_t boots = prefs.getUInt("boots", 0);  // anahtar yoksa 0 / 0 if the key is missing
10  boots++;
11  prefs.putUInt("boots", boots);
12
13  if (!prefs.isKey("ssid")) {                  // ilk açılış / first boot
14    prefs.putString("ssid", "EvAgi");          // örnek değer / example value
15  }
16  String ssid = prefs.getString("ssid", "");   // kayıtlı Wi-Fi adı / stored Wi-Fi name
17  int brightness = prefs.getInt("bright", 128);   // yoksa 128 / 128 if missing
18
19  Serial.print("Acilis / boot: ");
20  Serial.println(boots);
21  Serial.print("SSID: ");
22  Serial.println(ssid);
23  Serial.print("Parlaklik / brightness: ");
24  Serial.println(brightness);
25  Serial.print("Bos kayit yeri / free entries: ");
26  Serial.println(prefs.freeEntries());
27  prefs.end();                                 // ad alanını kapat / close the namespace
28}
29
30void loop() {}
КомандаЧто делает
prefs.begin("имя", false)Открывает пространство имён; false — чтение и запись, true — только чтение
putUInt, putInt, putFloat, putBool, putString, putBytes (запись)Сохраняет значение под именем ключа
getUInt("ключ", по_умолчанию)Читает значение; если ключа нет, возвращает значение по умолчанию
isKey("ключ")Проверяет, существует ли ключ
remove("ключ")Удаляет один ключ
clear() (очистка)Удаляет все ключи в открытом пространстве имён
freeEntries() (свободные записи)Возвращает число свободных записей, оставшихся в NVS
end() (закрытие)Закрывает пространство имён

Значение нужно читать тем же типом, которым оно записано: если записать через putUInt, а читать через getInt, вернётся значение по умолчанию. Вызов isKey() перед getString() в коде тоже не случаен: если в «Инструменты › Core Debug Level» выбран уровень Error или выше, чтение несуществующего текстового ключа выводит в монитор порта строку nvs_get_str len fail: ssid NOT_FOUND; это безобидно, но сбивает с толку. Повторная загрузка по USB не стирает NVS, но если параметр Erase All Flash Before Sketch Upload (стирать всю флеш-память перед загрузкой) стоит в Enabled, каждая загрузка удаляет все записи. Хранить данные Wi-Fi через Preferences — обычная практика; если удобнее вводить имя сети и пароль с телефона, подойдёт и ESP32 WiFiManager, а свои настройки Wi-Fi ESP32 хранит в той же области NVS. Для короткоживущих значений, которые должны пережить пробуждение из глубокого сна, лучше подходит память RTC: она вообще не изнашивает флеш; подробности в статье Глубокий сон ESP32.

При переносе старого кода Arduino на ESP32 библиотека EEPROM тоже работает, но с двумя отличиями: перед использованием нужно указать, сколько байтов понадобится, через EEPROM.begin(размер), а после изменений — вызвать EEPROM.commit(). put() и write() меняют только копию в ОЗУ; без commit() во флеш ничего не попадает. На ESP32 эта библиотека работает как один блок данных внутри NVS, и Espressif считает её устаревшей (deprecated); все байты только что созданной области равны 0xFF. Тот же код компилируется и для ESP8266: там размер не больше 4096 байт, а каждый commit() стирает и перезаписывает сектор флеш-памяти на 4 КБ, поэтому вызывай commit как можно реже.

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3void setup() {
4  Serial.begin(115200);
5  EEPROM.begin(64);                            // kullanılacak bayt sayısı / number of bytes to use
6  uint32_t boots;
7  EEPROM.get(0, boots);
8  if (boots == 0xFFFFFFFF) boots = 0;          // yeni alan 0xFF dolu / a fresh area is 0xFF
9  boots++;
10  EEPROM.put(0, boots);                        // yalnız RAM'deki kopyayı değiştirir / changes only the RAM copy
11  EEPROM.commit();                             // flaşa şimdi yazılır / written to flash now
12  Serial.println(boots);
13  EEPROM.end();
14}
15
16void loop() {}

Постоянные данные на Raspberry Pi Pico: файловая система MicroPython

В микроконтроллере RP2040 на Raspberry Pi Pico EEPROM тоже нет; на плате стоит флеш-память на 2 МБ. MicroPython отводит около 1,4 МБ этой памяти под файловую систему LittleFS (на Pico W меньше из-за прошивки Wi-Fi). Так что на Pico аналог EEPROM — запись настроек в небольшой файл. LittleFS распределяет записи по разным блокам флеш-памяти, то есть сама выравнивает износ, и файловая система не повреждается, даже если питание пропадёт во время записи. Код ниже хранит настройки в формате JSON, считает каждый запуск и сохраняет счётчик при каждом нажатии кнопки, подключённой между GP15 и GND.

MicroPython
1import json, os, time
2from machine import Pin
3
4FILE = "settings.json"
5DEFAULTS = {"boots": 0, "count": 0, "password": "1234"}
6
7def load():
8    try:
9        with open(FILE) as f:
10            data = json.load(f)
11    except (OSError, ValueError):            # dosya yok ya da bozuk / file missing or corrupt
12        data = {}
13    for key, value in DEFAULTS.items():      # eksik anahtarları tamamla / fill in missing keys
14        data.setdefault(key, value)
15    return data
16
17def save(data):
18    with open(FILE + ".tmp", "w") as f:      # önce geçici dosyaya yaz / write a temp file first
19        json.dump(data, f)
20    os.rename(FILE + ".tmp", FILE)           # sonra eskisinin yerine koy / then replace the old one
21
22settings = load()
23settings["boots"] += 1
24save(settings)
25print("Acilis / boot:", settings["boots"], "Sayac / count:", settings["count"])
26
27button = Pin(15, Pin.IN, Pin.PULL_UP)        # buton GP15 ile GND arasında / button between GP15 and GND
28
29while True:
30    if button.value() == 0:                  # basıldı / pressed
31        time.sleep_ms(30)                    # sıçramayı bekle / let the bounce settle
32        if button.value() == 0:
33            settings["count"] += 1
34            save(settings)                   # seyrek olay, her basışta yaz / rare event, save on each press
35            print("Sayac / count:", settings["count"])
36            while button.value() == 0:       # bırakılmasını bekle / wait for release
37                time.sleep_ms(10)
38            time.sleep_ms(30)                # bırakma sıçraması / release bounce
39    time.sleep_ms(10)

Вместо того чтобы перезаписывать файл напрямую, код сначала пишет во временный файл .tmp, а затем через os.rename() ставит его на место старого; так старый файл настроек остаётся целым, даже если запись прервётся. Если файла нет или его содержимое испорчено, load() возвращается к значениям по умолчанию, а когда в программу позже добавится новая настройка, setdefault() сам допишет недостающий ключ. Если Pico программируется из Arduino IDE, можно использовать и библиотеку EEPROM из ядра Arduino-Pico от Earle Philhower: EEPROM.begin(256) открывает область от 256 до 4096 байт, а EEPROM.commit() сохраняет изменения. Эта эмулированная EEPROM находится в последнем секторе флеш-памяти на 4 КБ, и каждый commit() стирает и перезаписывает этот сектор; флеш рассчитана примерно на 100 000 циклов стирания на сектор, поэтому вызывай commit как можно реже. Если работа с Pico только начинается, хорошая отправная точка — статьи Что такое Raspberry Pi Pico и Основы MicroPython.

Больше постоянной памяти: внешняя EEPROM, FRAM и SD-карты

Если 1 КБ не хватает или сохранять нужно очень часто, можно добавить внешнюю память. Микросхемы I2C EEPROM вроде AT24C256 дают 32 КБ и около 1 млн циклов записи, а подключаются всего двумя проводами SDA и SCL (Что такое I2C). Малоизвестная деталь: на многих продающихся модулях часов реального времени DS3231 уже стоит EEPROM AT24C32 на 4 КБ, обычно доступная по I2C-адресу 0x57. Для задач, где сохранять нужно много раз в секунду, лучше подходят модули FRAM, рассчитанные примерно на триллион (10^12) циклов записи, а для больших журналов данных — модуль SD-карты, который хранит данные в файлах.

Идеи проектов с EEPROM

  • Лампа, которая помнит последнее состояние: когда питание вернётся, переведи лампу на реле в то состояние, в котором она была до отключения.
  • Сейф со сменным кодом: записывай в EEPROM новый код, введённый на клавиатуре, чтобы код можно было менять без повторной загрузки скетча.
  • Дверной замок по картам: храни идентификаторы разрешённых карт, считанных через RFID RC522, а новые карты добавляй с помощью мастер-карты.
  • Постоянный рекорд: сохраняй лучший счёт в «Саймоне», T-Rex или «Змейке»; примеры кода есть в статье Игровые проекты на Arduino.
  • Робот-манипулятор, записывающий движения: записывай углы сервоприводов поза за позой и воспроизводи их; код из статьи Робот-манипулятор на Arduino делает это через EEPROM.update().
  • Весы, которые помнят калибровку: сохрани калибровочный коэффициент, найденный с тензодатчиком и HX711, как float через put().
  • Счётчик сбросов: считай, сколько раз плата перезапускается, чтобы найти проблемы с питанием; метод описан в статье Arduino постоянно перезагружается.

Частые ошибки и как их исправить

СимптомПричинаРешение
При первом запуске читается 255, -1, 65535 или nanВ EEPROM ещё ни разу не писали; пустые ячейки равны 0xFFЗаписывай значения по умолчанию, если проверка «магического числа» или 0xFF не проходит
Записано 300, читается 44write() и read() переносят только 1 байтДля многобайтовых данных используй put() и get()
Два значения портят друг другаАдреса перекрываются; int занимает 2 байта, float — 4Составь карту адресов и считай адреса через sizeof
После перезапуска текст превращается в мусорЧерез put() записан объект StringХрани текст в массиве char
Новый код читает настройки старого проектаОбычная загрузка не стирает EEPROMИспользуй «магическое число» и поле версии или очисти EEPROM
Настройки пропали после загрузки скетчаЗагрузка через программатор на Uno, параметр Erase All Flash на ESP32Загружай как обычно по USB, на ESP32 выбери для параметра Disabled
На ESP32 значения не сохраняютсяНет EEPROM.begin() или EEPROM.commit()Добавь оба вызова или перейди на Preferences
Preferences всегда возвращает значение по умолчаниюЧтение другим типом, имя длиннее 15 символов или другое пространство имёнСогласуй типы put и get, делай имена короткими
Ошибка nvs_open failed: NOT_FOUNDНи разу не созданное пространство имён открыто только для чтения (true)Сначала открой пространство имён с false и запиши значение
Через какое-то время значения портятсяПостоянная запись в loop() или просадка питания во время записиИспользуй update() и отложенное сохранение, добавь кольцевой буфер и CRC
Самый быстрый способ найти ошибку — посмотреть, что лежит в EEPROM. Прочитай в цикле for первые 64 адреса через EEPROM.read() и выведи их в монитор порта (или открой готовый пример Arduino eeprom_read), и сразу станут видны пустые ячейки, значения, записанные не по тому адресу, и данные, оставшиеся от старых проектов.

EEPROM — самый простой способ дать проектам на Arduino память: несколько строк кода защищают настройки, счётчики и пароли устройства от отключения питания. Следить нужно только за ресурсом записи: если пользоваться update() и put() и не писать без нужды, EEPROM на Uno годами работает без сбоев. В приложении BlueGrays есть проекты, которые можно дополнить EEPROM, например «Кодовый замок с клавиатурой 4x4 и LCD», «Умный электронный сейф», «Аутентификация по RFID», «Игра на память» и «OLED T-Rex Run», и у каждого есть схема подключения и пошаговый код.

Частые вопросы

Сколько раз можно записывать в EEPROM Arduino?

В даташите ATmega328P для каждой ячейки EEPROM указано не менее 100 000 циклов записи/стирания. Предел действует для каждой ячейки отдельно, а чтение на него вообще не влияет. Если писать по одному адресу раз в секунду, ячейка изнашивается примерно за 28 часов; если использовать update() и записывать только при изменении значения, EEPROM прослужит годы.

Стирается ли EEPROM Arduino при загрузке нового скетча?

Нет. Обычная загрузка по USB записывает только память программ, а данные в EEPROM сохраняются. На Uno EEPROM стирается только при загрузке через ISP-программатор или при записи загрузчика. Чтобы новый проект не подхватил старые настройки, используй проверку «магического числа».

Чем EEPROM.write отличается от EEPROM.update?

write() перезаписывает значение в ячейку при каждом вызове и каждый раз тратит цикл записи. update() сначала читает ячейку и вообще ничего не пишет, если значение то же самое. Результат одинаковый, но update() бережёт ресурс EEPROM; put() внутри использует тот же способ.

Как очистить EEPROM Arduino?

Запиши 255 (или 0) во все адреса циклом for до EEPROM.length(); пример eeprom_clear в Arduino IDE сразу записывает 0. Очистка всех 1024 байт на Uno занимает около 3,4 секунды. После этого не оставляй на плате скетч очистки, а загрузи свою основную программу.

Есть ли у ESP32 EEPROM?

Настоящей EEPROM у ESP32 нет; постоянные данные хранятся в разделе NVS флеш-памяти. Для новых проектов рекомендуется библиотека Preferences. Для старого кода библиотека EEPROM тоже работает, но сначала обязательно нужно вызвать EEPROM.begin(размер), а после каждого изменения — EEPROM.commit().

Как сохранить в EEPROM int, float или текст?

Многобайтовые данные — int, long, float и структуры — записывай через EEPROM.put(адрес, переменная), а читай через EEPROM.get(адрес, переменная). int занимает 2 байта, float — 4, так что следующий адрес выбирай с учётом этого. Текст храни в массиве char фиксированного размера, а не в String.

Все проекты
Включение и выключение светодиода кнопкой (вкл/выкл)
#10Новичок

Включение и выключение светодиода кнопкой (вкл/выкл)

Нажми кнопку один раз — свет загорится, нажми ещё раз — погаснет, совсем как выключатель в твоей комнате. Arduino замечает каждое нажатие и переводит светодиод в противоположное состояние, выжидая кор

4.8(100)
167
Счётчик на семисегментном индикаторе с кнопками
#43Продвинутый
Premium

Счётчик на семисегментном индикаторе с кнопками

Простой счётчик с тремя кнопками: нажми одну — цифра на индикаторе увеличится на единицу, нажми другую — уменьшится, а третья возвращает её к нулю. Одноразрядный семисегментный индикатор считает от 0

4.6(35)
47
Кодовый замок с клавиатурой 4x4 и LCD
#64Продвинутый
Premium

Кодовый замок с клавиатурой 4x4 и LCD

Маленький электронный дверной замок, который не пускает тех, кто не знает кода. Ты нажимаешь клавишу решётки и вводишь код на клавиатуре; если он верный, экран сообщает, что доступ разрешён, загораетс

4.8(59)
75
Умный электронный сейф
#92Продвинутый
Premium

Умный электронный сейф

Маленький сейф для ценных вещей, который открывается кодом. Ты вводишь на клавиатуре свой четырёхзначный код; если он верный, сервопривод открывает замок, загорается зелёный свет, а экран показывает с

4.7(42)
63
Аутентификация по RFID
#54Средний
Premium

Аутентификация по RFID

Совсем как вход по карте у двери подъезда: ты подносишь карту к считывателю RFID, а Arduino сравнивает её номер с сохранённым. Правильная карта зажигает зелёный светодиод на три секунды, чужая — красн

4.8(64)
83
Игра на память на Arduino (4 светодиода + 4 кнопки + зуммер)
#93Продвинутый
Premium

Игра на память на Arduino (4 светодиода + 4 кнопки + зуммер)

Доверяешь своей памяти? Четыре цветных светодиода загораются в определённой последовательности, каждый со своей нотой, а ты повторяешь тот же порядок кнопками. Каждый верный раунд добавляет в последов

4.8(46)
69
OLED T-Rex Run: игра с динозавром на Arduino
#87Продвинутый
Premium

OLED T-Rex Run: игра с динозавром на Arduino

Игра с динозавром, которая появляется, когда пропадает интернет, теперь у тебя на столе: динозавр бежит по маленькому OLED-экрану, навстречу едут кактусы, а прыгаешь ты одной кнопкой. Счёт растёт в ре

4.7(33)
53
Глубокий сон ESP32: экономия батареи (OLED + счётчик в RTC RAM)
#135Продвинутый
Premium

Глубокий сон ESP32: экономия батареи (OLED + счётчик в RTC RAM)

Режим глубокого сна (Deep Sleep) у ESP32 снижает потребление до ~10 µA, выключая процессор и большую часть периферии, — это важно для IoT-устройств на батарейках. В этом проекте используются три источника пробуждения: таймер (15 секунд), внешний пин (EXT0 — кнопка GPIO 33) и касание (T0/GPIO 4). Переменные с RTC_DATA_ATTR сохраняются во время глубокого сна — счётчики загрузок, пробуждений по таймеру и по кнопке ведутся непрерывно.

4.3(10)
16