Во встраиваемых устройствах существуют два основных вида долговременной памяти: EEPROM (Electrically Erasable Programmable Read-only Memory) и flash (NAND/NOR).

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

Для программиста взаимодействовать со flash памятью неудобно. Появляется необходимость в создании библиотеки, которая будет осуществлять:

  • поиск данных по ключу за минимально возможное время
  • поддержку целостности данных при сбоях во время записи
  • использование минимума оперативной памяти для хранения вспомогательных структур
  • циркуляцию использования страниц (wear-leveling)
  • абстрагирование от конкретной задачи, ОС и устройства


Для осуществления первого требования необходимо решить как ссылаться на данные. Два простых способа:
ссылка по имени, ссылка по названию. Первый вариант, по типу файловых систем, приведет к излишним накладным расходам, необходимо будет ограничивать длину названия (как это делается в FAT), и скорее всего будет для многих встраиваемых систем ограничением на пути к использованию. Поэтому я выбрала решение на основе ссылки по идентификатору. В случае 16-битного значения, существует 65536 различных идентификаторов. Для многих задач такого количества достаточно, учитывая значительные ограничения по ресурсам во встраиваемых системах.

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

Для удовлетворения условия целостности, после данных следует 16 битная контрольная сумма, вычисляемая по алгоритму longitudinal parity check от идентификатора, длины и данных.

Приходим к следующему формату хранения данных:
 
|  ID (16 бит) | LENGTH (16 бит) | DATA (LENGTH байт) | CHECKSUM (16 бит)  | 


Равномерное использование flash страниц (wear leveling) является ключевым алгоритмом для многих производителей устройств хранения на основе flash памяти. Алгоритмы известны в общих чертах, хотя готовых реализаций я не нашла. Я выбрала самый простой вариант: использование алгоритма round robin. Помимо страничного закольцовывания, нужно хранить и поддерживать дополнительную информацию. Ввиду этого каждая страница имеет заголовок, состоящий, как минимум, из статуса страницы. Для целей реализации хватило трех типов статусов: VALID, EMPTY, RECEIVING.

Страницы типа VALID являются страницами, на которых располагаются данные и/или на которые можно записать
необходимую информацию в соответствии с вышеприведенным форматом данных. Для VALID страниц после статуса хранится их порядковый номер. Страницы типа EMPTY не содержат данных и могут быть аллоцированы. Страницы типа RECEIVING — страницы, которые находятся в состоянии перехода из статуса EMPTY в VALID.

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

Во время инициализации VIrtEEPROM производится присваивание переменным необходимых значений, а также восстановление системы после сбоя, проверка валидности хранимых значений. Все RECEIVING страницы стираются и переводятся в статус ERASE. Заполняется массив, задающий правильный логический порядок страниц, а также массив валидных и занятых страниц.

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

Выше были перечислены требования и основные нюансы реализации. Далее обсудим API взаимодействия с VirtEEPROM.

В самом начале необходимо создать в единственном экземпляре текущий статус виртуального EEPROM'a, что осуществляется вызовом функции veeprom_create_status(), которое возвращает указатель на структуру типа veeprom_status.

Далее нужно сообщить адрес начала отображаемой flash памяти c помощью функции
veeprom_vstatus_init(veeprom_status *vstatus, uint16_t *addr), где vstatus — ранее созданный указатель на структуру veeprom_status, addrs — указатель, на область памяти начала flash памяти.

После этого необходимо вызывать veeprom_init(veeprom_status *s), которая возвращает 0 (OK), в случае, если не произошло ошибок в процессе инициализации.

Для поиска поиска данных по идентификатору используется функция veeprom_read(uint16_t id, veeprom_status *vstatus), которой в качестве параметров необходимо передать проинициализированный указатель на структуру vtstatus. Функция возвращает либо NULL в случае, если данные не найдены, либо указатель на структуру vdata*, которая в поле p содержит указатель, где записаны данные в вышеописанным формате.

Для записи необходимо указать идентификатор через параметр id, передать указатель на данные через data, передать размер данных в параметре length, так же передать указатель на veeprom_status в функцию veeprom_write(uint16_t id, uint8_t *data, int length, veeprom_status *vstatus). В случае успешной записи, функция возвращает 0. Если данные с переданным идентификатором уже были записаны, то значение обновится новой информацией.

Для удаления используется функция veeprom_delete(uint16_t id, veeprom_status *vstatus), которой передаётся удаляемый идентификатор и указатель на veeprom_status. В случае успеха функция возвращает 0.

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

$ git clone https://github.com/ninaevseenko/virteeprom.git
$ cd virteeprom/verification/ 
$ make 
$ ./examine 


Помимо этого, у кого есть STM32F3-Discovery, то можно прошить и посмотреть код VirtEEPROM в действии. Все необходимое находится в virteeprom/verification/stm32f3discovery/. После запуска на UART1 печатаются различные отладочные сообщения, и в котором запускается shell со следующим набором команд:

  • initflash — инициализирует необходимые структуры для работы с virteeprom
  • wipeflash — стирает все, кроме кода и данных программы
  • writeflash — записывает 100 однобайтовых значений
  • readflash — считывает 100 однобайтовых значений


Из скриншота ниже видно, что до инициализации было 34184 байт свободной памяти, после инициализации и записи 100 значений — 30544. То есть накладные расходы на хранение каждого отдельного элемента, позволяющего осуществить быстрый поиск по идентификатору, составляет примерно 36 байт.



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

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


  1. VT100
    14.10.2015 12:28

    ToDo:
    1. Учитываете-ли заводскую информацию о битых страницах (если брать современные NAND со страницей в 2112 байт, то это записано в последних 64 байтах)?
    2. Планируете-ли вписывать ECC в эти 64 байта на живых страницах?


    1. Sergey_datex
      14.10.2015 12:58

      Уточню
      1) Современные имеют размер страницы 16к.
      2) Обычно плохой блок маркирован нулем в первом байте Spare области.
      3) Современная память TLC часто имеет неисправные столбцы, список которых можно запросить у самого чипа.
      Столбцы нужно вырезать на этапе чтения страницы в буфер.


      1. anvoebugz
        14.10.2015 13:35

        1. Сейчас размер страниц не имеет значения.
        2.-3. Понятно, это нужно учесть.


    1. anvoebugz
      14.10.2015 13:24

      1. Это обязательно надо будет учитывать, при наличии такой информации
      2. Не очень понятен вопрос. По идее ECC полезно при записи во флешку с выставленным LowLatency,
      чтобы реже получать ошибки. То есть, да, можно в реализацию flash_write добавить коррекцию
      для устройств, которые имеют контроллер, в котором есть ECC.


      1. VT100
        14.10.2015 14:17

        У Вас «чистый» чип NAND или со встроенным контроллером? Если «чистый», то обязанность учёта битых блоков и использования корректирующих кодов — на Вас.


        1. anvoebugz
          14.10.2015 14:23

          Встроенный.


          1. VT100
            14.10.2015 16:08
            -1

            В таком случае — Вы кардинально заблуждаетесь, встроенная память программ МК — всегда NOR. И статья требует переписывания (что-бы не вводить в заблуждение читателей и что-бы указать отличия предложенного метода от AN2594, AN4061 и AN4056).


            1. anvoebugz
              14.10.2015 16:34

              Да, это правда, встроенная — NOR. NAND на usb флешках.
              Я поправила данное заблуждение в статье.
              Я напишу новую статью про отличия от метода, который по ссылке находится.


  1. Sergey_datex
    14.10.2015 13:04

    Вопрос автору статьи — а где коррекция ошибок? Это самое сложное в работе с «сырым» NAND без хардварного модуля ECC.

    Основное отличие EEPROM от NAND — это необходимость (обязательная) использования коррекции ошибок. Глубина коррекции диктуется типом используемой памяти. В даташите на конкретную память почти всегда указано минимально допустимую корректирующую способность для обеспечения прогнозируемого ресурса работы. Минимально для SLC — 4 бита на страницу 528 байт. Типично для современной TLC — 60 бит на 1 килобайтовый диапазон.

    Ну и поправьте немного в статье — NAND это тоже подкласс EEPROM


    1. anvoebugz
      14.10.2015 13:40

      Сейчас нету коррекции. Либо записали, либо не записали.

      «NAND Flash является разновидностью EEPROM» — как раз то, что вы имеете ввиду.


      1. alabram
        14.10.2015 13:57

        Сейчас нету коррекции. Либо записали, либо не записали.

        Боюсь, что реализовать коррекцию битовых ошибок нужно первым делом. Потому как вероятность варианта «не записали» или «записали, а завтра не прочитали» очень велика.
        Да и вообще вы себе задачу выбрали неблагодарную. Я не специалист, но мне кажется лучше использовать SPI flash, чем пытаться имитировать ее с помощью NAND.


        1. anvoebugz
          14.10.2015 14:21

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


          1. Sergey_datex
            14.10.2015 14:48

            Вы что-то путаете. Встроенная флешка не имеет отношения к NAND. Мне кажется вы запутались в терминах и называете NANDом то, что является внутренней NOR памятью контроллера.


  1. MrYuran
    14.10.2015 13:14
    +1

    В МК вообще и конкретно STM32 набортная флешь разбита на страницы по 1 или 2кБ, оттестирована и полностью готова к употреблению. Никаких спецполей не предусмотрено, контроль целостности и исправности полностью на плечах программиста. Вообще, если надо хранить много данных, обычно ставят сбоку обычную SPI-флешь, EEPROM или FRAM, у которой практически бесконечный ресурс.


    1. VT100
      14.10.2015 14:14

      Судя по статье — автор использует внешний чип NAND-Flash. Или нет?


      1. anvoebugz
        14.10.2015 14:19
        +2

        Я использую встроенный флеш микроконтроллера.


        1. Sergey_datex
          14.10.2015 14:50
          +2

          Тогда редактируйте статью, и заменяйте все NAND на NOR. Это совершенно другой тип памяти, с другой организацией и целевым назначением.


    1. anvoebugz
      14.10.2015 14:17

      SPI-флеш — может потребавать SPI шины, которая уже используется под другие нужды.
      Чтение/запись в EEPROM осуществляется за несколько мс, для флешки — за несколько мкс.
      Про FRAM не слышала. Киньте, пожалуйста, ссылку.


      1. MrYuran
        14.10.2015 14:37
        +1

        Производит Ramtron, вот пример.
        Также, TI встраивают FRAM в некоторые свои МК вместо флеши


      1. JerleShannara
        14.10.2015 14:40

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


      1. boeing777
        15.10.2015 10:26

        Как правило, внешние чипы EEPROM/FLASH с SPI не требуют высокой частоты интерфейса, поэтому вполне можно использовать программный SPI, реализованный на GPIO. В нашей промышленной разработке так и произошло — при переходе с stm32l (у которого был EEPROM на борту) к stm32f стали использовать внешний чип. Связано это, главным образом, с максимальным количеством циклов перезаписи: у встроенного flash stm32f гарантируется всего 10к циклов. Нам этого мало :)


        1. anvoebugz
          15.10.2015 14:37

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


  1. Sergey_datex
    14.10.2015 14:56

    Разобрались таки. Автор описывает использование встроенной Flash памяти для организации виртуального EEPROM. Такое решение кстати используется производителями жестких дисков, например Toshiba (бывшая Hitachi), Samsung.


  1. kacang
    15.10.2015 05:30
    +1

    А вот заводская реализиция для PIC-ов, если кому интересно


    1. anvoebugz
      15.10.2015 14:31

      Спасибо за ссылку, очень интересно.
      Посмотрела.

      Они себе задачу упростили тем, что записывают данные фиксированного размера (пишут int'ы):

      unsigned char DataEEWrite (unsigned int data, unsigned int addr);

      Еще довольно странно, что не проверяется осуществилась ли запись:

      DataEEWrite(DEEdata,DEEaddr1);
      value1 = DataEERead(DEEaddr1);
      Nop();