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.

Что такое 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 (память программ) | SRAM | EEPROM |
|---|---|---|---|
| Объём на Uno | 32 КБ (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 Mini | ATmega328P | 1024 байта | Настоящая EEPROM |
| Arduino Mega 2560 | ATmega2560 | 4096 байт | Настоящая EEPROM |
| Arduino Leonardo, Micro | ATmega32U4 | 1024 байта | Настоящая EEPROM |
| Arduino Nano Every | ATmega4809 | 256 байт | Настоящая EEPROM, но маленькая |
| Arduino Uno R4 Minima, WiFi | Renesas RA4M1 | 8 КБ | Флеш для данных; библиотека EEPROM работает с теми же командами |
| ESP32 | ESP32 (Xtensa) | Нет | Раздел NVS во флеш-памяти; рекомендуется Preferences |
| ESP8266 (NodeMCU, Wemos D1) | ESP8266 | Нет | Эмуляция во флеш-памяти; нужны EEPROM.begin() и commit() |
| Raspberry Pi Pico | RP2040 | Нет | В 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 побайтно; внутри использует update | sizeof(данные) | Только изменившиеся байты |
| EEPROM.get(адрес, переменная) | Читает записанное через put обратно в том же типе | sizeof(переменная) | Нет |
| EEPROM[адрес] | Доступ как к массиву; чтение бесплатно, присваивание через = работает как write | 1 байт | При каждом присваивании |
| EEPROM.length() (размер) | Возвращает общий объём EEPROM | - | Нет |
Первый скетч: запись в EEPROM и чтение из неё
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 символов плюс завершающий нуль | Пароль, имя устройства |
Ресурс записи 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 запись произойдёт только один раз.
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 не даёт прочитать старые несовместимые данные после того, как в структуру позже добавится новое поле. Если хотя бы одно из них не совпадает, скетч записывает настройки по умолчанию.
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.
Как умножить ресурс записи: кольцевой буфер (wear leveling)
Если часто обновляемый счётчик всё время писать по одному адресу, эта единственная ячейка быстро изнашивается. В кольцевом буфере значение записывается по очереди в разные слоты: одно сохранение идёт в один слот, следующее — в следующий, а после последнего слота запись возвращается к началу. Каждый слот записывается лишь раз на 16 сохранений, поэтому ресурс вырастает в 16 раз, примерно до 1,6 млн сохранений. При запуске читаются все слоты, чтобы найти самое свежее значение; так как счётчик только растёт, самое большое значение и есть самое новое.
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 вручную, загрузи один раз этот скетч:
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.
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 как можно реже.
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.
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, читается 44 | write() и 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 — самый простой способ дать проектам на 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.
Что собрать после прочтения
Все проекты
Включение и выключение светодиода кнопкой (вкл/выкл)
Нажми кнопку один раз — свет загорится, нажми ещё раз — погаснет, совсем как выключатель в твоей комнате. Arduino замечает каждое нажатие и переводит светодиод в противоположное состояние, выжидая кор

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

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

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

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

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

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

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

