Главная chevron_right Уроки chevron_right Программирование chevron_right Руководство по EEPROM на Arduino

Руководство по EEPROM на Arduino

Узнайте, как использовать EEPROM — энергонезависимую память микроконтроллера — для хранения данных между перезагрузками Arduino.

Микроконтроллер на платах Arduino оснащён EEPROM: памятью типа EEPROM, значения в которой сохраняются даже после отключения питания — как крошечный жёсткий диск.

Функции класса EEPROM уже включены в платформу для вашей платы, поэтому устанавливать сторонние библиотеки не нужно.

Где пригодится EEPROM

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

Поддерживаемые платы

Все перечисленные ниже платы оснащены EEPROM:

Важно: платы на базе SAMD-микроконтроллеров (например, Arduino Zero) не имеют аппаратной EEPROM. На них нужно использовать эмуляцию через Flash-память или внешние микросхемы.

EEPROM Clear — очистка памяти

Этот пример показывает, как сбросить все байты EEPROM в 0, подготовив память к записи новых данных. Используется функция EEPROM.write().

Очистка особенно полезна при первом запуске нового проекта: старые данные от предыдущих скетчей могут мешать корректной работе программы.

/*
 * EEPROM Clear
 *
 * Устанавливает все байты EEPROM в 0.
 * См. eeprom_iteration для более подробного
 * изучения способов обхода EEPROM.
 *
 * Код примера находится в общественном достоянии.
 */

#include <EEPROM.h>

void setup() {
  // инициализируем пин LED как выход.
  pinMode(13, OUTPUT);
  
  /***
    Перебираем каждый байт памяти EEPROM.
    Более крупные AVR-процессоры имеют больший объём EEPROM, например:
    - Arduino Duemilanove: 512 B EEPROM.
    - Arduino Uno:         1 kB EEPROM.
    - Arduino Mega:        4 kB EEPROM.
    Вместо жёстко заданной длины используйте встроенную функцию length().
    Это сделает код переносимым для всех AVR-процессоров.
  ***/

  for (int i = 0 ; i < EEPROM.length() ; i++) {
    EEPROM.write(i, 0);
  }

  // включаем LED по завершении
  digitalWrite(13, HIGH);
}

void loop() {
  /** Пустой loop. **/
}

EEPROM CRC — проверка целостности данных

CRC — это простой способ убедиться, что данные не изменились и не были повреждены. Данный пример вычисляет значение CRC непосредственно по содержимому EEPROM. CRC работает как цифровая подпись: если хотя бы один байт изменился, результат вычисления тоже изменится.

Дополнительно пример демонстрирует, что объект EEPROM можно использовать как обычный массив — обращаться к элементам по индексу.

/***

    Автор: Christopher Andrews.

    Алгоритм CRC сгенерирован pycrc, лицензия MIT ( https://github.com/tpircher/pycrc ).

    CRC — простой способ проверить, изменились ли данные или были повреждены.

    В этом примере CRC вычисляется непосредственно по значениям EEPROM.

    Цель примера — показать, что объект EEPROM можно использовать как массив.

***/

#include <Arduino.h>
#include <EEPROM.h>

void setup() {

  //Запускаем serial

  Serial.begin(9600);

  while (!Serial) {

    ; // ожидаем подключения serial-порта. Нужно только для native USB

  }

  //Выводим длину данных для вычисления CRC.

  Serial.print("EEPROM length: ");

  Serial.println(EEPROM.length());

  //Выводим результат вызова eeprom_crc()

  Serial.print("CRC32 of EEPROM data: 0x");

  Serial.println(eeprom_crc(), HEX);

  Serial.print("\n\nDone!");
}

void loop() {

  /* Пустой loop */
}

unsigned long eeprom_crc(void) {

  const unsigned long crc_table[16] = {

    0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,

    0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,

    0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,

    0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c

  };

  unsigned long crc = ~0L;

  for (int index = 0 ; index < EEPROM.length()  ; ++index) {

    crc = crc_table[(crc ^ EEPROM[index]) & 0x0f] ^ (crc >> 4);

    crc = crc_table[(crc ^ (EEPROM[index] >> 4)) & 0x0f] ^ (crc >> 4);

    crc = ~crc;

  }

  return crc;
}

EEPROM Get — чтение составных типов данных

