Добавить в избранное

EEPROM на Arduino: хранение настроек и данных без питания

Чтение ≈ 4 мин

Встроенная EEPROM ATmega328P — 1 КБ энергонезависимой памяти для настроек, счётчиков и калибровок. Библиотека EEPROM, read/write/put/get, ресурс ячеек, выравнивание износа и хранение структур.

Что такое 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) — триллион циклов.
Поделиться
Понравился материал?

Комментарии

Добавить комментарий