FlashFS для микроконтроллеров

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

FlashFS может пригодится для хранения конфигураций умных навороченных микросхем типа беспроводных трансиверов, драйверов шаговых двигателей, значение одометра и тп. Наличие 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ы) с плавно конфигурируемым размером.

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

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

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

#ifndef FLASH_DRV_H
#define FLASH_DRV_H

#include 
#include 

#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.

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.

0cb6dbced3b47226382d96f018735b1c.png

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

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


#ifndef NOR_FLASH_H
#define NOR_FLASH_H

#include 
#include 

#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 можно показать так

7d3288bc472f800d2dd61957c3d9f255.png

Или так

4a80817fc5605934a238c30795699c2a.png


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

рис 2рис 2

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

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

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

#ifndef PARAM_DRV_H
#define PARAM_DRV_H

#include 
#include 

#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  */

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

3bd93d03008fd95277e02e5035036f36.png

Вывод

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

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

Добавляйте в свои прошивки файловые системы. В этом нет ничего сложного.

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

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

Акроним

Расшифровка

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

© Habrahabr.ru