Этот пример показывает разницу между методами put/get и write/read. Функции read и write работают с одним байтом за раз, тогда как get считывает столько байт, сколько занимает переменная нужного типа данных. Например, для float будет прочитано 4 байта, для int — 2 байта.

/***

    Пример eeprom_get.

    Показывает, как использовать метод EEPROM.get().

    Для предварительной записи данных запустите скетч eeprom_put.

    Скетч работает и без этого, однако отображаемые значения

    будут взяты из того, что уже хранится в EEPROM.

    Если в одной из загруженных строк нет нулевого символа,

    serial может вывести длинную строку мусора.

    Автор: Christopher Andrews, 2015

    Лицензия MIT.

***/

#include <EEPROM.h>

void setup() {

  float f = 0.00f;   //Переменная для хранения данных, считанных из EEPROM.

  int eeAddress = 0; //Адрес EEPROM, с которого начинаем чтение

  Serial.begin(9600);

  while (!Serial) {

    ; // ожидаем подключения serial-порта. Нужно только для native USB

  }

  Serial.print("Read float from EEPROM: ");

  //Считываем данные типа float из EEPROM по адресу 'eeAddress'

  EEPROM.get(eeAddress, f);

  Serial.println(f, 3);    //Может вывести 'ovf, nan', если данные в EEPROM не являются корректным float.

  /***

    Так как get возвращает ссылку на 'f', его можно использовать inline.

    Например: Serial.print( EEPROM.get( eeAddress, f ) );

  ***/

  /***

    get также работает с пользовательскими структурами.

    Это вынесено в отдельную функцию.

  ***/

  secondTest(); //Запускаем следующий тест.
}

struct MyObject {

  float field1;

  byte field2;

  char name[10];
};

void secondTest() {

  int eeAddress = sizeof(float); //Смещаем адрес на следующий байт после float 'f'.

  MyObject customVar; //Переменная для хранения пользовательского объекта, считанного из EEPROM.

  EEPROM.get(eeAddress, customVar);

  Serial.println("Read custom object from EEPROM: ");

  Serial.println(customVar.field1);

  Serial.println(customVar.field2);

  Serial.println(customVar.name);
}

void loop() {

  /* Пустой loop */
}

EEPROM Iteration — обход всей памяти

Цель этого примера — показать разные способы перебора всего адресного пространства EEPROM. Код не является самостоятельным скетчем, а представляет собой набор готовых фрагментов, которые удобно вставлять в собственные проекты.

Обход памяти пригодится, например, для поиска первого свободного адреса, массовой инициализации или диагностики содержимого.

/***

    Пример eeprom_iteration.

    Набор фрагментов, демонстрирующих

    простейшие способы обхода EEPROM.

    Запускать этот скетч необязательно —

    он лишь иллюстрирует определённые приёмы программирования.

    Автор: Christopher Andrews, 2015

    Лицензия MIT.

***/

#include <EEPROM.h>

void setup() {

  /***

    Обход EEPROM с помощью цикла for.

  ***/

  for (int index = 0 ; index < EEPROM.length() ; index++) {

    //Прибавляем единицу к каждой ячейке EEPROM

    EEPROM[ index ] += 1;

  }

  /***

    Обход EEPROM с помощью цикла while.

  ***/

  int index = 0;

  while (index < EEPROM.length()) {

    //Прибавляем единицу к каждой ячейке EEPROM

    EEPROM[ index ] += 1;

    index++;

  }

  /***

    Обход EEPROM с помощью цикла do-while.

  ***/

  int idx = 0;  //Используется 'idx', чтобы избежать конфликта имён с 'index' выше.

  do {

    //Прибавляем единицу к каждой ячейке EEPROM

    EEPROM[ idx ] += 1;

    idx++;

  } while (idx < EEPROM.length());

} //Конец функции setup.

void loop() {}

EEPROM Put — запись составных типов данных

Пример демонстрирует метод EEPROM.put(), который записывает в EEPROM данные любого типа — включая пользовательские структуры (struct). Внутри put использует EEPROM.update(), поэтому физическая запись происходит только тогда, когда новое значение отличается от уже хранящегося. Это продлевает срок службы памяти.

Количество записываемых байт зависит от типа переменной или размера структуры.

