Arduino сохранение переменной в памяти

Обновлено: 19.05.2024

Сколько памяти у Ардуино?

Размер памяти у микроконтроллера Atmega328:

  • Flash: 32 кб;
  • SRAM: 2 кб;
  • EEPROM: 1 кб;
  • регистры: в сумме 256 байт.

Для сравнения, у Ардуино Мега памяти гораздо больше:

  • Flash: 256 кб;
  • SRAM: 8 кб;
  • EEPROM: 4 кб.

Как видим, EEPROM в Arduino Уно всего 1 килобайт. Что можно хранить в таком объеме? Предположим, мы собираемся хранить значение ручек регулировки, подключенных к аналоговым входам Ардуино. Каждая такая ручка дает число от 0 до 1024 и требует 2 байта для хранения (по факту 1 байт и 2 бита, но для простоты используем 2б ), получается в EEPROM мы можем хранить значения 512 ручек настройки!

В общем, для большинства DIY проектов 1 кб EEPROM более чем достаточно. На этом уроке мы научимся записывать данные в EEPROM и считывать их оттуда.

Программа

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

Для работы с EEPROM используется стандартная библиотека EEPROM.h. Запись и чтения в эту память ведется побайтово. Это значит, что за раз мы можем записать и считать число от 0 до 255. Запись одного байта осуществляется с помощью функции write:

Чтение байта осуществляется с помощью функции read:

Итак, пишем программу.

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

Настроив таким образом все три числа, отключим Ардуино от компьютера, а затем снова включим и откроем монитор последовательного порта.

Работа с EEPROM на Ардуино

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

Надо заметить, что put и get умеют работать не только с естественными типами int, float, long, char, byte, но и со структурами. Для оценки их размера лучше использовать упомянутую функцию sizeof.

У плат семейства плат Arduino есть несколько видов памяти. Во-первых, это статическое ОЗУ (оперативное запоминающее устройство), которая используется для хранения переменных в процессе выполнения программы. Во-вторых, это флеш-память, в которой хранятся написанные вами скетчи. И в-третьих, это EEPROM, которую можно использовать для постоянного хранения информации. Первый тип памяти – энергозависимый, он теряет всю информацию после перезагрузки Arduino. Вторые два типа памяти хранят информацию пока она не будет перезаписана новой, даже после отключения питания. Последний тип памяти – EEPROM – позволяет записывать данные, хранить их и считывать при необходимости. Эту память мы и рассмотрим сейчас.

1 Описание памяти EEPROM

EEPROM означает Electrically Erasable Programmable Read-Only Memory, т.е. электрически стираемое перепрограммируемое постоянное запоминающее устройство. Данные в этой памяти могут храниться десятки лет после отключения питания. Количество циклов перезаписи – порядка нескольких миллионов раз.

  • для плат, основанных на микроконтроллере ATmega328 (например, Arduino UNO и Nano), количество памяти составляет 1 кБ,
  • для плат на ATmega168 и ATmega8 – 512 байт,
  • на ATmega2560 и ATmega1280 – 4 кБ.

2 Библиотека EEPROM

ФункцияНазначение
read(address)считывает 1 байт из EEPROM ; address – адрес, откуда считываются данные (ячейка, начиная с 0);
write(address, value)записывает в память значение value (1 байт, число от 0 до 255) по адресу address;
update(address, value)заменяет значение value по адресу address, если её старое содержимое отличается от нового;
get(address, data)считывает данные data указанного типа из памяти по адресу address;
put(address, data)записывает данные data указанного типа в память по адресу address;
EEPROM[address]позволяет использовать идентификатор "EEPROM" как массив, чтобы записывать данные в память и считывать их из памяти.

3 Запись целых чисел в EEPROM

Если число больше, чем 255, то с помощью операторов highByte() и lowByte() его нужно делить на байты и записывать каждый байт в свою ячейку. Максимальное число при этом – 65536 (или 2 16 ).

Смотрите, монитор последовательного порта в ячейку 0 просто выводит число, меньшее, чем 255. В ячейках 1 и 2 хранится большое число 789. При этом ячейка 1 хранит множитель переполнения 3, а ячейка 2 – недостающее число 21 (т.е. 789 = 3×256 + 21).

