В разработке на микроконтроллерах хорошей практикой считается, когда на устройстве есть число-хранилище для запоминания чиселок между пере сбросом питания. Все это называют по-своему: StoreFS, FlashFS, Энергонезависимая Key-Value Map(а), HashMap(ка), NVS, NVRAM и прочее.

FlashFS может пригодится для хранения конфигураций умных навороченных микросхем типа беспроводных трансиверов, драйверов шаговых двигателей, значение одометра, пароли, SSID, серийные номера и т.п. Наличие FlashFS позволяет на порядок уменьшить количество сборок в репозитории, так как устройства всегда можно до программировать уже в Run-Time(е). Буквально открыв UART Shell можно прямо руками в TeraTerm/Putty прописать во FlashFS конфиги и перезагрузить(reset(нуть)) гаджет, чтобы новые настройки применились в инициализации. Easy. И не надо варить отдельные прошивки с какими-то специфическими настройками, когда есть FlashFS. FlashFs добавляет в программу положительную динамику.

Почему именно on-chip NorFlash?

1--Это дешево. Как правило после накатывания прошивки всегда остается ещё пара пустых страниц(секторов) Flash(а). За неё "уже заплачено" при покупке микроконтроллера. Нет нужды ставить еще отдельный off-chip SPI-NorFlash чипы и тем самым увеличивать габариты, стоимость, расширять логистику, уменьшать надежность, увеличивать сложность устройства. FlashFS предусматривают сами производители микроконтроллеров. Например в STM32 первые 4 сектора вообще по 16kByte, остальные больше по 128kByte. Это сделано специально, с расчетом на то, чтобы запускать на маленьких секторах по 16kByte FlashFS(cы) с плавно конфигурируемым размером.

У меня в проектах на STM32 FlashFS обитает во втором и третьем секторах on-chip Nor Flash(а).

2--On-chip FS безопаснее чем off-chip FS, так как нет возможности подцепиться к SPI или SDIO проводам на PCB и тем самым считать передаваемые данные обыкновенным китайским логическим анализатором за 500 рублей.

Однако у Nor-Flash памяти есть одна проблема. Стирать её (заполнять 0xFF(ками)) можно только страницами по несколько килобайт (обычно это 4kByte, 8kByte, 16kByte, 64kByte, 128kByte). Одновременно запись заключается в том, что можно только биты 1 сбросить в 0. Иногда даже нельзя до сбросить уже сброшенные биты, например, в байте 0x55. Это как писать шариковой ручкой по бумаге. Написанное не сотрешь ластиком. Есть смысл исписать весь лист, а затем вырвать его и выбросить.

Вот так обычно выглядит API для работы с On-Chip Nor-Flash памятью.

#ifndef FLASH_DRV_H
#define FLASH_DRV_H

#include <stdbool.h>
#include <stdint.h>

#include "flash_const.h"

#ifndef HAS_FLASH
#error "Add HAS_FLASH"
#endif

#ifndef HAS_MCU
#error "Add HAS_MCU"
#endif

#ifdef HAS_FLASH_WRITE
bool flash_wr(uint32_t addr, uint8_t* array, uint32_t array_len);
bool flash_wr4(uint32_t flash_addr, uint32_t* wr_array, uint32_t byte_size);
bool flash_errase(uint32_t addr, uint32_t len);
bool flash_erase_pages(uint8_t page_start, uint8_t page_end);
#endif

bool Addr2SectorSize(uint32_t addr, uint32_t *sector, uint32_t *sec_size);
bool flash_read(uint32_t in_flash_addr, uint8_t* rx_array, uint32_t array_len);
bool flash_init(void);
bool flash_scan(uint8_t* base, uint32_t size, float* usage_pec, uint32_t* spare, uint32_t* busy);
bool is_errased(uint32_t addr, uint32_t size);
bool is_flash_spare(uint32_t flash_addr, uint32_t size);
bool is_flash_addr(uint32_t flash_addr);

#endif /* FLASH_DRV_H */

Нам же, людям, удобнее просто записывать данные по ключу. Это как в театре. Даешь номерок, получаешь свой тренч. Или как в телефонной записной книжке. Даешь имя, получаешь номер телефона.

Нужен определенный уровень абстракции, который бы давал такой API и делал бы всю остальную работу под капотом с массивами сырой Flash памяти. Даешь число и массив, Массив записывается. Через неделю даешь число, и получаешь массив.

Это как писать карандашом. Можно написать и можно стереть ластиком и написать что-н другое. Удобно? Очень.

Попробую перечислить самые базовые требования для таких embedded on-chip файловых систем FlashFS.

1–Рациональное использование Nor-Flash памяти (endurance optimization)

2–Зашита данных от внезапного пропадания питания (power off tolerance).

3–Простота реализации чтобы нечему было ломаться.

4--Файл должен быстро находится по имени

5--Файл должен быстро записываться

6--Файл должен быстро обновляться

7--Файл должен быстро стираться

8--Компактность кода, который реализует этот алгоритм FlashFS. Чем меньше кода, тем больше файлов.

Определится с терминологией. Что такое File?

File это именованный бинарный массив байтов в памяти. В качества памяти может выступать RAM, ROM (Flash), FRAM, EEPROM, SD карты. Что значит именованный? Это значит, что к этим данным можно обращаться по значению. Пусть это будет натуральное 16-битное число. Так проще. Так как это массив, то рядом с данными также надо хранить и длину этого массива. Вот так может выглядеть примерный заголовок записи в FlashFs. Во Flash переменные будут в little-endian формате.

struct xFlashFsFileHeader_t {
    uint16_t id;
    uint16_t nid; /* bit inverted id*/
    uint16_t length;
    uint8_t crc8;   /*only for payload*/
} __attribute__((packed)); /*to save flash memory*/
typedef struct xFlashFsFileHeader_t FlashFsFileHeader_t;


Основная идея Flash FS

Если записывать файлы с одинаковыми ID, то они будут записываться последовательно. Этим достигается защита данных от пропадания питания. Всегда можно взять предыдущий файл. Каждый файл оснащен 8ми битной контрольной суммой. Это позволит выявлять только реальные файлы, а не просто случайные числа в памяти. Чтобы каждый раз не считать контрольную сумму для каждого отступа, есть преамбула из ID(шников). Это FileID и его инвертированная копия. Зачем нужен инвертированный ID? Это для того чтобы ускорить поиск нужного файла. Дело в том что процедура вычисления CRC это весьма продолжительная процедура. Было бы расточительно рассчитывать CRC для каждого отступа, чтобы понять файл тут или нет. Поэтому алгоритм рассчитывает CRC только по тем отсупам, где прописана валидная преамбула.

И вот заполнилась страница флешь памяти. Что делать? Надо отчистить вторую страницу и перекопировать в неё попарно отличные самые свежие файлы.

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

Помимо функций чтения и записи файлов нужна еще вспомогательная функция, которая будет как раз следить за самой файловой системой. Как только страница "B" переполнится, то background процедура должна выполнить процедуру toggle flash page, то есть отчиcтить страницу "A". Взять самые свежие файлы из страницы "B" и перекопировать их в страницу "A".

Вот так может выглядеть API для микроконтроллерной файловой системы


#ifndef NOR_FLASH_H
#define NOR_FLASH_H

#include <stdbool.h>
#include <stdint.h>

#include "flash_drv.h"
#include "flash_fs_config.h"
#include "flash_fs_types.h"

#ifndef HAS_FLASH
#error "+ HAS_FLASH"
#endif

#ifndef HAS_NVS
#error "+ HAS_NVS"
#endif

#ifndef HAS_FLASH_FS
#error "+ HAS_FLASH_FS"
#endif

#ifndef HAS_CRC8
#error "+HAS_CRC8"
#endif

#ifdef HAS_FLASH_FS_WRITE
bool flash_fs_format(void);
bool flash_fs_erase(void);
bool flash_fs_invalidate(uint16_t data_id);
bool flash_fs_set(uint16_t data_id, uint8_t* new_file, uint16_t new_file_len);
bool flash_fs_maintain(void);
bool flash_fs_turn_page(void);
#endif

bool flash_fs_is_active(uint8_t page_num);
bool flash_fs_init(void);
bool flash_fs_proc(void);
bool flash_fs_get(uint16_t data_id, uint8_t* value, uint16_t max_value_len, uint16_t* value_len);
bool flash_fs_get_active_page(uint32_t* flash_fs_page_start, uint32_t* flash_fs_page_len);
bool flash_fs_get_address(uint16_t data_id, uint8_t** value_address, uint16_t* value_len);
bool is_flash_fs_addr(uint32_t addr);
uint32_t flash_fs_get_page_size(uint8_t page_num);
uint32_t flash_fs_get_page_base_addr(uint8_t page_num);
uint32_t flash_fs_cnt_files(uint32_t start_page_addr, uint32_t page_len, uint32_t* spare_cnt);
uint32_t flash_fs_get_remaining_space(void);
uint8_t addr2page_num(uint32_t flash_fs_page_start);

#endif /* MEMORY_MANAGER_NOR_FLASH_H */

Зависимости программных компонентов для FlashFs можно показать так

Или так


Вот так может выглядеть диагностика файловой системы (рис 2). Это список файлов в файловой системе, их адреса, размер, данные внутри, контрольная сумма, ID(шник)

рис 2
рис 2

С файлами разобрались. Но читать сырые данные в памяти тоже как-то не очень-то удобно. Для человека это просто последовательность нимблов (hex разрядов). Надо как-то интерпретировать эти данные в реальные физические величины и типы данных. Этим займется программный компонент Param.

Поверх Flash Fs должен работать еще один уровень абстракции. Я его называю параметры (Param). Работает он так. Даешь ID файла и данные (hex массив с длинной), получаешь тип данных и его значение.