/***

    Пример eeprom_put.

    Показывает, как использовать метод EEPROM.put().

    Также этот скетч предварительно записывает данные для

    примера eeprom_get.

    В отличие от побайтового EEPROM.write(),

    метод put использует семантику обновления: байт записывается

    в EEPROM только если данные действительно изменились.

    Автор: Christopher Andrews, 2015

    Лицензия MIT.

***/

#include <EEPROM.h>

struct MyObject {

  float field1;

  byte field2;

  char name[10];
};

void setup() {

  Serial.begin(9600);

  while (!Serial) {

    ; // ожидаем подключения serial-порта. Нужно только для native USB

  }

  float f = 123.456f;  //Переменная для записи в EEPROM.

  int eeAddress = 0;   //Адрес, по которому хотим сохранить данные.

  //Один простой вызов: первый аргумент — адрес, второй — объект.

  EEPROM.put(eeAddress, f);

  Serial.println("Written float data type!");

  /** put также предназначен для работы с пользовательскими структурами. **/

  //Данные для сохранения.

  MyObject customVar = {

    3.14f,

    65,

    "Working!"

  };

  eeAddress += sizeof(float); //Смещаем адрес на следующий байт после float 'f'.

  EEPROM.put(eeAddress, customVar);

  Serial.print("Written custom data type! \n\nView the example sketch eeprom_get to see how you can retrieve the values!");
}

void loop() {

  /* Пустой loop */
}

EEPROM Read — побайтовое чтение

Пример показывает, как прочитать значение каждого байта EEPROM с помощью функции EEPROM.read() и вывести эти значения в монитор последовательного порта Arduino IDE.

Это удобный способ диагностики: можно быстро проверить, что именно хранится в памяти после предыдущей сессии работы.

/*
 * EEPROM Read
 *
 * Считывает значение каждого байта EEPROM и выводит его
 * на компьютер.
 * Код примера находится в общественном достоянии.
 */

#include <EEPROM.h>

// начинаем чтение с первого байта (адрес 0) EEPROM
int address = 0;
byte value;

void setup() {
  // инициализируем serial и ожидаем открытия порта:
  Serial.begin(9600);
  while (!Serial) {
    ; // ожидаем подключения serial-порта. Нужно только для native USB
  }
}

void loop() {
  // считываем байт по текущему адресу EEPROM
  value = EEPROM.read(address);

  Serial.print(address);
  Serial.print("\t");
  Serial.print(value, DEC);
  Serial.println();

  /***
    Переходим к следующему адресу; по достижении конца возвращаемся в начало.

    Более крупные AVR-процессоры имеют больший объём EEPROM, например:
    - Arduino Duemilanove: 512 B EEPROM.
    - Arduino Uno:         1 kB EEPROM.
    - Arduino Mega:        4 kB EEPROM.

    Вместо жёстко заданной длины используйте встроенную функцию length().
    Это сделает код переносимым для всех AVR-процессоров.
  ***/
  address = address + 1;
  if (address == EEPROM.length()) {
    address = 0;
  }

  /***
    Поскольку размеры EEPROM являются степенями двойки, переполнение адреса
    можно предотвратить побитовым AND с (length() - 1).

    ++address &= EEPROM.length() - 1;
  ***/

  delay(500);
}

EEPROM Update — умная запись

Метод EEPROM.update() записывает данные только в том случае, если новое значение отличается от уже хранящегося по данному адресу. Это важно по двум причинам:

  • Скорость: каждая операция записи занимает около 3,3 мс. Пропуск лишних записей ускоряет выполнение программы.
  • Ресурс: EEPROM рассчитана примерно на 100 000 циклов записи на каждую ячейку. Использование update вместо write значительно продлевает срок службы памяти.

Рекомендуется использовать EEPROM.update() везде, где возможна повторная запись одного и того же значения.

/***
   Метод EEPROM Update

   Сохраняет значения, считанные с аналогового входа 0, в EEPROM.
   Эти значения останутся в EEPROM после выключения платы
   и могут быть прочитаны другим скетчем позднее.

   Если значение в EEPROM не изменилось, оно не перезаписывается,
   что позволяет не сокращать ресурс EEPROM без необходимости.

   Лицензия MIT.
 ***/

#include <EEPROM.h>

