Зачем ставить внешнюю IC памяти или SD карту если в микроконтроллере осталось много свободной Flash памяти! 

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

Как сделать из внутренней Flash подобие EEPROM сравнительно неплохо написано в этом апноуте от ST. Но с некоторого уровня комплексности встроенного ПО хранить данные в именованных файлах удобнее чем в жёстких структурах. Файлы упрощают реюзинг, облегчают поддержку преемственности версий, апгрейды и даунгрейды. Файлы освобождают от хлопот с планированием размещения во флэш и разруливанием конфликтов размещения, особенно если приложение модульное и модули разрабатываются отдельно. 

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

Здесь представлена линейная файловая система относящаяся к классу журналируемых. Такие файловые системы известны давно.  Быстротой они не блещут, потому эффективны на сравнительно небольших объемах памяти. Но им присуще крайне малая потребность в ОЗУ, малый размер кода и сравнительная простота. При этом по своей природе линейные файловые системы обеспечивают равномерный износ Flash и им не нужен дополнительный программный слой FTL, как например для FAT на базе NAND. 

Это продолжение развития открытого проекта платы контроллера резервного питания BACKPMAN v2.0. Вот тут и тут написано как началась разработка BACKPMAN v2.0. Движение идет по пути наращивания функциональности. Поэтому представленный здесь следующий проект с пафосным названием Example2 является продолжением проекта Example1 с некоторым рефакторингом и c демонстрацией линейной файловой системы STfs (не расшифровывается, просто префикс в сорсах).  

Истоки STfs

Выше упомянул апноут от ST. На самом деле там все довольно угловато. Главная функция записи там может записывать только 2-х байтными словами сопровождаемыми тэгом под называемым виртуальный адрес. Что делать если надо сохранять структуры или не выровненные блоки неясно.

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

Источником вдохновения послужила файловая система из среды Keil для SPI Flash. Но там все аскетично и нет поддержки RTOS.

В предыдущих статьях было описано портирование Azure RTOS, и STfs  использует сервисы этой RTOS. Как любая файловая система STfs не предоставляет жесткого детерминизма.  Поэтому чтобы внедрить ее в среду задач с жестким реальным временем приходится использовать RTOS. Однако количество сервисов требующихся файловой системе от RTOS невелико, и портирование её на другую операционку не должно вызывать проблем.

Почему всё не так просто с STM32Hxxx

Особенностью семейства STM32H является наличие механизма контроля и исправления целостности блоков внутренней Flash памяти и разбивка Flash на стираемые секторы сравнительно большого размера. Конкретно у STM32H753VIH цельные блоки имеют размер 32 байта, а минимальные стираемые секторы 128 Кбайт. Т.е. можно записывать за одну итерацию во Flash только блоками по 32 байта называемыми Flash word, не больше и не меньше, только один раз в одно место после последнего стирания, но стирать можно только блоками по 128 Кбайт. Если попытаться повторно записать что-то во Flash, даже если это будет один байт и он просто поменяет один бит с 1 на 0, то 32-байтный блок, на который придется такая запись, будет с высокой вероятностью обозначен системой как сбойный и последующее чтение в границах этого блока вызовет ошибку доступа к шине и вызов исключения BusFault, которое нарушит ход выполнения программы. И такое поведение никак отключить нельзя. Т.е. после некорректной записи всего в один байт в системе станет недоступным блок Flash памяти размером в 32 байта и его невозможно будет читать ни внутренней программой, ни с помощью отладочного адаптера, пока не будет стерт весь сектор128 Кбайт содержащий этот блок.  Указанные ограничения препятствует использованию файловых систем типа SPIFFS или  littlefs. Требуется более специфичный подход. 

Описание STfs.

Технология проста. В секторы памяти размером 128 Кб последовательно записываются данные файлов. Каждый раз при открытии файлов на запись, фиксации данных и закрытии этих файлов создаются дескрипторы и записываются в тот же сектор. Данные файлов пишутся от нижней границы сектора вверх, а дескрипторы пишутся от верхней границы сектора вниз, как показано на рисунке ниже    

Способ организации файловой системы на примере одного сектора
Способ организации файловой системы на примере одного сектора

Когда данные и дескрипторы сходятся в одной точке запись переносится в следующий сектор Flash.