Вот так могут выглядеть прототипы функций для компонента param

#ifndef PARAM_DRV_H
#define PARAM_DRV_H

#include <stdbool.h>
#include <stdint.h>

#include "param_types.h"

#ifndef HAS_PARAM
#error "+HAS_PARAM"
#endif /*HAS_PARAM*/

bool param_init(void);
bool param_proc(void);

#ifdef HAS_PARAM_SET
bool param_set(Id_t id, uint8_t* in_data);
#endif /*HAS_PARAM_SET*/

bool param_get(Id_t id, uint8_t* out_data);
ParamType_t param_get_type(Id_t id);
uint16_t param_get_real_len(Id_t id) ;
uint16_t param_get_len(Id_t param_id);
uint16_t param_get_type_len(ParamType_t type_id);
uint32_t param_get_cnt(void);

#endif /* PARAM_DRV_H  */

Вот теперь с этим можно работать. Переменные понятные, значения интерпретируются. Успех.

Вывод

Как видите программы для микроконтроллеров строятся из уровней абстракции, которые работают один поверх другого.

Теперь вы представляете как делать энергонезависимую файловую систему для хранения всякого разного (настроек, прошивок, калибровочных параметров). Не обязательно делать файловую систему именно на on-сhip Nor Flash. Можно и на off-chip Nor Flash или EEPROM.

Добавляйте в свои прошивки файловые системы. В этом нет ничего сложного. Любая вещь лучше, когда внутри её есть FlashFS.

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

Если есть, что добавить, то пишите в комментариях.

Акроним

Расшифровка

FS

file system

SPI

Serial Peripheral Interface

ID

Identifier

NVIC

Nested Vectored Interrupt Controller

CRC

Cyclic redundancy check

NVS

Non-Volatile Storage

API

Application Programming Interface

Links

https://habr.com/ru/post/584156/
https://habr.com/ru/post/483280/

https://www.allaboutcircuits.com/technical-articles/microfat-a-file-system-for-micro-controllers/
https://bestofcpp.com/repo/matt001k-STORfs

https://itnan.ru/post.php?c=1&p=573244

https://habr.com/ru/post/281242/

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


  1. CodeRush
    06.01.2023 20:26
    +3

    Следующим шагом будет расширение подобного простого NVS до полноценной NVRAM, для чего понадобятся:

    • wear-out leveling, т.е. выравнивание нагрузки на страницы флеша, чтобы многократная запись в переменную Х использовала не только страницу, куда Х была записан изначально, но все доступные по очереди.

    • fault-tolerant write, т.е. такой алгоритм записи в переменную, который гарантирует, что "настоящей" эта запись станет исключительно после того, как все данные и все критические для правильной работы метаданные уже записаны, а все остальное можно однозначно восстановить после перезагрузки.

    • transparent storage backup, т.е. такой алгоритм управления хранилищем, который гарантирует, что при его разрушении у нас есть копия большинства переменных, пусть даже и не самая свежая.

    Понятно, что нужно вот это все только там, где софт много пишет в свою же NVRAM по разным причинам. Именно так делают большинство современных систем вычислительных систем разных архитектур, в том числе ПК на х86 (использующие UEFI NVRAM), вся техника Apple (iBoot NVRAM), и т.п.


    1. aabzel Автор
      07.01.2023 21:01

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

      Можно организовать RAID5. Тогда можно будет сломать 1 чип из трех.


  1. smart_pic
    06.01.2023 20:48
    +2

    Предложенный алгоритм работы с сохраненными настройками (читай переменными ) нашей программы имеет недостаток по скорости работы программы с этими самыми настройками.

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

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

    Большие объемы данных, а так же длинные структуры (строковые данные настройки длиной более 32 символов, ВЕБ страницы) - размещаем во внешней SPI памяти

    Часто меняющиеся значения, например счетчики , размещаем в относительно энергонезависимой памяти микросхемы часов (пока батарейка не помрет).

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


  1. andreykorol
    08.01.2023 12:44
    +1

    Например в STM32 первые 4 сектора вообще по 16kByte, остальные больше по 128kByte. Это сделано специально, с расчетом на то, чтобы запускать на маленьких сесекторах  по 16kByte FlashFS(cы)

    Всегда думал, что они для того чтобы туда записывать свой бутлоадер (т.н. second bootloader). Куда его тогда предлагаете записывать если эти сектора будут заняты под storage?


    1. aabzel Автор
      08.01.2023 13:42

      В самом первом секторе Flash стоит разместить крохотный первичный загрузчик master boot record (MBR). Его можно уместить и в 16kByte, а если писать на ASM то и в 1кByte можно.
      Который просто прыгнет на вторичный загрузчик на Custom(ном) отступе (последний сектор Flash).

      В моих проектах на STM32 карта памяти обычно такая:


  1. aumi13
    09.01.2023 11:17

    как показывает практика - чем сложнее система тем больше вероятность накосячить.

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