урок ардуино eeprom

Знакомимся с EEPROM AVR в Arduino IDE.

от автора

в

Самое время вспомнить, что при перезагрузке arduino теряет сохранённые данные. Конечно, мы можем вписать пароль, который будет верифицирован, в тело программы, но если потом пользователь захочет поменяеть его? Для этого в arduino есть ячейки энергонезависимой памяти eeprom. Расшифровывается, как Electrically Erasable Programmable Read-Only Memory, т.е. дословно, энергонезависимая память. Отличается от ПЗУ, в которой хранится программа тем, что в исходном коде в процессе работы устройства изменения без программатора сделать нельзя, а в eeprom – можно. Значит мы можем менять пароль в процессе работы устройства, а также хранить данные карт памяти и другие параметры.

В ATmega328 – 1кБ такой памяти, но есть нюансы её использования, особенно в arduino. Попробуем разобрать все интересные моменты работы с этим типом памяти, для начала загрузим необходимую библиотеку

#include <EEPROM.h>  // Библиотека EEPROM

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

unsigned int correctPassword = 6774;    // Переменная корректного пароля
unsigned int test;                      // Переменная, куда будем загружать данные, размером два байта
unsigned char a,b;                      // Переменная, куда будем загружать данные, размером один байт

В оболочке arduino ide, есть две простые команды для записи и чтения

EEPROM.write(address, data);       // Запись 
EEPROM.read(address);              // Чтение

Адрес мы можем задавать в пределах нашего объёма EEPROM, для ATMEGA328 он составляет 1 кбайт. Запишем значение в ячейку 1, считаем и выведем в терминал

void setup()
{
  Serial.begin(9600);
  EEPROM.write(1, correctPassword);   // Записываем значение переменной в ячейку 1
  test = EEPROM.read(1);              // Считываем в переменную test значение ячейки 1
  Serial.println(test);
}

Загружаем в плату, смотрим, получилось 118 вместо 6774.

Почему так получилось? Потому-что ячейки памяти 1-байтовые, а число у нас было 2 байтовое, вот оно и не поместилось полностью. Что же делать в таких ситуациях? Разбить число на отдельные байты, для этого в оболочке есть простые команды highByte и lowByte.

void setup()
{
  Serial.begin(9600);
  // Запись
  byte hi = highByte(correctPassword); // Выделяем старший байт
  byte low = lowByte(correctPassword); // Выделяем младший байт
  EEPROM.write(1, hi);                 // Старший байт в адрес 1 EEPROM
  EEPROM.write(2, low);                // Младший байт в адрес 2 EEPROM
}

Выполним этот код, и запишем его в плату. Потом напишем код, который сможет его прочитать. Так, как данные у нас уже записаны, для надёжности закомментируем участок кода, где идёт запись. Будем оперировать двумя переменными типа unsigned char = a и b, куда будем складывать части числа.

  a = EEPROM.read(1);        // Старший байт читаем в переменную a
  b = EEPROM.read(2);        // Младший байт читаем в переменную b
  Serial.println(a);
  Serial.println(b);
  int test = word(a, b);     // Объединяем a и b в переменную int test
  Serial.println(test);

Командой EEPROM.read(ячейка памяти); загрузим значения в a и b. Затем командой int (переменная) = word(a, b); соберём всё в одну переменную типа int. Для наглядности посмотрим, какие значения получились в терминале, выведем a,b и собственно собранную переменную test

Если вам интересно, почему a и b, принимают значения 26 и 118, всё легко понять, стоит лишь перевести эти значения в двоичное представление. Это и есть старший и младший байт числа test

a == 00011010‭              // Старший байт
b == 01110110‬              // Младший байт
test == ‭00011010 01110110‬  // Двухбайтовое число

В симуляторе можно посмотреть занятую область памяти EEPROM, и убедиться, что мы записали в ячейки 1 и 2 данные. Ячейка 0 осталась пустая, со значением FF.

Нужно учитывать при написании программ, что запись занимает намного больше времени, чем считывание, около 3,3 мс. Для контроллера, даже с 16мгц, это почти целая вечность. Мы рассмотрели способ записи и чтения по байтам, но эта конструкция не подходит, если нужно будет записать число типа long, потому что highByte и lowByte, работают только с младшим и старшим байтом. Но с какой-то версии в ардуино появились макросы
EEEPROM.get и EEPROM.put. Попробуем поработать с переменной типа uint32_t, но на самом деле эти команды отлично подойдут и для uint16_t, также для float и структур.

uint32_t big = 1765476;   // Большая переменная
EEPROM.put(3,big);        // Записываем в ячейку 3 переменную big

Почему-то во всех других статьях, пишут что put для работы с переменной float, и структур, но никогда не упоминается, что он может по сути работать с любыми данными. Макрос сам решит, сколько ячеек в памяти нужно занять числу. Посмотрим в proteus

Если перевести 1765476 в hex код, то мы получим ‭1A F0 64‬, ячейка 6, с результатом 0х00 образовалась из-за типа переменной  uint32_t, потому что она занимает 4 байта. Вот ещё пример

uint32_t big2 = 12;  // Переменная unsigned long, но с маленьким значением
EEPROM.put(7,big2);  // Записываем в ячейку 7