Ниже описание структуры дескриптора:

  // Размер структуры дескриптора фрагмента файла - 64 байта
  // Дескриптор состоит из двух 32 байтных частей.
  // Вторая часть дописывается когда в дескрипторе ставится тэг(штамп) стертого файла
  typedef struct
  {
      uint32_t   file_id;       // Идентификационный номер файла. Если здесь находится EMPTY_PTTRN, то это чистый блок без данных о фрагменте
      int32_t    start_addr;    // Абсолютный адрес начала фрагмента
      int32_t    size;          // Размер фрагмента. Запись ведется 32 байтными блоками поэтому этот размер должен быть кратным 32
      uint32_t   number;        // Порядковый номер фрагмента, начинается с 0. В фрагменте с номером 0 хранится имя файла
      int32_t    data_size;     // Количество данных реально записанных во фрагменте
      uint32_t   version;       // Номер версии файла. Используется для безопасного переименования файла. По номеру версии определяется какая версия актуальна.
      uint32_t   reserv1[2];

  } T_stfs_file_normal_descriptor;

  typedef struct
  {
      uint32_t   deleted;      // Тэг удаленного файла
      uint32_t   reserv2[7];
  } T_stfs_file_erase_descriptor;


  typedef  struct
  {
      T_stfs_file_erase_descriptor  e;
      T_stfs_file_normal_descriptor n;
  } T_stfs_file_descriptor;

Дескриптор имеет размер 64 байта и состоит из двух половин по 32 байта. Это позволяет записывать дескрипторы всегда выровненными по границе 32-байтного Flash word и ровно соответствующего размера.

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

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

Технические характеристики файловой системы STfs

  • 32-битная линейная многозадачная для среды Azure RTOS адаптированная под 32-батное Flash word

  • Размер и количество секторов произвольное. Назначается через массив секторов на этапе компиляции

  • Длина имени файла не более 31 символа и не менее 1

  • Количество дисков произвольное.

  • Поддержки директорий нет, поддержки даты создания нет (но можно добавить).

  • Есть поддержка поиска файлов по имени, по шаблону, по порядку создания.

  • Количество одновременно открытых файлов на чтение произвольное. Определяется на этапе компиляции.

  • Количество одновременно открытых файлов на запись не должно превышать количества секторов.

  • Нельзя одновременно открывать файлы на запись и на чтение.

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

Описание API STfs

Название функции

Описание

STfs_init

Инициализация файловой системы. Инициализирует драйвер Flash, проверяет целостность, форматирует если целостность нарушена

STfs_find

Поиск файла на виртуальном диске по строковому шаблону и получение сведений о файле

STfs_find_first_file

Поиск самого раннего файла на виртуальном диске и получение сведений о файле

STfs_find_next_file

Продолжение поиска файлов от ранних к поздним

STfs_open

Открытие файла на запись или чтение

STfs_rename

Переименование файла

STfs_read

Чтение данных из файла

STfs_write

Запись данных в файл

STfs_setpos

Установка позиции чтения из файла от его начала

STfs_len

Получение размера файла

STfs_close

Закрытие файла

STfs_delete

Удаление файла

STfs_flush

Фиксация записанных данных во Flash

STfs_free_space

Получение информации о количестве свободного пространства на виртуальном диске

STfs_format

Форматирование виртуального диска. Физически просто стирание всех секторов

STfs_check

Проверка целостности файловой системы на виртуальном диске и получение информации о системе

STfs_defrag

Дефрагментация файловой системы

STfs_free_task_cbls

Освобождение захваченных задачей RTOS ресурсов у файловой системы.

Симулятор STfs на персональном компьютере

Тестировать на реальном железе логику файловой системы слишком долго и неудобно.
Поэтому была написана программа симулятор на C++ под Windows 10.

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

Все записи, считывания производятся на основе генератора случайных чисел. Каждый 10-й файл не удаляется для имитации фрагментации. Если дефрагментация не удаётся, то чтобы не прерывать тест производится выборочное удаление более старших файлов.

Внешний вид программы симулятора STfs
Каждый пиксел в столбце отображает состояние байта в секторе. Каждый столбец это отдельный сектор. Адреса растут слева направо и снизу вверх.

Зелёным цветом обозначаются блоки данных актуальных файлов, желтым обозначаются дескрипторы актуальных файлов. Красным и тёмно-красным обозначаются блоки уделенных файлов и их дескрипторов.
Серый цвет обозначает чистые области Flash, куда еще ничего не записывалось после последнего стирания сектора.
Внешний вид программы симулятора STfs Каждый пиксел в столбце отображает состояние байта в секторе. Каждый столбец это отдельный сектор. Адреса растут слева направо и снизу вверх. Зелёным цветом обозначаются блоки данных актуальных файлов, желтым обозначаются дескрипторы актуальных файлов. Красным и тёмно-красным обозначаются блоки уделенных файлов и их дескрипторов. Серый цвет обозначает чистые области Flash, куда еще ничего не записывалось после последнего стирания сектора.

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

Симулятор написан в среде Embarcadero RAD Studio 11 Alexandria. Но легко скомпилируется и на более ранних версиях, поскольку использует только минимальный набор стандартных компонентов. Подойдет триальная версия.

И наконец скоростные характеристики STfs

Сначала об износе. Как уже писал выше выравнивание износа присуще линейным FS и это показала симуляция случайного потока записей, удалений и дефрагментаций .

Вот результат после 100 тыс. итераций теста:

Номер сектора

Количество стираний

0

2961

1

3442

2

3541

3

3941

4

4386

5

3653

6

3798

7

3166

Как видно цифры одного порядка. При том что предельный жизненный цикл у Flash очень размыт то более точного совпадения цифр и не требуется. Надо напомнить что в STM32H работает механизм исправления одиночных ошибок Flash и предупреждения о двойных ошибках. Т.е. пользователь имеет дополнительную возможность проактивно среагировать на износ в своем прикладном коде.

Теперь цифры измеренные на реальном микроконтроллере STM32H753VIH после 200 итераций

Тип операции

Минимальное время, мкс

Максимальное время, мкс

Стирание сектора

855691

930729

Открытие файла на запись

179

768

Время записи файла размером 10 Кбайт

27100

30742

Время чтения файла размером 10 Кбайт

25

203

Проверка целостности файловой системы

189

8983

Итог

Скорость записи в файл колеблется в пределах 325...369 Кбайт в секунду. Это типичная цифра для NOR памяти и тут лучшего ожидать не приходится. Но надо отметить неплохую детерминированность.

Ложку дёгтя вносит время стирания сектора. А значит дефрагментация может затянуться до 8 сек. Это надо учитывать.

Но чтение будет идти со скоростью не менее 40 Мбайт в секунду. И этого более чем достаточно.

Все проекты хранятся здесь.

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


  1. Sun-ami
    05.11.2021 13:37
    +1

    Если верить Reference manual на STM32H753, при ошибке контроля ECC Programm Flash генегируется не Hard Fault, а Bus Fault, и только в случае, если была обнаружена двойная ошибка, если была обнаружена одинарная — она исправляется, и может произойти маскируемое прерывание, если оно включено. А Hard Fault возникает, если обнаружена двойная ошибка, и Bus Fault маскировано. При этом, Bus Fault можно обработать, ориентируясь на адрес и флаги от блока контроля ECC. Это, конечно, слабо поможет повторной записи, ведь блок ECC может исказить данные при чтении, пытаясь исправить одиночную ошибку — писать повторно имеет смысл разве что несколько значений из фиксированной последовательности, каждое из которых имеет корректный ECC. Или практика расходится с Reference manual?


    1. Indemsys Автор
      05.11.2021 13:48

      Да все верно написали.
      Я просто ошибочно написал название функции в которую у меня все немаскируемые прерывания прилетают. Исправлю.

      По поводу одиночной ошибки при изменении одного бита не уверен.
      Мне кажется логичным что при каждой записи записывается и ECC. И ECC пишется в ту же Flash. Т.е. искажается и бит и ECC, что в результате приведёт сразу к двойной ошибке.


      1. Sun-ami
        05.11.2021 14:10

        Думаю, да, записывается и ECC. Но думаю, существует несколько последовательностей из 256-битных данных с кодом ЕСС, в которых следующий элемент отличается набором дополнительных нулей в данных и ECC. А если не смотря на Bus Fault всё-таки можно прочитать дескриптор — наверное можно писать маркер стирания прямо в 32-байтный дескриптор, в виде поля из нескольких нулей. Это позволит уменьшить расход памяти на запись одного байта в файл в полтора раза.


        1. Indemsys Автор
          05.11.2021 15:17

          Я некоторое время думал об этом и искал алгоритм ECC от ST, но не нашёл.

          Опять же вычислительная сложность не ясна. Если это потребует итераций или больших таблиц, то тоже не вариант.

          Проще уж рационально писать файлы. Не писать файлы из одного байта.


          1. Sun-ami
            05.11.2021 16:43

            Не совсем понимаю, как Вы хотели применить алгоритм ECC. Если он нужен — возможно, стоит попробовать предположение, что он принципиально такой же, какой применялся в IBM-370 ещё в 70-х, и отличается только разрядностью. Но для записи маркера стирания в 32-байтный дескриптор он не нужен — достаточно в обработчике Bus Fault игнорировать это исключение, если установлен флаг двойной ошибки ECC, а в дескрипторе первым делом проверять маркер стирания. Это может быть байт, который для нестёртого дескриптора равен FF. Для стёртого — писать туда 0, и проверять на равенство FF. Это добавит время на обработку Bus Fault по каждому удалённому дескриптору — но это немного времени.


            1. Indemsys Автор
              05.11.2021 17:10

              Алгоритм ECC нужен для того чтобы сформировать дескриптор для подходящей ECC чтобы не было ошибки или была только одна.
              Потому что если будет две, то я просто не смогу прочитать ничего в дескрипторе.
              Bus Fault возникает до того как выполниться команда чтения. Чтобы ни пытался читать в 32-байтном блоке будет Bus Fault и я не узнаю содержимое ни одного байта.

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


              1. Sun-ami
                05.11.2021 17:27

                Так из обработчика Bus Fault можно вернуться как из обычного прерывания, и данные скорее всего будут прочитаны


                1. Indemsys Автор
                  05.11.2021 17:35

                  Так адрес возврата указывается до команды чтения вызывающей ошибку. Попытка просто вернуться мгновенно вызывает новый Bus Fault.

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

                  Я пришёл к выводу что в таком случае уже нужно сгребать все логи и ресетиться.


                  1. Sun-ami
                    05.11.2021 17:45

                    Но из Bus Fault нужно возвращаться не в общем случае, а в конкретном. Поэтому чтобы узнать на сколько прирастить адрес возврата достаточно глянуть в дизассемблер в этом месте. Правда, шансов получить значение в этом случае остаётся немного.


                    1. Indemsys Автор
                      05.11.2021 17:47

                      Хорошая идея для новой статьи.


  1. Sun-ami
    05.11.2021 14:09

    del, промахнулся


  1. Dpal
    05.11.2021 17:10

    Спасибо!


  1. fk01
    07.11.2021 18:02
    +1

    Непонятно, зачем дескрипторы хранить отдельно от данных. По-моему всё давно изобретено есть так называемые log file systems, где идея в том, что данные просто пишутся "по кругу" на некий носитель (новые данные просто перезаписывают наиболее старые со временем). Так обеспечивается идеальный wear leveling. Например JFFS, разработанная фирмой Axis для камер наблюдения. Файлы, ясное дело, при этом не исчезают и не портятся, просто данные которые не должны быть удалены перезаписываются заново. Преимущество такого решения, что в случае сбоя при записи происходит автоматический откат на предыдущую версию. Файловая система получает свойство быть устойчивой к отказам, в частности к отключению питания. Разумеется, для работы с такой файловой системой после включения питания она вся считывается и строится индекс, который занимает относительно большой объём ОЗУ. Последнее впрочем не проблема, т.к. и индекс можно хранить в самой файловой системе, а в ОЗУ только последние не записанные изменения.

    А в вашей системе получается, успели записать дескриптор, например, а файл не успели -- сбой. Или наоборот. Система не обладает отказоустойчивостью. А это критично не только в случае отключения питания, но и в случае сбоев ПО, когда процессор уходит в перезагрузку, в случае внешних воздействий на вычислительную систему, таких как статическое электричество, радиация, электромагнитное излучение. Так или иначе на большом периоде времени перезапуски вызванные перечисленными факторами -- будут. И будут потери данных.


    1. Indemsys Автор
      07.11.2021 18:17

      В моей файловой системе системе данные также пишутся по кругу. Посмотрите на анимацию, там это явно видно. Дескрипторы для данных по любому нужны в любой файловой системе. Называться только может по другому. Иначе нельзя будет найти где данные начинаются, а где кончаются.
      Индекс в моей системе не нужен, она настолько мала, что любая операция способна прочитать всю файловую систему менее чем за 10 мс если ей требуется что-то найти. Это резко снижает потребность в RAM.

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

      Нельзя забывать и о наличии механизма исправления одиночных ошибок в STM32 и предупреждения о них. Механизм работает, поскольку я сохранил в системе нативный формат записи 32-байтными словами. Такой фичи нет, скажем в SPI флэш или в JFFS для общего случая.
      Поэтому я могу считать свою систему даже потенциально более надёжной чем обычные. Но требуется конечно доработки.


  1. spam-receiver
    08.11.2021 22:28

    Примеры Embedded Flash хранилищ, которые прекрасно работают:

    https://docs.zephyrproject.org/latest/reference/storage/nvs/nvs.html

    https://infocenter.nordicsemi.com/topic/sdk_nrf5_v17.1.0/lib_fds.html


    1. Indemsys Автор
      09.11.2021 15:49

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


      1. spam-receiver
        11.11.2021 00:41

        Спасибо, буду ждать.