Запись целых чисел в EEPROM Arduino

Запись целых чисел в EEPROM Arduino

Чтобы заново «собрать» большое число, разобранное на байты, есть функция word(): int val = word(hi, low), где "hi" и "low" – это значения старшего и младшего байтов числа "val".

Во всех остальных ячейках, которые не были нами ни разу записаны, хранятся числа 255.

4 Запись чисел с плавающей запятой и строк в EEPROM

Для записи чисел с плавающей запятой и строк нужно использовать метод EEPROM.put(), а для чтения – EEPROM.get().

В процедуре setup() сначала запишем число с плавающей запятой "f". Затем сдвинемся на количество ячеек памяти, которое занимает тип "float", и запишем строку символов "char" ёмкостью 20 ячеек.

В процедуре loop() будем считывать все ячейки памяти и пытаться расшифровать их сначала как тип "float", а затем как тип "char", и выводить результат в последовательный порт.

Запись чисел с плавающей запятой в EEPROM Arduino

Запись чисел с плавающей запятой в EEPROM Arduino

Видно, что значение в ячейках с 0 по 3 правильно определилось как число с плавающей точкой, а начиная с 4-ой – как строка.

Появляющиеся значения ovf (переполнение) и nan (не число) говорят о том, что значение в ячейке памяти по этому адресу не может быть корректно преобразовано в число с плавающей точкой. Если вы точно знаете, какого типа данные какие ячейки памяти занимают, то у вас не будет возникать проблем.

5 Работа с EEPROM как с массивом

Очень удобная возможность – обращение к ячейкам памяти как к элементам массива EEPROM. В данном скетче в процедуре setup() мы сначала запишем данные в 4 первых байта, а в процедуре loop() ежеминутно будем считывать данные из всех ячеек и выводить их в последовательный порт.

Работа с ячейками памяти EEPROM Arduino как с элементами массива

Работа с ячейками памяти EEPROM Arduino как с элементами массива

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

Предположим, у нас есть переменная, и мы хотим сохранить ее в EEPROM. Казалось бы, все инструменты для этого у нас в руках:


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

3. Как сделать так, чтобы незавершенная операция записи оставляла старое сохраненное значение неизменным? То есть, операция сохранения должна либо успешно завершиться, либо не должна иметь какого либо наблюдаемого эффекта вообще. Иначе говоря, она должна быть атомарной или транзакционной, если мы говорим о транзакции, которая сводится к безусловному обновлению одного единственного значения. Очевидно, что мы не можем обеспечить атомарность записи, переписывая предыдущее значение, мы должны писать на новое место, чтобы старое сохраненное значение осталось нетронутым по крайней мере вплоть до завершения записи нового. Эта техника часто называется 'копирование при записи', если обновляется лишь часть сохраненного значения, но часть, оставшаяся неизменной, все равно копируется и записывается на новое место. Развивая нашу аналогию, мы будем писать письма самому себе, оставляя старые нетронутыми, но снабжая каждое письмо увеличивающимся порядковым номером, чтобы в следующей жизни у нас была возможность найти последнее написанное письмо. При этом, однако, возникает новая проблема — место в ящике, куда мы кладем письма, рано или поздно закончится, если мы не будем выбрасывать старые письма, ставшие неактуальными. Нетрудно понять, что достаточно хранить всего 2 письма — одно старое и одно новое, оно может быть в процессе написания. Соответственно, для номера письма тоже не нужно много бит.

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

Желающие вникнуть в детали реализации могут посмотреть картинки и код в репозитории. Я же, дабы не утомлять читателя, перейду к использованию. Сами функции записи / чтения данных получают по 5 параметров, так что удобством их использования пожертвовано в пользу гибкости. Зато это щедро скомпенсировано двумя наборами макросов, которые делают использование библиотеки таким же простым, как и в случае с EEPROM.get/put. Первый набор макросов используется, если вы просто хотите сохранить переменную по заданному адресу:


Если сохраняемых переменных несколько, то придется каждой определить адрес и при этом корректно учесть размер, дабы области памяти, где сохраняются переменные, не пересекались. Чтобы упростить задачу, второй набор макросов реализует автоматическое распределение адресов, причем делает это во время компиляции. К примеру, библиотека Arduino-EEPROMEx умеет распределять память во время исполнения, при этом она хранит адрес в оперативной памяти для каждой сохраняемой переменной. Библиотека NvTx распределяет место в EEPROM не добавляя ничего ни к исполняемому коду, ни к содержимому оперативной памяти.


Макрос NvPlace задает начальный адрес области EEPROM, где мы будем сохранять переменные, и идентификатор экземпляра. Макрос NvAfter резервирует область памяти для сохранения своего первого аргумента сразу после области памяти, отведенной для второго. При распределении памяти также проверяется, что мы не вышли за пределы доступного размера EEPROM, а также то, что мы не зарезервировали пересекающиеся области памяти (это может произойти, если два макроса NvAfter имеют совпадающий второй аргумент). В случае нарушения любого из двух указанных условий программа просто не скомпилируется. Желающие разобраться с механизмом распределения памяти найдут его в заголовочном файле NvTx.h. Все, что делают макросы NvPlace и NvAfter, — определяют перечисления, формируя их имена на основе имен переменных, а также используют весьма полезную идиоматическую конструкцию валидатор времени компиляции (compile time assert).

Надеюсь, библиотека NvTx поможет читателям в написании надежного кода промышленного качества.


Тип Чтение из программы Запись из программы Очистка при перезагрузке
Flash Да, PROGMEM Можно, но сложно Нет
SRAM Да Да Да
EEPROM Да Да Нет

EEPROM представляет собой область памяти, состоящую из элементарных ячеек с размером в один байт (как SRAM). Объём EEPROM разный у разных моделей МК:

  • ATmega328 (Arduino UNO, Nano, Pro Mini): 1 кБ
  • ATmega2560 (Arduino Mega): 4 кБ
  • ATtiny85 (Digispark): 512 Б

Важный момент: все ячейки имеют значение по умолчанию (у нового чипа) 255.

Скорость работы с EEPROM (время не зависит от частоты системного клока):

Возможны искажения при записи данных в EEPROM при слишком низком VCC (напряжении питания), настоятельно рекомендуется использовать BOD или вручную мониторить напряжение перед записью.

При использовании внутреннего тактового генератора на 8 МГц, его отклонение не должно быть выше 10% (7.2-8.8 МГц), иначе запись в EEPROM или FLASH скорее всего будет производиться с ошибками. Соответственно все разгоны внутреннего клока недопустимы при записи EEPROM или FLASH.

Библиотека avr/eeprom.h

Запись:

Обновление:

Макросы:

Рассмотрим простой пример, в котором происходит запись и чтение единичных типов данных в разные ячейки:

Точно так же можно хранить массивы:

Ну и напоследок, запись и чтение блока через EEMEM. Адрес придётся преобразовать в (const void*) вручную:

Библиотека EEPROM.h

В отличие от avr/eeprom.h у нас нет отдельных инструментов для работы с конкретными типами данных, отличными от byte, и сделать write/update/read для float/long/int мы не можем. Но зато у нас есть всеядный put/get, который очень удобно использовать! Также можем пользоваться тем, что нам даёт avr/eeprom.h, которая подключается автоматически с EEPROM.h. Рассмотрим пример с чтением/записью байтов:

Логика работы с адресами такая же, как в предыдущем пункте урока! Обратите внимание на работу с EEPROM как с массивом, можно читать, писать, сравнивать, и даже использовать составные операторы, например EEPROM[0] += 10 , но это работает только для элементарных ячеек, байтов. Теперь посмотрим, как работает put/get:

Гораздо удобнее чем write_block и read_block, не правда ли? Put и get сами преобразовывают типы и сами считают размер блока данных, использовать их очень приятно. Они работают как с массивами, так и со структурами.

EEPROM.h + avr/eeprom.h

Ну и конечно же, можно использовать одновременно все преимущества обеих библиотек, например автоматическую адресацию EEMEM и put/get. Рассмотрим на предыдущем примере, вместо ручного задания адресов используем EEMEM, но величину придётся привести к целочисленному типу, сначала взяв от него адрес, т.е. (int)&адрес_еемем

С возможностями библиотек разобрались, перейдём к практике.

Реальный пример