Число 12, которое должно занять в памяти 1 байт, заняло аж 4 байта, заполнив EEPROM нулями. Поэтому, нужно понимать с какими числами мы работаем, и грамотно подбирать тип переменной, и команды, которые мы используем для работы с памятью

Ну и попробуем команду EEPROM.get, чтобы считать данные обратно.

uint32_t result;            // Переменная для считывания
EEPROM.get(3, result);      // Запрашиваем состояние ячейки 3
Serial.println(result);     // Выводим в терминал значение ячейки 3 через переменную result
EEPROM.get(7, result);      // Проделываем аналогичные действия с ячейкой 7
Serial.println(result);

Числа считались верно. На самом деле, в arduino это самые удобные команды для записи и чтения, я бы советовал использовать их, правильно подбирая тип переменной

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

Для интереса рассмотрим способ разложения числа на байты, который более кроссплатформенный, не задействуя put, get. В с++ есть такая вещь, как указатели. Это очень удобный инструмент, и он достаточно сложный, но мы рассмотрим его на простом примере.

Указатели – это тоже переменные, но они хранят в себе адрес других переменных. Если мы укажем указатель на переменную, он примет не её значение, а значение её адреса. Но и значение можно потом узнать, если указатель разыменовать, используя *. Такая конструкция выведет переменную correctPassword

  uint16_t *PointerRead;            // Создаём указатель PointerRead
  PointerRead = &correctPassword;   // Присваиваем указателю адрес переменной correctPassword
  Serial.println(*PointerRead);     // Разыменовываем указатель и выводим в терминал

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

Если мы знаем адрес по которой находится переменная, но она занимает разные области памяти (например int = два байта), мы можем отдельно указать на байты, из которых состоит эта переменная и достать их значения отдельно.

  byte *PointerWrite = (byte*)&correctPassword;  // Указываем на байты в int переменной correctPassword
  EEPROM.write(11, PointerWrite[0]);             // записываем в ячейку 11 старший байт
  EEPROM.write(12, PointerWrite[1]);             // записываем в ячейку 12 младший байт

В первой строчке мы создаём указатель *PointerWrite типа byte, на переменную correctPassword, но если мы не укажем, что обращаемся к ней по байтам, то компилятор выдаст ошибку. Затем мы можем уже обращаясь к конкретному байту указателя записать значения в область памяти EEPROM. Смотрим результат – появилась знакомое нам уже число в ячейке 11 и 12

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

  byte *PointerRead = (byte*)&testPointer;  // Указываем на байты в переменной типа testPointer
  PointerRead[0] = EEPROM.read(11);         // Первый байт r[0] читаем из 11 ячейки EEPROM
  PointerRead[1] = EEPROM.read(12);         // Второй байт r[1] читаем из 12 ячейки EEPROM

Проверив результат, убедились, что значение прочитано верно – 6774. Мы разобрали уже три способа записи и чтения в arduino EEPROM числа типа int. Что ещё можно интересно найти в командах на официальном сайте? Например, что вместо команды eeprom.write, лучше использовать eeprom.update

EEPROM.update(address, val);

В случае, если в ячейке будет такое-же значение компилятор проигнорирует запись, чем даст вам сразу два бонуса: 1) Вы не будете тратить 3.3мс, на новую бесполезную запись, если там и так уже такое значение. 2) Ресурс каждой ячейки – около 100 000 циклов записи (по реальным тестам – бывает и 300 000 и 500 000), так-что экономится ресурс EEPROM. Прицип действия её прост, эта команда эквивалентна коду

if( EEPROM.read(address) != val )  // Читаем ячейку, если адрес не равен значению val
    {
      EEPROM.write(address, val);  // То прописываем значение val
    }

Чтобы проверить эту команду, напишем простой цикл for, на 1024 итерации, с записью значения 176 в каждую новую ячейку памяти

  for (int index = 0 ; index < 1024 ; index++)
  {
    a = 176;
    EEPROM.update(index, a);
  }

Перед экспериментов я обнулил память, и ячейки приняли значение FF. Делая симуляцию по шагам, можно заметить, как области памяти заполняются нашим значением 176 (B0 в HEX)

В логе можно посмотреть, как происходит запись одной ячейки. Время и правда не маленькое, как и заявляет производитель – около 3мс.

Proteus, ещё информирует о бите EEMPE – бит защиты от записи. Откладывает процесс записи данных в EEPROM на четыре цикла, это необходимо, для того, чтобы память подготовилась к записи данных. Если обратиться к концу лога

Можно подсчитать, что запись 1кб памяти заняла целых 3.2 секунды. Теперь данные EEPROM остались в нашей памяти, и попробуем запустить программу ещё раз. По идее EEPROM.update не будет вести запись, чем сэкономит время и ресурс процессора. Проверяем

Видно, что процедуры записи не происходит, только чтение. А эта процедура занимает очень мало времени, в отличии от записи. Смотрим конец лога

Вся операция заняла 2мс, т.е. команда EEPROM.update, сохранила нам 3.2 секунды чистого времени, при условии, что значения не поменялись.

Мы получили достаточно сведений с работой EEPROM в AVR, в оболочке Arduino IDE, чтобы продолжить проектирование нашего устройства, чем и займёмся в следующей статье.