При разработке собственного устройства очень часто возникает необходимость сохранять и считывать данные из энергонезависимой памяти. Конечно, для этих целей можно использовать SD карты. Например, к платам семейства Arduino можно легко подключить адаптер для работы с microSD. Однако, часто нам не нужно сохранять такой большой объем данных. Например, если мы делаем умный будильник, где время срабатывания каждый день может быть разным, то эти значения неплохо бы сохранить в EEPROM. Или, если доступ к устройству осуществляется по паролю, то хэш этого пароля (не сам пароль!) неплохо бы тоже сохранить в EEPROM. Если зашьем пароль в прошивку, у нас не будет возможности его поменять, а это не очень хорошо с точки зрения безопасности.

В общем, есть масса ситуаций, когда нужно хранить небольшой объем информации и тогда лучше всего использовать энергонезависимую память на самом микроконтроллере.

В этой статье мы будем говорить об Arduino; но на самом деле на многих МК есть энергонезависимая память, например, на моем любимом МК ATtiny13, материалов о котором на Хабре пока не так много (но я надеюсь это в скором времени исправить).

Какая память бывает

Для того, чтобы избежать путаницы и подмены понятий, предлагаю разобраться с тем, какая память бывает на микроконтроллерах. В МК есть три вида памяти:

  • Flash‑память (ROM) — это тоже энергонезависимая память, в которой хранится прошивка контроллера; то, что там записано, нельзя изменить в процессе работы программы.

  • ОЗУ (RAM) — это оперативная память, используется для размещения переменных, массивов и других данных, при отключении питания эта информация будет потеряна.

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

Вот характеристики объемов памяти для разных моделей Arduino:

Единственная поправка к представленным значениям Flash заключается в том, что из них нужно вычесть как минимум 4Кб на загрузчик Bootloader. Благодаря этому загрузчику мы можем заливать прошивки на плату через USB порт без помощи программатора.

Но вернемся к EEPROM. Как видно из таблицы, у нас будет как минимум килобайт памяти — давайте посмотрим, как мы можем их использовать.

Запись и чтение

Работу с энергонезависимой памятью в Arduino мы начнем с рассмотрения примера записи в память. Для выполнения любых операций с EEPROM необходимо подключить соответствующую библиотеку EEPROM.h. Ниже мы осуществим запись значения в память двумя способами:

#include <EEPROM.h>

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    // ждем подключения
  }

  byte i= 1;  // Значение, которое будем записывать в EEPROM.
  int eeAddress = 0;   //адрес в памяти

  //Запишем значение в память

  EEPROM.write(eeAddress, i);
  eeAddress = eeAddress+1; //Увеличим адрес в памяти на размер сохраненных данных

  Serial.println("Запись успешно произведена!");

 //Запишем значение в память

  EEPROM.update(eeAddress, i);
  Serial.println("Запись успешно произведена!");

}

 

void loop() {

} 

В примере мы использовали две функции EEPROM.write() и EEPROM.update() для записи значений в память. Чем они отличаются? Первая функция выполнить запись в ячейку в любом случае, независимо от того, изменилось ли значение, которое мы хотим записать или оно осталось прежним. Вторая функция update в соответствии со своим названием запишет выполнит операцию записи только в том случае, если записываемое значение отличается от того, котрое уже есть в ячейке. Так в нашем примере кода в памяти был 0 и update выполнит запись, поместив в память 1. Если бы в памяти на этом месте уже была 1 , то операция записи бы не выполнялась.

Дело в том, что количество циклов записи в EEPROM не бесконечно. В сети можно найти описания того, как физически работает энергонезависимая память. Но суть в том, что в процессе выполнения операций записи элементы памяти изнашиваются. В среднем производители обычно обещают порядка 100 000 циклов записи (по данным https://docs.arduino.cc/). Поэтому я бы рекомендовал по возможности при записи в EEPROM использовать функцию update, для снижения «износа» модуля памяти.

Теперь поговорим о том, как можно прочитать содержимое энергонезависимой памяти. Здесь все проще: для чтения используется функция EEPROM.read(), которой в качестве аргумента передается номер ячейки памяти, из которой нужно прочитать значение.

#include <EEPROM.h>

// начинаем чтение с нулевого адреса

int address = 0;
byte value;

void setup() {
  Serial.begin(9600);
  while (!Serial) {

    // ждем подключения

  }
}

 

void loop() {

  // читаем байты из текущей ячейки памяти в цикле

  value = EEPROM.read(address);

// выводим номер ячейки и значение в десятичном виде

  Serial.print(address);
  Serial.print("\t");
  Serial.print(value, DEC);
  Serial.println();
  address = address + 1;
//если дошли до конца памяти, возвращаемся в начало
  if (address == EEPROM.length()) {
    address = 0;
  }
  delay(500);
}

Для того, чтобы ваш скетч (прошивка) была универсальной и могла работать на разных моделях Arduino, используйте функцию EEPROM.length() для выяснения размера памяти. Это лучше, чем жестко приколачивать значение в коде.

Посмотрим еще два полезных метода — это put и get. Они аналогичны операциям записи и чтения, но количество записываемых или читаемых байт зависит от типа данных или пользовательской структуры записываемой переменной. Put работает по принципу update, то есть записывает данные только в случае изменения.

#include <EEPROM.h>
struct MyObject {
  float field1;
  byte field2;
  char name[10];
};

void setup() {
  Serial.begin(9600);
  while (!Serial) {

}

  float f = 123.456f;  
  int eeAddress = 0;  .

  //Пишем float в первую ячейку

  EEPROM.put(eeAddress, f);

  Serial.println("Запись успешно произведена!");

  //Создаем свой объект

  MyObject customVar = {
    3.14f,
    65,
    "Working!"
  };

  eeAddress += sizeof(float); //Увеличиваем адрес
  EEPROM.put(eeAddress, customVar);

  Serial.print("Запись успешно произведена!");

}

void loop() {

}

Как видно, нам не надо думать о том, сколько байт в памяти занимает тот или иной тип данных.

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

#include <EEPROM.h>

void setup() {
  float f = 0.00f;  
  int eeAddress = 0; 
  Serial.begin(9600);
  while (!Serial) {

  }

  Serial.print("Читаем из EEPROM: ");
  EEPROM.get(eeAddress, f);
  Serial.println(f, 3);    //Можем получить 'ovf, nan' если значение f не типа float.
  secondTest(); //Run the next test.
}

 

struct MyObject {
  float field1;
  byte field2;
  char name[10];
};

void secondTest() {

  int eeAddress = sizeof(float); 
  MyObject customVar; //готовим переменную для чтения MyObject
  EEPROM.get(eeAddress, customVar);
  Serial.println("Читаем объекты из EEPROM: ");
  Serial.println(customVar.field1);
  Serial.println(customVar.field2);
  Serial.println(customVar.name);

}

void loop() {

}

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

Собственно, представленные функции являются достаточными для работы с EEPROM. Так, если нужно очистить память, то можно с помощью update записать 0 во все ячейки. Для поиска значения в памяти можно воспользоваться read и так далее.

Заключение

Мы рассмотрели базовые функции для работы с энергонезависимой памятью в Arduino. С их помощью можно реализовать необходимый для работы с памятью функционал и использовать в своих устройствах.


В декабре в Otus в рамках онлайн-курса «Электроника и электротехника» пройдут два открытых урока, на которые приглашаются все желающие:

  • 9 декабря: Отладка встраиваемого программного обеспечения в симуляторах Proteus и NI Multisim. Записаться

  • 17 декабря: Автоматическая регулировка усиления каскадов на операционных усилителях и транзисторах. Записаться

Комментарии (6)


  1. CitizenOfDreams
    06.12.2024 05:20

    из них нужно вычесть как минимум 4Кб на загрузчик Bootloader.

    Не многовато? Optiboot для Атмеги-328 (Arduino Nano) весит 512 байт.


  1. randomsimplenumber
    06.12.2024 05:20

    Как правильно писать в eeprom вы так и не рассказали :(


    1. voldemar_d
      06.12.2024 05:20

      Эта тема гораздо шире раскрыта, например, здесь:

      https://alexgyver.ru/lessons/eeprom/


  1. voldemar_d
    06.12.2024 05:20

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

    Номер начинается с единицы. А здесь используется адрес или индекс, начинающийся с нуля:

    https://arduino.ru/Reference/Library/EERPOM/read


  1. vvm13xx
    06.12.2024 05:20

    "Flash‑память (ROM) — это тоже энергонезависимая память, в которой хранится прошивка контроллера; то, что там записано, нельзя изменить в процессе работы программы. "

    Специфика Ардуино? Например, на (как минимум некоторых) STM32 можно изменить.

    "Но вернемся к EEPROM. Как видно из таблицы, у нас будет как минимум килобайт памяти..."

    Но в таблице у Mini 256B. По приведённой таблице можно подумать, что килобайт - максимум (но у Мега R3 четыре килобайта).


    1. randomsimplenumber
      06.12.2024 05:20

      Например, на (как минимум некоторых) STM32 можно изменить.

      Так и в Arduino можно. Загрузчик же каким-то образом читает данные с uart и пишет в ROM.