/** текущий адрес в EEPROM (т.е. байт, в который будет выполнена следующая запись) **/
int address = 0;

void setup() {
  /** Пустой setup **/
}

void loop() {
  /***
    Делим на 4, потому что аналоговые входы дают значения от
    0 до 1023, а каждый байт EEPROM может хранить только
    значение от 0 до 255.
  ***/
  int val = analogRead(0) / 4;

  /***
    Обновляем конкретную ячейку EEPROM.
    Эти значения останутся там после выключения платы.
  ***/
  EEPROM.update(address, val);

  /***
    Функция EEPROM.update(address, val) эквивалентна следующему:

    if( EEPROM.read(address) != val ){
      EEPROM.write(address, val);
    }
  ***/


  /***
    Переходим к следующему адресу; по достижении конца возвращаемся в начало.

    Более крупные AVR-процессоры имеют больший объём EEPROM, например:
    - Arduino Duemilanove: 512 B EEPROM.
    - Arduino Uno:         1 kB EEPROM.
    - Arduino Mega:        4 kB EEPROM.

    Вместо жёстко заданной длины используйте встроенную функцию length().
    Это сделает код переносимым для всех AVR-процессоров.
  ***/
  address = address + 1;
  if (address == EEPROM.length()) {
    address = 0;
  }

  /***
    Поскольку размеры EEPROM являются степенями двойки, переполнение адреса
    можно предотвратить побитовым AND с (length() - 1).

    ++address &= EEPROM.length() - 1;
  ***/

  delay(100);
}

EEPROM Write — запись аналоговых показаний

Этот пример показывает, как записывать значения, считанные с аналогового входа 0, в EEPROM с помощью функции EEPROM.write(). Записанные данные сохранятся после отключения питания и могут быть прочитаны другим скетчем позднее.

/*

 * EEPROM Write

 *

 * Сохраняет значения, считанные с аналогового входа 0, в EEPROM.

 * Эти значения останутся в EEPROM после выключения платы

 * и могут быть прочитаны другим скетчем позднее.

 */

#include <EEPROM.h>

/** текущий адрес в EEPROM (т.е. байт, в который будет выполнена следующая запись) **/
int addr = 0;

void setup() {

  /** Пустой setup. **/
}

void loop() {

  /***

    Делим на 4, потому что аналоговые входы дают значения от

    0 до 1023, а каждый байт EEPROM может хранить только

    значение от 0 до 255.

  ***/

  int val = analogRead(0) / 4;

  /***

    Записываем значение в соответствующий байт EEPROM.

    Эти значения останутся там после выключения платы.

  ***/

  EEPROM.write(addr, val);

  /***

    Переходим к следующему адресу; по достижении конца возвращаемся в начало.

    Более крупные AVR-процессоры имеют больший объём EEPROM, например:

    - Arduino Duemilanove: 512 B EEPROM.

    - Arduino Uno:        1 kB EEPROM.

    - Arduino Mega:       4 kB EEPROM.

    Вместо жёстко заданной длины используйте встроенную функцию length().

    Это сделает код переносимым для всех AVR-процессоров.

  ***/

  addr = addr + 1;

  if (addr == EEPROM.length()) {

    addr = 0;

  }

  /***

    Поскольку размеры EEPROM являются степенями двойки, переполнение адреса

    можно предотвратить побитовым AND с (length() - 1).

    ++addr &= EEPROM.length() - 1;

  ***/

  delay(100);
}

Типичные ошибки при работе с EEPROM

  • Выход за пределы адресного пространства. Попытка записать по адресу, превышающему размер EEPROM, приведёт к непредсказуемому поведению. Используйте EEPROM.length(), чтобы узнать реальный объём памяти.
  • Частая перезапись одного адреса. Если скетч в цикле постоянно пишет в одну и ту же ячейку, ресурс в 100 000 циклов исчерпается неожиданно быстро. Используйте EEPROM.update() или реализуйте wear leveling.
  • Запись многобайтовых типов через write. Функция EEPROM.write() работает только с одним байтом. Для int, float и структур используйте EEPROM.put() и EEPROM.get().
  • Отсутствие инициализации. Новая плата или плата после прошивки может содержать в EEPROM случайные значения (0xFF в незаписанных ячейках). Всегда проверяйте или инициализируйте данные перед использованием.