Инициализация eeprom что это
Перейти к содержимому

Инициализация eeprom что это

  • автор:

Введение

При программировании микроконтроллеров AVR иногда возникает потребность сохранять данные, которые бы после выключения питания или сброса контроллера не изменяли свое значение. Для этих целей в составе AVR есть энергонезависимая память данных EEPROM (Electrically Erasable Programmable Read-Only Memory — электрически стираемое перепрограммируемое ПЗУ).
EEPROM имеет адресное пространство отличное от адресных пространств ОЗУ и flash памяти, в котором можно читать и записывать одиночные байты. В зависимости от модели микроконтроллера EEPROM может иметь объем от 512 байт (как, например, в микроконтроллере atmega16) до нескольких килобайт. Гарантированное количество циклов перезаписи этой памяти составляет не меньше 100000.
В этой статье на примере atmega16 мы разберемся, как работать с этим типом памяти, какие возможные проблемы при этом могут возникать и как с ними бороться.

Объявление переменных в EEPROM

Использование EEPROM начинается с объявления переменных, хранящиеся в этой памяти. Синтаксис объявления таких переменных отличается от объявлений обычных переменных (хранящихся в ОЗУ) только наличием ключевого слова. В зависимости от компилятора данное ключевое слово может разным.

Объявление переменной в EEPROM для IAR AVR и CodeVision AVR:

Объявление переменной в EEPROM для AtmelStudio 6:

Инициализация переменных в EEPROM

При объявлении переменных в EEPROM их можно инициализировать, то есть присвоить начальные значения.

Инициализацию переменных хранящихся в ОЗУ компилятор «запихивает» в начало программы микроконтроллера — перед вызовом функции main. И она выполняется каждый раз, когда на микроконтроллер подается питание или происходит его сброс.
С EEPROM переменными ситуация немного другая, их должен инициализировать сам пользователь путем программирования EEPROM специальным файлом (с расширением .eep).
Как сгенерировать файл для EEPROM? Если в коде есть инициализация EEPROM переменных, то AtmelStudio 6 и CodeVision AVR создадут этот файл автоматически. А вот в IAR`e для этого нужно прописывать линкеру команды. Делается это так.
Меню Project > Options. >Linker вкладка Output. Устанавливаем значения по умолчанию — галочка Override default снята, формат — Debug information for C-SPY. На вкладке Extra Options ставим галочку Use Command Options и в окошке ниже прописываем такие строчки:

После компиляции и сборки проекта IAR создаст файл прошивки — .hex и файл для EEPROM`a — .eep

Полученный eep файл записывается с помощью программатора в микроконтроллер.

Чтение и запись EEPROM