Рассмотрим пример, в котором происходит следующее: две кнопки управляют яркостью светодиода, подключенного к ШИМ пину. Установленная яркость сохраняется в EEPROM, т.е. при перезапуске устройства будет включена яркость, установленная последний раз. Для опроса кнопок используется библиотека GyverButton. Для начала посмотрите на первоначальную программу, где установленная яркость не сохраняется. Программу можно чуть оптимизировать, но это не является целью данного урока.

  • Подключить библиотеку EEPROM.h
  • При запуске: чтение яркости из EEPROM и включение светодиода
  • При клике: запись актуального значения в EEPROM

Полезные трюки

Инициализация

  1. Чтение из EEPROM в переменную
  2. Использование переменной по назначению

Рассмотрим на всё том же примере со светодиодом и кнопками:

Теперь при первом запуске мы получим инициализацию нужных ячеек. Если нужно переинициализировать EEPROM, например в случае добавления новых данных, достаточно изменить наш ключ на любое другое значение в пределах одного байта (0-254). Я пишу именно до 254, потому что 255 является значением ячейки по умолчанию и наш трюк не сработает.

Скорость

Как я писал выше, скорость работы с EEPROM составляет:

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

Для изменения значения придётся прочитать ячейку, выполнить нужные операции, и снова в неё записать. Ещё один удобный хак: можно ввести макросы на чтение и запись определённых значений, например:

Получим удобные макросы, с которыми писать код будет чуть быстрее и удобнее, т.е. строка SET_MODE(3) запишет 3 в ячейку 0

Уменьшение износа

Посмотрим на всё том же примере:

    Ёмкий конденсатор по питанию микроконтроллера, позволяющий сохранить работу МК после отключения питания на время, достаточное для записи в EEPROM (

Вариантов уменьшения износа ячеек EEPROM можно придумать много, уникально под свою ситуацию. Есть даже библиотеки готовые, например EEPROMWearLevel. Есть очень интересная статья на Хабре, там рассмотрено ещё несколько хороших алгоритмов и даны ссылки на ещё большее их количество.

Видео

Ардуино предоставляет своим пользователям три типа встроенной памяти устройств и одна из них EEPROM – энергонезависимая память.

Описание памяти EEPROM

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

Ардуино предоставляет своим пользователям три типа встроенной памяти устройств: стационарное ОЗУ (оперативно-запоминающее устройство или SRAM — static random access memory) – необходимо для записи и хранения данных в процессе использования; флеш-карты – для сохранения уже записанных схем; EEPROM – для хранения и последующего использования данных.

На ОЗУ все данные стираются, как только происходит перезагрузка устройства либо отключается питание. Вторые две сохраняют всю информацию до перезаписи и позволяют извлекать ее при необходимости. Флеш-накопители достаточно распространены в настоящее время. Подробнее стоит рассмотреть память EEPROM.

Аббревиатура расшифровывается, как Electrically Erasable Programmable Read-Only Memory и в переводе на русский дословно означает – электрически стираемая программируемая память только для чтения. Производитель гарантирует сохранность информации на несколько десятилетий вперед после последнего отключения питания (обычно приводят срок в 20 лет, зависит от скорости снижения заряда устройства).

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

Объем памяти, в сравнении с современными носителями, очень небольшой и разный для различных микроконтроллеров. Например, для:

  • ATmega328 – 1кБ
  • ATmega168 и ATmega8 – 512 байт, и ATmega1280 – 4 кБ.

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

Для записи на EEPROM требуется значительное количество времени – около 3 мс. Если в момент записи отключается питание, данные не сохраняются вовсе либо могут быть записаны ошибочно. Требуется всегда дополнительно проверять внесенную информацию, чтобы избежать сбоев во время работы. Считывание данных происходит гораздо быстрее, ресурс памяти от этого не снижается.

Библиотека

Далее используются простые команды:

  • для записи – EEPROM.write(address, data);
  • для чтения – EEPROM.read(address).

В данных скетчах: address – аргумент с данными ячейки, куда вносятся данные второго аргумента data; при считывании используется один аргумент address, который показывает, откуда следует читать информацию.

Функция Назначение
read(address) считывает 1 байт из EEPROM ; address – адрес, откуда считываются данные (ячейка, начиная с 0);
write(address, value) записывает в память значение value (1 байт, число от 0 до 255) по адресу address;
update(address, value) заменяет значение value по адресу address, если её старое содержимое отличается от нового;
get(address, data) считывает данные data указанного типа из памяти по адресу address;
put(address, data) записывает данные data указанного типа в память по адресу address;
EEPROM[address] позволяет использовать идентификатор "EEPROM" как массив, чтобы записывать данные в память и считывать их из памяти.

Запись целых чисел

Запись целых чисел в энергонезависимую память EEPROM осуществить достаточно просто. Внесение чисел происходит с запуском функции EEPROM.write(). В скобках указываются необходимые данные. При этом числа от 0 до 255 и числа свыше 255 записываются по-разному. Первые вносятся просто – их объем занимает 1 байт, то есть одну ячейку. Для записи вторых необходимо использовать операторов highByte() высший байт и lowByte() низший байт.

Число делится на байты и записывается отдельно по ячейкам. Например, число 789 запишется в две ячейки: в первую пойдет множитель 3, а во вторую – недостающее значение. В итоге получается необходимое значение:

Для «воссоединения» большого целого числа применяется функция word(): int val = word(hi, low). Нужно читывать, что максимальное целое число для записи – 65536 (то есть 2 в степени 16). В ячейках, в которых еще не было иных записей, на мониторе будут стоять цифры 255 в каждой.

Запись чисел с плавающей запятой и строк

Числа с плавающей запятой и строк – это форма записи действительных чисел, где они представляются из мантиссы и показателя степени. Запись таких чисел в энергонезависимую память EEPROM производится с активацией функции EEPROM.put(), считывание, соответственно, – EEPROM.get().

При программировании числовые значения с плавающей запятой обозначаются, как float, стоит отметить, что это не команда, а именно число. Тип Char (символьный тип) – используется для обозначения строк. Процесс записи чисел на мониторе запускается при помощи setup(), считывание – с помощью loop().

В процессе на экране монитора могут появиться значения ovf, что значит «переполнено», и nan, что значит «отсутствует числовое значение». Это говорит о том, что записанная в ячейку информация не может быть воспроизведена, как число с плавающей точкой. Такой ситуации не возникнет, если достоверно знать, в какой ячейке какой тип информации записан.

Примеры проектов и скетчей

Пример №1

Скетч запишет до 16 символов с последовательного порта и в цикле выведет 16 символов из EEPROM. Благодаря Arduino IDE данные записываются в EEPROM и контролируется содержимое энергонезависимой памяти.

Пример №3

Запись в память два целых числа, чтение их из EEPROM и вывод в последовательный порт. Числа от 0 до 255 занимают 1 байт памяти, с помощью функции EEPROM.write() записываются в нужную ячейку. Для чисел больше 255 их нужно делить на байты с помощью highByte() и lowByte() и записывать каждый байт в свою ячейку. Максимальное число при этом – 65536 (или 2 16 ).

Пример №4

Запись чисел с плавающей запятой и строк - метод EEPROM.put(). Чтение – EEPROM.get().

Пример №5

Использование EEPROM как массива.

Работа с EEPROM

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

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

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

Существует несколько ситуаций, когда память EEPROM содержит неправильные данные:

  1. При первоначальной активации – еще не было ни одной записи.
  2. В момент неконтролируемого отключения питания – часть или все данные не запишутся или запишутся некорректно.
  3. После завершения возможных циклов перезаписи данных.

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

Для контроля целостности информации используют контрольный код системы. Он создается по образцу записи первоначальных данных и, при проверке, он вновь просчитывает данные. Если результат отличается – это ошибка. Самым распространенным вариантом такой проверки является контрольная сумма – выполняется обычная математическая операция по сложению всех значений ячеек.

Опытные программисты добавляют к этому коду дополнительное «исключающее ИЛИ», например, E5h. В случае если все значения равны нулю, а система по ошибке обнулила исходные данные – такая хитрость выявит ошибку.

Таковы основные принципы работы с энергонезависимой памятью EEPROM для микроконтроллеров Arduino. Для определенных проектов стоит использовать только этот вид памяти. Он имеет как свои плюсы, так и свои недостатки. Для освоения методов записи и чтения лучше начать с простых задач.

Читайте также: