01 |
Микроконтроллеры семейства Atmega, на базе которых построены платы Arduino, имеют на борту 3 типа памяти:
|
Подробно о памяти можно почитать на официальном ресурсе — http://playground.arduino.cc/Learning/Memory.
EEPROM (англ. Electrically Erasable Programmable Read-Only Memory) — электрически стираемое перепрограммируемое ПЗУ (ЭСППЗУ), один из видов энергонезависимой памяти. |
02 |
Объемы памяти для каждого из чипов различны: |
|
03 |
|
|
04 | На заметку: |
Память EEPROM, по заявлениям производителя, обладает гарантированным жизненным циклом 100 000 операций записи/стирания и 100 лет хранения данных при температуре 25°С. Эти данные не распространяются на операции чтения данных из EEPROM — чтение данных не лимитировано. Исходя из этого, нужно проектировать свои скетчи максимально щадящими по отношению к EEPROM.
Время затраченное на одну операцию записи в EEPROM составляет примерно 3,3 мс. |
|
05 |
Доступ к EEPROM организован таким образом, что пользователь имеет возможность манипулировать каждым отдельным байтом памяти. |
|
06 |
Взаимодействие с памятью EEPROM обеспечивает штатная библиотека Arduino IDE — EEPROM.h, которую нужно импортировать в начале каждого скетча: |
|
07 | Arduino (C++) |
1 2 #include <EEPROM.h>
// ... |
|
08 |
Каждый байт EEPROM по умолчанию (ни разу не записанный) имеет значение 255. |
|
09 |
Библиотека EEPROM.h предоставляет доступ к основным функциям по работе с EEPROM: |
|
10 |
|
|
11 |
Примеры Для чтения всех байт EEPROM правильнее пользоваться универсальным решением, которое подходит для МК с разным объемом EEPROM — EEPROM.length(). Пример получения значения всех байт памяти EEPROM: |
|
12 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <EEPROM.h> // Импортируем бмблиотеку
int address = 0; // Переменная для хранения адреса
byte value; // Переменная для хранения значения
void setup() {
Serial.begin(9600);
while (address < EEPROM.length()) { // Перебираем адреса, до тех пор, пока не перебирем все
value = EEPROM.read(address); // Считываем значение байта
Serial.println("Address: "+String(address)+", value: "+String(value)); // Выводим полученное значение в монитор
address++; // Наращиваем адрес
}
}
void loop() {
} |
|
13 |
Операции чтения и записи данных с типом byte (диапазон значений 0-255) очень просты: |
|
14 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <EEPROM.h> // Импортируем бмблиотеку
int address = 0; // Переменная для хранения адреса
byte value; // Переменная для хранения значения
void setup() {
Serial.begin(9600);
EEPROM.write(0,195); // Записать значение 195 в ячейку с адресом 0
value=EEPROM.read(0); // Считать значение ячейки с адресом 0
Serial.println(value, DEC); // Вывести полученное значение
// 195
}
void loop() {} |
|
15 |
Поскольку память EEPROM имеет ограниченный жизненный цикл, вместо функции записи write() предпочтительнее использовать функцию обновления значения update() — она осуществляет запись, только в случае отличия нового значения от существующего: |
|
16 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <EEPROM.h> // Импортируем бмблиотеку
int address = 0; // Переменная для хранения адреса
byte value; // Переменная для хранения значения
void setup() {
Serial.begin(9600);
EEPROM.update(0,195); // Записать значение 195 в ячейку с адресом 0
value=EEPROM.read(0); // Считать значение ячейки с адресом 0
Serial.println(value, DEC); // Вывести полученное значение
// 195
}
void loop() {} |
|
17 |
Для записи в EEPROM данных, размер которых превышает 1 байт, нужно использовать функцию put(). Для корректного расчета адресов, по которым будет производиться запись сложных значений, необходимо пользоваться функцией sizeof() для расчета объема занимаемой памяти. Для чтения данных, в качестве второго аргумента функции put(), необходимо передать переменную, в которую эти данные будут считаны из EEPROM. Пример, записи числа типа integer (int, занимает 2 байта): |
|
18 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <EEPROM.h> // Импортируем бмблиотеку
int address = 0; // Переменная для хранения адреса
int value1 = 4587; // Переменная для хранения значения 1
int value2 = -25214; // Переменная для хранения значения 2
void setup() {
Serial.begin(9600);
EEPROM.put(address, value1); // Записать значение value1 в ячейку с адресом 0
// Для вычисления следующего доступного адреса, необходимо получить объем памяти,
// отведенной под предыдущее значение:
address += sizeof(int); // Корректируем адрес на уже записанное значение - 2 байта занимает тип int
Serial.println("Следующее значение будет записано по адресу: " + String(address));
EEPROM.put(address, value2); // Записать значение value2 в ячейку с адресом 2
// Обнуляем переменные для чистоты экперимента
address = 0;
int value1 = 0;
int value2 = 0;
// Получаем данные из EEPROM
EEPROM.get(address, value1);
address += sizeof(int); // Корректируем адрес на уже записанное значение
EEPROM.get(address, value2);
Serial.println( value1, DEC); // Вывести полученное значение 1
Serial.println( value2, DEC); // Вывести полученное значение 2
}
void loop() {} |
|
20 |
Пример записи/чтения значения типа float: |
|
21 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <EEPROM.h> // Импортируем бмблиотеку
int address = 0; // Переменная для хранения адреса
float value = -14.048f; // Переменная для хранения значения типа float
void setup() {
Serial.begin(9600);
EEPROM.put(address, value); // Записать значение value1 в ячейку с адресом 0
// Для вычисления следующего доступного адреса, необходимо получить объем памяти,
// отведенной под предыдущее значение:
address += sizeof(float); // Корректируем адрес на уже записанное значение - 2 байта занимает тип int
Serial.println("Следующее значение будет записано по адресу: " + String(address));
address = 0;
value = 0;
// Получаем данные из EEPROM
EEPROM.get(address, value);
Serial.println(value); // Вывести полученное значение float
}
void loop() {} |
|
22 | Важно: |
При хранении в памяти EEPROM типов данных отличных от byte, нельзя забывать о ситуации «первого чтения» из EEPROM — когда происходит чтение данных, которые не были записаны ранее. Выходом из ситуации может служить использование CRC (контрольной хеш-суммы). Об этом ниже в статье, в разделе о контроле целостности данных.
|
|
23 |
При чтении данных типа пользовательской структуры, если данные не были записаны ранее, будут получены неприемлемые данные: |
|
24 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <EEPROM.h> // Импортируем бмблиотеку
int address = 0; // Переменная для хранения адреса
struct myStruct { // Создаем пользовательскую структуру
float sum;
byte age;
char fullname[15];
};
void setup() {
Serial.begin(9600);
myStruct person = {}; // Создаем объект пользовательской структуры для записи в EEPROM
EEPROM.get(address, person); // Считываем данные из EEPROM в созданную переменную
Serial.println("Чтение пользовательской структуры из EEPROM: ");
Serial.println(person.sum);
Serial.println(person.age);
Serial.println(person.fullname);
}
void loop() {} |
|
25 |
Результат: 1 2 3 4 Чтение пользовательской структуры из EEPROM:
nan
255
⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮ |
|
26 |
Пример записи и чтения пользовательских структур: |
|
27 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <EEPROM.h> // Импортируем бмблиотеку
int address = 0; // Переменная для хранения адреса
struct myStruct { // Создаем пользовательскую структуру
float sum; // 4 байта
byte age; // 1 байт
char fullname[15]; // 15 байт - итого 20 байт занимает объект в памяти
};
void setup() {
Serial.begin(9600);
myStruct persons[] = { // Создаем массив объектов пользовательской структуры
{
110.20,
42,
"Ivanov Ivan"
}, {
72.45,
24,
"Petrova Elena"
}, {
22.98,
32,
"Popov Vasiliy"
}
};
// Записываем все данные в EEPROM
for (int i = 0; i < 3; i++) {
EEPROM.put(address, persons[i]); // Записываем значение переменной в EEPROM
address += sizeof(myStruct); // Корректируем адрес следующей записи на объем записываемых данных
}
// Теперь данные можно считать
address = 0;
myStruct person; // В переменную person будем считывать данные из EEPROM
for (int i = 0; i < 3; i++) {
EEPROM.get(address, person); // Считываем данные из EEPROM в созданную переменную
Serial.println("Чтение пользовательской структуры из EEPROM по адресу: " + String(address));
Serial.println( String(person.sum) + " " +
String(person.age) + " " +
String(person.fullname));
address += sizeof(myStruct); // Корректируем адрес следующей записи на объем записываемых данных
}
}
void loop() {} |
|
29 |
Контроль целостности данных Возможность использования объекта EEPROM[] как массива данных, бывает востребована для задач связанных с контролем целостности данных. Контроль целостности данных заключается в расчете контрольной суммы CRC — отпечатка состояния EEPROM, и в дальнейшем сравнении эталонного отпечатка с отпечатком, полученным из актуального состояния EEPROM. В случае несовпадения отпечатков можно однозначно делать вывод о нарушении целостности памяти и запускать заранее подготовленную реакцию на эту ситуацию. |
CRC (англ. Cyclic redundancy check, циклический избыточный код) — алгоритм нахождения контрольной суммы, предназначенный для проверки целостности данных
|
30 |
Для ускорения вычисления CRC используется таблица с заранее просчитанными «магическими» числами: |
|
31 | Arduino (C++) |
1 2 3 4 5 6 const unsigned long crc_table[16] = {
0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c
}; |
|
32 |
Можно привести 2 основных примера применения. Во-первых, удобно заранее посчитать CRC нетронутой памяти, для того, чтобы при идентификации первого запуска устройства, EEPROM заполнялась значениями по умолчанию. При последующих включения, процедура предварительного заполнения будет пропущена. |
|
33 |
Получение CRC EEPROM в «нетронутом», первоначальном состоянии выглядит так: |
|
34 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <EEPROM.h> // Импортируем бмблиотеку
const unsigned long crc_table[16] = { // Таблица заранее просчитанных констант для ускорения расчета CRC
0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c
};
void setup() {
Serial.begin(9600);
for (int i=0; i<EEPROM.length(); i++) { // Обнуляем EEPROM - приводим в первоначальное состояние
EEPROM.update(i,255);
}
Serial.println("Отпечаток EEPROM (CRC):");
Serial.println(eeprom_crc(), HEX); // В HEX-формате
Serial.println(eeprom_crc(), DEC); // В чиловом формате
}
unsigned long eeprom_crc(void) { // Функция вычисления CRC EEPROM
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;
}
void loop() {} |
|
36 |
Теперь, зная CRC нетронутой EEPROM, нетрудно инициализировать её первоначальными значениями: |
|
37 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <EEPROM.h> // Импортируем бмблиотеку
const unsigned long crc_table[16] = { // Таблица заранее просчитанных констант для ускорения расчета CRC
0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c
};
const unsigned long freeEEPROM = 4288708479; // Значение CRC неинициализированной памяти
void setup() {
Serial.begin(9600);
long actualEEPROM = eeprom_crc(); // Получаем актуальное состояние EEPROM при каждом запуске
while (actualEEPROM == freeEEPROM) { // Пока отпечатки совпадают, память пуста и её нужно инициализировать
Serial.println("EEPROM не инициализирована");
initializeEEPROM();
actualEEPROM = eeprom_crc(); // Перерасчитываем CRC
}
// Программа не продолжит выполнение, пока EEPROM не будет инициализирована
Serial.println("EEPROM инициализирована успешно");
}
void initializeEEPROM() { // Процедура заполнения EEPROM начальными значениями
Serial.println("Инициализируем EEPROM");
EEPROM.update(0, 0);
}
unsigned long eeprom_crc(void) { // Функция вычисления CRC EEPROM
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;
}
void loop() {} |
|
38 |
Только при первом запуске будет выведено сообщение об инициализации EEPROM. |
|
40 |
Все последующие будут информировать о том, что EEPROM уже инициализирован. |
|
41 |
Во-вторых, при хранении в EEPROM статических неизменяемых параметров, зная отпечаток EEPROM, при сравнении с эталоном легко идентифицировать нарушение целостности хранимых в EEPROM данных. Получим CRC необходимых данных: |
|
42 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <EEPROM.h> // Импортируем бмблиотеку
const unsigned long crc_table[16] = { // Таблица заранее просчитанных констант для ускорения расчета CRC
0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c
};
void setup() {
Serial.begin(9600);
for (int i=0; i<EEPROM.length(); i++) { // Обнуляем EEPROM - приводим в первоначальное состояние
EEPROM.update(i,255);
}
EEPROM.update(0,100); // Устанавливаем первоначальные параметры, для расчета эталонной CRC
EEPROM.update(1,50); // 2 параметр
EEPROM.update(2,25); // 3 параметр
EEPROM.update(3,0); // 4 параметр
Serial.println("Отпечаток EEPROM (CRC):");
Serial.println(eeprom_crc(), HEX); // В HEX-формате
Serial.println(eeprom_crc(), DEC); // В чиловом формате
}
unsigned long eeprom_crc(void) { // Функция вычисления CRC EEPROM
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;
}
void loop() {} |
|
44 |
Теперь остается сравнить актуальное состояние EEPROM с заданным и запустить сценарий — либо инициализация, либо авария: |
|
45 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <EEPROM.h> // Импортируем бмблиотеку
const unsigned long crc_table[16] = { // Таблица заранее просчитанных констант для ускорения расчета CRC
0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c
};
const unsigned long needEEPROM = 1607697274; // Значение CRC инициализированной памяти
void setup() {
Serial.begin(9600);
long actualEEPROM = eeprom_crc(); // Получаем актуальное состояние EEPROM при каждом запуске
while (actualEEPROM != needEEPROM) { // Если отпечатки не совпадают, нарушена целостность EEPROM
Serial.println("EEPROM не соответствует заданному значению");
// Здесь принимается решение, либо об инициализации...
initializeEEPROM();
// либо аварийное предупреждение
// ТРЕВОГА!
actualEEPROM = eeprom_crc(); // Перерасчитываем CRC
Serial.println(actualEEPROM, DEC); // В числовом формате
}
// Программа не продолжит выполнение, пока EEPROM не будет инициализирована
Serial.println("Целостность EEPROM не нарушена");
}
void initializeEEPROM() { // Процедура заполнения EEPROM начальными значениями
Serial.println("Инициализируем EEPROM");
for (int i = 0; i < EEPROM.length(); i++) { // Обнуляем EEPROM - приводим в первоначальное состояние
EEPROM.update(i, 255);
}
EEPROM.update(0, 100); // Устанавливаем первоначальные параметры, для расчета эталонной CRC
EEPROM.update(1, 50); // 2 параметр
EEPROM.update(2, 25); // 3 параметр
EEPROM.update(3, 0); // 4 параметр
}
unsigned long eeprom_crc(void) { // Функция вычисления CRC EEPROM
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;
}
void loop() {} |
|
47 |
Плюшки При разработке устройств, для чтения и записи значений удобно использовать макросы (директива #define), в том числе и с параметрами. При таком подходе, при смене адреса ячейки памяти, будет удобно изменить её в скетче только в одном месте. Также удобно записать макросы для всех параметров, которые сохраняются в EEPROM, в одном месте. |
|
48 |
В скетче демонстрируется создание именованных макросов, для осуществления чтения/записи значений из/в EEPROM наглядным способом, без указания адресов при каждом обращении. При чем показаны макросы как для простых однобайтовых значений типа byte, так и сложных типов, занимающих в EEPROM более 1 байта: |
|
49 | Arduino (C++) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <EEPROM.h> // Импортируем бмблиотеку
// Макросы чтения параметром из EEPROM
#define LOAD_VALUE_PARAMNAME_1 EEPROM.read(0) // Чтение значения типа byte (0-255)
#define LOAD_VALUE_PARAMNAME_2(value) EEPROM.get(1, value) // Чтение значения типа long
#define LOAD_VALUE_PARAMNAME_3(value) EEPROM.get(5, value) // Чтение значения типа String
// Макросы записи параметров в EEPROM
#define SAVE_VALUE_PARAMNAME_1(value) EEPROM.update(0, value) // Запись значения тип byte (0-255)
#define SAVE_VALUE_PARAMNAME_2(value) EEPROM.put(1, value) // Запись значения типа long
#define SAVE_VALUE_PARAMNAME_3(value) EEPROM.put(5, value) // Запись значения типа char[2]
byte paramname1 = 12;
long paramname2 = -213841524;
String paramname3 = "Hello";
void setup() {
Serial.begin(9600);
// Теперь для манипуляций с EEPROM не нужно знать адреса хранения
// Намного проще пользоваться заранее определенными именами переменных
saveParams(); // После изменения параметры можно сохранить
loadParams(); // Загружаем данные из EEPROM
showParams(); // Показываем параметры в мониторе порта
paramname1 = 154; // Изменяем параметры
paramname2 = 4858 / 2;
paramname3 = "New value";
saveParams(); // После изменения параметры можно сохранить
loadParams(); // Загружаем данные из EEPROM
showParams(); // Показываем новые параметры в мониторе порта
}
void loadParams() {
paramname1=LOAD_VALUE_PARAMNAME_1;
LOAD_VALUE_PARAMNAME_2(paramname2);
LOAD_VALUE_PARAMNAME_3(paramname3);
}
void saveParams() {
SAVE_VALUE_PARAMNAME_1(paramname1);
SAVE_VALUE_PARAMNAME_2(paramname2);
SAVE_VALUE_PARAMNAME_3(paramname3);
}
void showParams() {
Serial.println("param1:" + String(paramname1) +
", param2:" + String(paramname2) +
", param3:" + String(paramname3));
}
void loop() {} |
|
51 | На заметку: |
Необходимо помнить, что тип данных String не имеет фиксированного объема и меняется в зависимости от присвоенного значения. Таким образом, чтобы не происходило затирания данных, следующих в адресном пространстве следом за переменной типа String, нужно в ручном режиме контролировать её размер.
|
|
53 |
Похожие запросы:
|
|