В IAR`e и CodeVision AVR использование EEPROM переменных по сути ничем не отличается от использования обычных переменных (хранящихся в ОЗУ и регистрах). Вся работа по организации чтения и записи в эти EEPROM переменные выполняется компилятором.

В AtmelStudio для чтения/записи EEPROM переменных используются специальные макросы. Они определены в файле eeprom.h. Вот некоторые из них:

Макросы принимают в качестве параметра адрес переменной размещенной в EEPROM. Для взятия адреса переменной используется оператор &. Примеры ниже поясняют использование этих макросов.

Заключение

Из этой статьи вы узнали:

— как объявить переменную в EEPROM памяти,
— как создать файл для инициализации EEPROM,
— как прочитать и записать данные в EEPROM.

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

Работа с EEPROM памятью

Вот и добрались мы до третьего типа памяти, доступного на Arduino: EEPROM (англ. Electrically Erasable Programmable Read-Only Memory – электрически стираемое перепрограммируемое постоянное запоминающее устройство (ЭСППЗУ)), она же энергонезависимая память. Вспомним остальные типы памяти, Flash и SRAM, и их возможности по хранению данных:

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

Простыми словами: EEPROM – память, к которой мы имеем полный доступ из выполняющейся программы, т.е. можем во время выполнения читать и писать туда данные, и эти данные не сбрасываются при перезагрузке МК. Круто? Круто. Зачем?

  • Хранение настроек, изменяющихся “из меню” устройства, без перепрошивки;
  • Калибровка, сохранение калибровочных данных;
  • Использование как дополнительной SRAM памяти в случае её нехватки;
  • “Чёрный ящик” – постоянная запись показаний с датчиков для дальнейшей расшифровки сбоев;
  • Запись состояния рабочего процесса для восстановления работы после внезапной перезагрузки.

Единственный важный момент: EEPROM имеет ресурс по количеству перезаписи ячеек. Производитель гарантирует 100 000 циклов записи каждой ячейки (AVR Arduino), по факту это количество зависит от конкретного чипа и температурных условий, независимые тесты показали 3-6 миллионов циклов перезаписи при комнатной температуре до появления первой ошибки, т.е. заявленные 100 000 взяты с очень большим запасом. Но есть небольшое уточнение – при заявленных 100 000 циклах перезаписи гарантируется сохранность записанных данных в течение 100 лет при температуре 24°C, если перезаписывать по миллиону – данные испортятся быстрее. В то же время количество чтений каждой ячейки неограниченно.

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

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

Основная задача при работе с EEPROM – не напутать с адресами, потому что каждый байт имеет свой адрес. Если вы пишете двухбайтные данные, то они займут два байта, и следующие данные нужно будет писать по адресу как минимум +2 к предыдущему, иначе они “перемешаются”. Рассмотрим пример хранения набора данных разного типа, расположенных в памяти последовательно друг за другом (в скобках я пишу размер текущего типа данных, на размер которого увеличится адрес для следующего “блока”):

  • byte – адрес 0 (+1)
  • byte – адрес 1 (+1)
  • int – адрес 2 (+2) (+4 для esp8266)
  • byte – адрес 4 (+1)
  • float – адрес 5 (+4)
  • int – адрес 9 (+2)
  • и так далее

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

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

    Запись одного байта занимает

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

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

Для работы с EEPROM в среде Arduino у нас есть целых две библиотеки, вторая является более удобной “оболочкой” для первой. Рассмотрим их обе, потому что в “чужом скетче” может встретиться всё что угодно, да и совместное использование этих двух библиотек делает работу с EEPROM невероятно удобной.

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

Описание к этой библиотеке я спрятал под спойлер, потому что она не очень актуальна и знать о ней необязательно. Также она не работает на esp8266/32 по понятным причинам.

Стандартная библиотека eeprom.h идёт в комплекте с компилятором avr-gcc, который компилирует наши скетчи из под Arduino IDE. Полную документацию можно почитать здесь. Для подключения библиотеки в скетч пишем #include <avr/eeprom.h> Библиотека имеет набор функций для работы с целочисленными типами данных ( byte – 1 байт, word – 2 байта, dword – 4 байта), float , и block “блоков” – наборов данных любого формата (структуры, массивы, и т.д.). Под работой подразумевается запись, чтение и обновление. Обновление – крайне важный инструмент, позволяющий избежать лишних перезаписей ячеек памяти. Обновление делает запись, если записываемое значение отличается от текущего в этой ячейке. Чтение:

  • eeprom_read_byte(адрес) – вернёт значение
  • eeprom_read_word(адрес) – вернёт значение
  • eeprom_read_dword(адрес) – вернёт значение
  • eeprom_read_float(адрес) – вернёт значение
  • eeprom_read_block(адрес в SRAM, адрес в EEPROM, размер) – прочитает содержимое по адрес в EEPROM в адрес в SRAM

Запись:

  • eeprom_write_byte(адрес, значение)
  • eeprom_write_word(адрес, значение)
  • eeprom_write_dword(адрес, значение)
  • eeprom_write_float(адрес, значение)
  • eeprom_write_block(адрес в SRAM, адрес в EEPROM, размер) – запишет содержимое по адрес в SRAM в адрес в EEPROM

Обновление:

  • eeprom_update_byte(адрес, значение)
  • eeprom_update_word(адрес, значение)
  • eeprom_update_dword(адрес, значение)
  • eeprom_update_float(адрес, значение)
  • eeprom_update_block(адрес в SRAM, адрес в EEPROM, размер) – обновит содержимое по адрес в SRAM в адрес в EEPROM

Макросы:

  • _EEPUT(addr, val) – записывает (write) байт val по адресу addr . Приведение типов не требуется (оно сделано в макросе)
  • _EEGET(val, addr) – читает байт по адресу addr и записывает его в переменную val . Приведение типов не требуется (оно сделано в макросе)

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

Хранить данные таким образом не очень удобно, потому что менеджмент адресов приходится проводить вручную, считать количество байт в каждом типе и “сдвигать” адрес на нужное количество. Гораздо удобнее хранить разношёрстные данные в структурах, про них мы подробнее говорили в уроке про типы данных. Мы должны передать функции адрес данных в памяти (оператор & ), по сути – указатель, а также преобразовать его к типу void* , потому что функция чтения/записи блока принимает именно такой тип. Подробнее про указатели мы говорили в отдельном уроке. Также функции чтения/записи блока нужно передать размер блока данных в количестве байт. Это можно сделать вручную (числом), но лучше использовать sizeof() , которая посчитает этот размер и передаст в функцию.

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

В библиотеке avr/eeprom.h есть ещё один очень полезный инструмент – EEMEM, он позволяет сделать автоматическую адресацию данных путём создания указателей, значение которым присвоит компилятор. Рассмотрим пример, в котором запишем в EEPROM несколько переменных, структуру и массив, раздав им автоматически адреса. Важный момент! Адреса задаются снизу вверх по порядку объявления EEMEM, я подпишу их в примере:

EEMEM сам раздаёт адреса, основываясь на размере данных. Важный момент: данный подход не занимает дополнительного места в памяти, т.е. нумерация адресов вручную цифрами, без создания EEMEM “переменных”, не занимает меньше памяти! Давайте вернёмся к нашему первому примеру и перепишем его с EEMEM. При указании адреса через EEMEM нужно использовать оператор взятия адреса &

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

Таким образом можно добавлять “данные” для хранения в EEPROM прямо по ходу разработки программы, не думая об адресах. Рекомендую добавлять новые данные над старыми, чтобы адресация не сбивалась (напомню, адресация идёт снизу вверх, начиная с нуля).

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

Библиотека EEPROM.h идёт в комплекте с ядром Arduino и является стандартной библиотекой. По сути EEPROM.h – это удобная оболочка для avr/eeprom.h, чуть расширяющая её возможности и упрощающая использование.

Рассмотрим инструменты, которые нам предлагает библиотека:

  • EEPROM.write(адрес, данные) – пишет данные ( только byte! ) по адресу
  • EEPROM.update(адрес, данные) – обновляет (та же запись, но лучше) байт данных, находящийся по адресу. Не реализована для esp8266/32!
  • EEPROM.read(адрес) – читает и возвращает байт данных, находящийся по адресу
  • EEPROM.put(адрес, данные) – записывает (по факту – обновляет , update) данные любого типа (типа переданной переменной) по адресу
  • EEPROM.get(адрес, данные) – читает данные по адресу и сам записывает их в данные – указанную переменную
  • EEPROM[] – библиотека позволяет работать с EEPROM памятью как с обычным массивом типа byte ( uint8_t )

У esp8266 и esp32 есть отличия:

  • Перед началом работы нужно вызвать EEPROM.begin(размер) с указанием максимального объёма памяти: 4.. 4096 Байт.
  • Для применения записи нужно вызвать EEPROM.commit() : например несколько раз делается write(), put(), и в завершение – commit()
  • В некоторых версиях SDK отсутствует EEPROM.update()

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

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

Теперь посмотрим, как работает put() и get() :

put() и get() сами определяют тип данных и считают размер блока данных, использовать их очень приятно. Они работают как с массивами, так и со структурами.

EEPROM.h + структуры

Самый удобный хранить набор данных в EEPROM – структура (разбирали в уроке про типы данных). Структура позволяет объединить любые данные под одним именем, и одной строчкой загонять их в EEPROM и так же читать обратно. А также не придётся думать об адресации! Пример:

EEPROM.h + avr/eeprom.h

Пример не очень актуален, используй EEPROM + структуры

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

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

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

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

В этот код нам нужно добавить:

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

Итак, теперь при запуске у нас восстанавливается последняя настроенная яркость, и при изменении она записывается. Напомню, что EEPROM изнашивается от перезаписи. Конечно для того, чтобы “накликать” яркость несколько миллионов раз и убить ячейку, у вас уйдёт очень много времени, но процесс записи нового значения можно и нужно оптимизировать, особенно в более серьёзных проектах, ниже поговорим об этом подробнее. Также в нашем коде есть ещё один неприятный момент: при самом первом запуске после прошивки EEPROM не инициализирована, каждая ячейка хранит в себе число 255, и именно такое значение примет переменная LEDbright после первого запуска, при так называемом “первом чтении”. Здесь это не имеет значения, но в более серьёзном устройстве нужно будет задать нужные значения по умолчанию в EEPROM при первом запуске, об этом мы тоже поговорим ниже. Иначе представьте, какие “настройки по умолчанию” получит ваше устройство для яркости/скорости/громкости/номера режима/прочее!

Запись и чтение строк

У нас есть два типа строк: массивы символов и String-строки. С массивом символов всё более-менее понятно: это массив, он имеет фиксированный размер, его можно записать при помощи put() и прочитать при помощи get() . Также такая строка может входить в структуру, что очень удобно. В этом случае нужно объявить структуру с указанием максимальной длины строки, которая может там храниться. Например для какого-нибудь проекта с WiFi нам хочется хранить логин и пароль от роутера и режим работы:

Структуру очень просто прочитать и записать, а работать со строками в ней можно при помощи стандартных строковых функций.

А как записать и прочитать динамические данные, такие как String-строки? Можно рассмотреть два способа: с массивом ограниченной длины (как в примере выше) и полностью динамическое хранение.

Будем считать, что максимальная длина строки – 20 символов. Простой пример:

При динамическом хранении мы будем сохранять также длину строки, в первой ячейке от которой идёт счёт. А уже дальше – саму строку. Писать и читать будем посимвольно, по другому тут уже не получится:

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

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

Под инициализацией я имею в виду установку значений ячеек в EEPROM “по умолчанию” во время первого запуска устройства. В рассмотренном выше примере мы действовали в таком порядке:

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

При первом запуске кода (и при всех дальнейших, в которых в ячейку ничего нового не пишется) переменная получит значение, которое было в EEPROM по умолчанию. В большинстве случаев это значение не подойдёт устройству, например ячейка хранит номер режима, по задумке разработчика – от 0 до 5, а из EEPROM мы прочитаем 255. Непорядок! При первом запуске нужно инициализировать EEPROM так, чтобы устройство работало корректно, для этого нужно определить этот самый первый запуск. Можно сделать это вручную, прошив программу, которая забьёт EEPROM нужными данными. Далее прошить уже рабочую программу. При разработке программы это очень неудобно, ведь количество сохраняемых данных может меняться в процессе разработки, поэтому можно использовать следующий алгоритм:

  1. Резервируем какую-нибудь ячейку (например, последнюю) под хранение “ключа” первого запуска
  2. Читаем ячейку, если её содержимое не совпадает с ключом – это первый запуск!
  3. В обработчике первого запуска пишем в ячейку нужный ключ
  4. Пишем в остальные ячейки необходимые значения по умолчанию
  5. И после этого уже читаем данные во все нужные переменные

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

Сброс до “заводских”

Чтобы вернуть настройки к изначально заданным в программе, нужно “спровоцировать” инициализацию. Очевидный способ сделать это – изменить ключ инициализации, который мы назвали INIT_KEY . Либо можно просто вызвать EEPROM.put(адрес, базовые настройки) в нужном месте программы.

Скорость

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

    Запись/обновление одного байта занимает

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

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

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

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

Важная тема: уменьшение износа ячеек частыми перезаписями. Ситуаций может быть много, интересных решений для них – тоже. Рассмотрим простейший пример – всё тот же код со светодиодом и кнопкой. Делать будем следующее: записывать новое значение будем только в том случае, если после последнего нажатия на кнопку прошло какое-то время. То есть нам понадобится таймер (воспользуемся таймером на millis), при нажатии на кнопку таймер будет сбрасываться, а при срабатывании таймера будем писать актуальное значение в EEPROM. Также понадобится флаг, который будет сигнализировать о записи и позволит записать именно один раз. Алгоритм такой:

  • При нажатии на кнопку:
    • Если флаг опущен – поднять флаг
    • Сбросить таймер
    • Опустить флаг
    • Записать значения в EEPROM

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

    Вот таким нехитрым способом мы многократно снизили износ EEPROM, я очень часто использую этот “алгоритм” работы с настройками в своих устройствах. Есть другие задачи, в которых данные в EEPROM пишутся не когда пользователь что-то изменит, а постоянно, т.е. память работает в режиме чёрного ящика и постоянно записывает значения. Это может быть например контроллер печи, который держит температурный режим по специальному закону, и после внезапной перезагрузки должен вернуться к тому месту в процессе, на котором прервался. Тут есть глобально два варианта:

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

    • Писать данные каждый раз в следующую ячейку, и закольцевать переход на первую. Также понадобится хранить где-то счётчик, указывающий на адрес текущей ячейки, и этот счётчик тоже придётся хранить хитро, чтобы он не износил ячейку. Например счётчик – это структура, состоящая из счётчика перезаписей этой структуры и счётчика адреса для большой структуры.
    • Писать данные, пока не достигнут лимит количества перезаписей, количество текущих перезаписей хранить например в этой же структуре. Скажем структура занимает 30 байт, то есть в перспективе мы можем найти эту структуру по адресу, кратному 30. Программа работает, счётчик считает количество перезаписей, при достижении опасного количества вся структура “переезжает” на следующие 30 адресов.

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

    Библиотека EEManager

    Я часто использую EEPROM в своих проектах, поэтому обернул все рассмотренные выше конструкции в библиотеку, изучить и скачать можно здесь. Библиотека подходит для всех архитектур, в которых есть стандартная EEPROM.h. В библиотеке реализовано:

    • Работа с данными любого типа
    • Чтение и запись в указанную переменную
    • Функция “ключа первого запуска” для задания начальных значений
    • Отложенное обновление по тайм-ауту для уменьшения износа

    Я надеюсь вы полностью разобрались с самым последним примером с кнопкой и светодиодом, поэтому покажу работу EEManager на его основе:

    Таким образом вся работа с еепром по чтению, записи, обеспечению корректного первого запуска и уменьшению износа памяти свелась к трём строчкам кода. Пользуйтесь!

    Работа с параметрами в EEPROM, как не износить память

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

    А именно поговорим о том, как хранить параметры, которые необходимо писать в EEPROM постоянно.

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

    Особенность таких параметров заключается в том, что их нельзя писать просто так в одно и то же место EEPROM, вы просто израсходуете все циклы записи EEPROM. Например, если, необходимо писать время работы один раз в 1 минуту, то нетрудно посчитать, что с EEPROM в 1 000 000 циклов записей, вы загубите его меньше чем за 2 года. А что такое 2 года, если обычное измерительное устройство имеет время поверки 3 и даже 5 лет.

    Кроме того, не все EEPROM имеют 1 000 000 циклов записей, многие дешевые EEPROM все еще производятся по старым технологиям с количеством записей 100 000. А если учесть, что 1 000 000 циклов указывается только при идеальных условиях, а скажем при высоких температурах это число может снизиться вдвое, то ваша EEPROM способно оказаться самым ненадежным элементом уже в первый год работы устройства.

    Поэтому давайте попробуем решить эту проблему, и сделать так, чтобы обращение к параметрам было столь же простым как в прошлой статье, но при этом EEPROM хватало бы на 30 лет, ну или на 100 (чисто теоретически).

    Итак, в прошлой статье, я с трудом показал, как сделать, так, чтобы с параметрами в EEPROM можно было работать интуитивно понятно, не задумываясь, где они лежат и как осуществляется доступ к ним

    Для начала проясню, для чего вообще нужно обращаться по отдельности к каждому параметру, этот момент был упущен в прошлой статье. Спасибо товарищам @Andy_Big и @HiSER за замечания.

    Все очень просто, существует огромный пласт измерительных устройств, которые используют полевые протоколы такие как HART, FF или PF, где пользовательские команды очень атомарные. Например, в HART протоколе есть отдельные команды — запись единиц изменения, запись верхнего диапазона, запись времени демпфирования, калибровка нуля, запись адрес опроса и т.д. Каждая такая команда должна записать один параметр, при этом успеть подготовить ответ и ответить. Таких параметров может быть до 500 — 600, а в небольших устройствах их около 200.

    Если использовать способ, который предложил пользователь @HiSER- это будет означать, что для перезаписи одного параметра размером в 1 byte, я должен буду переписать всю EEPROM. А если алгоритм контроля целостности подразумевает хранение копии параметров, то для 200 параметров со средней длиной в 4 байта, мне нужно будет переписать 1600 байт EEPROM, а если параметров 500, то и все 4000.

    Малопотребляющие устройства или устройства, питающиеся от от токовой петли 4-20мА должны потреблять, ну скажем 3 мА, и при этом они должны иметь еще достаточно энергии для питания модема полевого интерфейса, графического индикатора, да еще и BLE в придачу. Запись в EEPROM очень энергозатратная операция. В таких устройствах писать нужно мало и быстро, чтобы средний ток потребления был не высоким.

    Очевидно, что необходимо, сделать так, чтобы микроконтроллер ел как можно меньше. Самый простой способ, это уменьшить частоту тактирования, скажем до 500 КГц, или 1 Мгц (Сразу оговорюсь, в надежных применениях использование режима низкого потребления запрещено, поэтому микроконтроллер все время должен работать на одной частоте). На такой частоте, простая передача 4000 байт по SPI займет около 70 мс, прибавим к этому задержку на сохранение данных в страницу (в среднем 7мс на страницу), обратное вычитывание, и вообще обработку запроса микроконтроллером и получим около 3 секунд, на то, чтобы записать один параметр.

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

    Но вернемся к нашей основной проблеме — мы хотим постоянно писать параметры.

    Как работать с EEPROM, чтобы не износить её

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

    Как я уже сказал, число записей в EEPROM ограничено. Это число варьируется, и может быть 100 000, а может и 1 000 000. Так как же быть, если я хочу записать параметр 10 000 000 раз? И здесь мы должны понять, как внутри EEPROM устроен доступ к ячейкам памяти.

    Итак, в общем случае вся EEPROM разделена на страницы. Страницы изолированы друг от друга. Страницы могут быть разного размера, для небольших EEPROM это, скажем, 16, 32 или 64 байта. Каждый раз когда вы записываете данные по какому-то адресу, EEPROM копирует все содержимое страницы, в которой находятся эти данные, во внутренний буфер. Затем меняет данные, которые вы передали в этом буфере и записывает весь буфер обратно. Т.е. по факту, если вы поменяли 1 байт в странице, вы переписываете всю страницу. Но из-за того, что страницы изолированы друг от друга остальные страницы не трогаются.

    Таким образом, если вы записали 1 000 000 раз в одну страницу, вы можете перейти на другую страницу и записать туда еще 1 000 000 раз, потом в другую и так далее. Т.е. весь алгоритм сводится к тому, чтобы писать параметр не в одну страницу, а каждый раз сдвигаться в следующую страницу. Можно закольцевать эти действия и после 10 раз, снова писать в исходную страницу. Таким образом, вы просто отводите под параметр 10 страниц, вместо 1.

    Да придется пожертвовать память, но как сделать по другому, я пока не знаю. Если есть какие мысли — пишите в комментариях.

    Анализ требований и дизайн

    Итак, мы почти поняли что хотим. Но давайте немного формализуем это. Для начала, назовем наши параметры, которые нужно писать постоянно — AntiWearNvData (антиизносные данные). Мы хотим, чтобы обращение к ним было такое же простое и юзер френдли, как и к кешируемым параметрам из предыдущей статьи.

    Все требования можно сформулировать следующим образом:

    Пользователь должен задать параметры EEPROM и время обновления параметра

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

    Количество циклов перезаписи

    Время обновления параметра

    Время жизни устройства

    Хотя конечно, можно было дать возможность пользователю самому задавать количество записей, но что-то я хочу, чтобы все считалось само на этапе компиляции.

    Каждая наша переменная(параметр) должна иметь уникальный начальный адрес в EEPROM

    Мы не хотим сами руками задавать адрес, он должен высчитываться на этапе компиляции

    При каждой следующей записи, адрес параметра должен изменяться, так, чтобы данные не писались по одному и тому же адресу

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

    Мы не хотим постоянно лазить в EEPROM, когда пользователь хочет прочитать параметр

    Обычно EEPROM подключается через I2C и SPI, передача данных по этим интерфейсам тоже отнимает время, поэтому лучше кэшировать параметры в ОЗУ, и возвращать сразу копию из кеша.

    При инициализации мы должны найти самую последнюю запись, её считать и закешировать.

    За целостность должен отвечать драйвер.

    За алгоритм проверки целостности отвечает драйвер, если при чтении он обнаружил несоответствие он должен вернуть ошибку. В нашем случае, пусть в качестве алгоритма целостности будет простое хранение копии параметра. Сам драйвер описывать не буду, но приведу пример кода.

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

    Класс AntiWearNvData будет похож на, CachedNvData из прошлой статьи, но с небольшими изменениям. При каждой записи в EEPROM, нам нужно постоянно сдвигать адрес записи, поэтому необходимо хранить индекс, который будет указывать на номер текущей записи. Этот индекс должен записываться в EEPROM вместе с параметром, чтобы после инициализации можно было найти запись с самым большим индексом — эта запись и будет самой актуальной. Индекс можно сделать uint32_t точно хватит на 30 лет — даже при 100 000 циклах записи.

    И вот наш класс:

    Посмотрим на то, как реализуются наши требования таким дизайном.

    Пользователь должен задать параметры EEPROM и время обновления параметр

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

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

    При каждой следующей записи, адрес параметра должен изменяться, так, чтобы данные не писались по одному и тому же адресу

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

    По нему мы будет рассчитывать следующий адрес записи

    Для того, чтобы после выключения/включения датчика найти последнюю запись, считать её и проинициализировать значением по адресу этой записи кеширумое значение в ОЗУ.

    Для этого заведена специальная структура tAntiWear . Её то мы и будем сохранять при вызове метода Set(. ) , который, кроме непосредственно записи, еще сдвигает индекс текущей записи на 1.

    Давайте посмотрим как реализован метод расчета текущего адреса записи:

    Мы не хотим постоянно лазить в EEPROM, когда пользователь хочет прочитать параметр

    Метод Get() — крайне простой, он просто возвращает копию из ОЗУ

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

    В общем-то и все класс готов, полный код класса:

    Полный код класса

    По аналогии с CachedNvData из прошлой статьи, все параметры должны быть зарегистрированы в едином списке, причем, в этом списке мы можем регистрировать как и CachedNvData , так и наши AntiWearNvData параметры.

    Я немного переделал список, так как IAR компилятор все еще не понимает много фишек из С++17, и собственно теперь список принимает только типы, а не ссылки на параметры. Кроме того, теперь у него появились методы SetToDefault и Init . Первый нужен, например, чтобы сбросить все параметры в их начальное значение. А второй, чтобы проинициализировать кешируемые в ОЗУ копии.

    Также в CachedNvData я добавил параметр recordSize и recordCounts = 1 . Чтобы расчет адреса параметра был унифицирован для разного типа параметров.

    Результат

    Собственно все, теперь мы можем регистрировать в списке любые параметры:

    Замечу, что пользователю параметров нужно только объявить параметр и список, а вся портянка с кодом, до этого, пишется один раз. Используются параметры точно также как и CachedNvData .

    Что произойдет в этом примере, когда мы будем писать 10,11,12. 15 в наш параметр. Каждый раз при записи, адрес параметра будет смещаться на размер параметра + размер индекса + размер копии параметра и индекса. Как только количество записей превысит максимальное количество, параметр начнет писаться с начального адреса.

    На картинке снизу как раз видно, что число 15 с индексом 5 записалось с начального адреса, а 10 теперь нет вообще.

    В данном случае после сброса питания, при инициализации, будет найдена запись с индексом 5 и значением 15 и это значение и индекс будут записаны в кэшируемую копию нашего параметра.

    Вот и все, надеюсь в этой статье цель получилось пояснить более детально, спасибо за то, что прочитали до конца.

    Arduino EEPROM работа с памятью

    Работа с EEPROM Arduino

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

    Arduino EEPROM энергонезависимая память

    Энергонезависимая память EEPROM Arduino (Electrically Erasable Programmable Read-Only Memory) — это постоянное запоминающее устройство, которое может хранить записанные в нее данные десятки лет после отключения питания. Количество циклов перезаписи EEPROM памяти несколько сотен тысяч раз при температуре не ниже 24°С, а чтение данных не ограничено. Объем EEPROM памяти Arduino следующий:

    • ATmega168 (плата Arduino Nano) — 512 байт;
    • ATmega328 (плата Arduino Uno, Arduino Nano) — 1 Кбайт;
    • ATmega1280, ATmega2560 (плата Arduino Mega) — 4 Кбайт.

    На операцию записи одного байта в EEPROM уходит около 3,3 мкс, а на чтение одного байта уходит около 0,4 мкс (микросекунд). Каждый байт (ячейка) EEPROM в Ардуино по умолчанию (т.е. ни разу не записанная ячейка) имеет значение 255. Обратите внимание, что если данные занимают место больше 1 байта (например, число больше 255), то они разбиваются на несколько ячеек. Это важно учитывать при записи в EEPROM.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *