Что такое EEPROM и зачем она
EEPROM — энергонезависимая память: данные переживают выключение и перепрошивку. На ATmega328P (Uno, Nano) её всего 1024 байта — немного, но хватает для настроек, калибровок, счётчиков моточасов, последнего состояния устройства.
Типичные применения:
- запомнить яркость/режим после выключения;
- счётчик включений или моточасов;
- калибровочные коэффициенты датчика;
- «последняя радиостанция» / «последняя позиция серво»;
- максимумы/минимумы температуры за всё время.
Главное ограничение — ресурс ~100 000 циклов записи на ячейку. Писать раз в секунду — убьёте память за сутки. Пишите редко и только когда данные реально меняются.
Библиотека EEPROM: read и write
#include <EEPROM.h>
EEPROM.write(0, 42); // байт по адресу 0
byte val = EEPROM.read(0); // читает байт
write пишет один байт (0–255). Для чисел побольше — put и get, которые сами раскладывают любой тип по байтам:
int counter = 12345;
EEPROM.put(10, counter); // записать int (2 байта)
EEPROM.get(10, counter); // прочитать обратно
float calib = 3.14;
EEPROM.put(20, calib); // float — 4 байта
EEPROM.get(20, calib);
update — умная запись: пишет только если байт изменился, экономя ресурс:
EEPROM.update(0, newBrightness); // запишется только при изменении
Хранение структуры настроек
Удобно держать все настройки в одной структуре и сохранять целиком:
struct Settings {
uint8_t brightness; // 1 байт
int16_t targetTemp; // 2 байта
bool enabled; // 1 байт
uint32_t magic; // 4 байта — признак "данные валидны"
};
const uint32_t MAGIC = 0xDEADBEEF;
Settings cfg;
void loadSettings() {
EEPROM.get(0, cfg);
if (cfg.magic != MAGIC) {
// первая прошивка — ставим дефолты
cfg = {128, 2200, true, MAGIC};
EEPROM.put(0, cfg);
}
}
void saveSettings() {
cfg.magic = MAGIC;
EEPROM.put(0, cfg);
}
magic-поле — простой способ понять, что EEPROM не «мусорная»: при первой загрузке там случайные байты, проверка на сигнатуру выдаёт дефолты.
Пример: запоминание яркости
int8_t brightness;
void setup() {
EEPROM.get(0, brightness);
if (brightness < 0 || brightness > 255) brightness = 128; // незаписанная — дефолт
}
void onBrightnessChange(int8_t newVal) {
brightness = newVal;
EEPROM.update(0, brightness); // запишется только при реальном изменении
}
Износ EEPROM и его растяжка
100 000 циклов — много, но не бесконечно. Счётчик, пишущий раз в минуту — ~70 дней жизни ячейки. Правила:
- пишите только при изменении данных (
update); - для частых счётчиков — распределяйте запись по разным адресам (wear leveling) или сбрасывайте в EEPROM раз в час, а не в секунду;
- критичные данные дублируйте на два адреса с контрольной суммой;
- не используйте EEPROM как лог — для этого есть SD-карта.
Счётчик включений — рабочий пример
uint32_t bootCount;
void setup() {
EEPROM.get(0, bootCount);
bootCount++;
EEPROM.put(0, bootCount);
Serial.print("включений: ");
Serial.println(bootCount);
}
На каждом старте счётчик растёт — но пишется только раз за включение, ресурс израсходуется не быстрее, чем устройство состарится.
EEPROM против Flash и SD
- EEPROM — 1 КБ, ~100к циклов, побайтовый доступ — настройки и мелочи.
- Flash (через PROGMEM) — до 32 КБ программы, только чтение в рантайме — константные таблицы.
- SD-карта — гигабайты, файлы — логи и данные.
Для ESP8266/ESP32 нет настоящей EEPROM — она эмулируется через Flash (EEPROM.begin(size), EEPROM.commit()) или лучше через Preferences/LittleFS.
Типичные ошибки
- Пишут в loop каждую итерацию — убивают EEPROM за день.
- Читают без проверки — при первом запуске там 0xFF везде, данные мусорные. Проверка через magic или range-check.
- Пишут
intчерезwrite—writeтолько байты; для остальногоput/get. - Не делают update —
writeпишет всегда, даже то же значение, изнашивая ячейку. - Структура без версии — при смене прошивки и её полей старые данные ломают новую логику.
Итог
EEPROM — килобайт памяти, который выживает без питания. Набор: put/get для типов, update против износа, структура с magic для валидности, запись только при изменении. Счётчики, настройки, калибровки — всё это живёт здесь и переживает перепрошивку.
Пример: конфиг с валидацией и CRC
struct Config {
uint8_t brightness;
int16_t targetTemp;
bool enabled;
uint32_t magic;
uint16_t crc;
};
const uint32_t MAGIC = 0xC0FFEE42;
uint16_t crc16(const uint8_t* data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; ++i) {
crc ^= data[i];
for (int b = 0; b < 8; ++b)
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
}
return crc;
}
Config cfg;
void loadConfig() {
EEPROM.get(0, cfg);
uint16_t savedCrc = cfg.crc;
cfg.crc = 0;
uint16_t calc = crc16((uint8_t*)&cfg, sizeof(cfg) - sizeof(cfg.crc));
if (cfg.magic != MAGIC || calc != savedCrc) {
cfg = {128, 2200, true, MAGIC, 0};
cfg.crc = crc16((uint8_t*)&cfg, sizeof(cfg) - sizeof(cfg.crc));
EEPROM.put(0, cfg);
Serial.println("дефолтный конфиг");
}
}
Wear leveling: распределение износа
Для счётчика, который пишется часто, распределяйте запись по кольцу ячеек — 1 КБ EEPROM растянет жизнь в тысячи раз:
const int SLOT_COUNT = 64;
const int SLOT_SIZE = sizeof(uint32_t) + 2; // значение + crc
uint32_t readCounter() {
uint32_t maxVal = 0;
for (int i = 0; i < SLOT_COUNT; ++i) {
uint32_t v; uint16_t crc;
EEPROM.get(i * SLOT_SIZE, v);
EEPROM.get(i * SLOT_SIZE + 4, crc);
if (crc16((uint8_t*)&v, 4) == crc && v > maxVal) maxVal = v;
}
return maxVal;
}
Когда EEPROM не подходит
- Логирование каждую секунду — SD-карта.
- Настройки крупнее 1 КБ — ESP32 Preferences/LittleFS.
- Частые перезаписи — FRAM-модуль (FM24C64) — триллион циклов.
- Шифрование ключей — ATECC608 secure element.
Полезные библиотеки
- EEPROM — встроенная, read/write/get/put/update.
- EEPROM32 — эмуляция на ESP32.
- Preferences — ESP32 native, key-value, удобнее.
- FRAM_I2C — для ferroelectric памяти.
Полный пример: конфиг с CRC
struct Config {
uint8_t brightness;
int16_t targetTemp;
bool enabled;
uint32_t magic;
uint16_t crc;
};
const uint32_t MAGIC = 0xC0FFEE42;
uint16_t crc16(const uint8_t* data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; ++i) {
crc ^= data[i];
for (int b = 0; b < 8; ++b)
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
}
return crc;
}
Wear leveling
Для счётчика, который пишется часто, распределяйте запись по кольцу ячеек — 1 КБ EEPROM растянет жизнь в тысячи раз.
Когда EEPROM не подходит
- Логирование каждую секунду — SD-карта.
- Настройки >1 КБ — ESP32 Preferences/LittleFS.
- Частые перезаписи — FRAM (FM24C64) — триллион циклов.