Любая вещь лучше, когда внутри неё есть NVRAM.

Пролог

В программировании MCU порой приходится решать по-настоящему сложные задачи. При этом сложность как с алгоритмической точки зрения так и в объёме кода, который предстоит написать.
Вот вам яркий пример. Надо запустить NVRAM на микроконтроллере, где нет возможности дописывать интервалы памяти (это например FC7300x). Сказать по честному, не всякий программист МК способен разработать такой NVRAM. Чтобы сделать NVRAM при условии запрета до-записи нужна не хилая алгоритмическая подготовка (двух-связанные списки, деревья, дефрагментация памяти и прочее).

Существует два подхода к программированию

--Писать всё самому. Это сложно. Зато вы контролируйте каждую строку кода в проекте и можете проводить очень гибкую товарную политику.

--Портировать программные компоненты из Open-Source (third-party). Это быстро. Но вы теряете контроль над функционалом.

Однако, как ни крути, но порой приходится работать с third-party кодом. И вот настал такой случай.

Постановка задача

Организовать NVRAM на микроконтроллере с одноразовой flash память. Осуществить пуск файловой системы LittleFs на микроконтроллере. Понять, как конфигурировать LittleFs.

Требования к NVRAM
1--Позволять записывать данные произвольной длинны (а не только степени двойки).
2--Не позволять до записывать страницы Flash памяти.
3--NVRAM должен поддерживать Lazy Write. Не писать данные, если они там уже есть.
4--Если стирать flash, то секторами по 8k Byte
5--Если писать flash, то только страницами 128 байт.
6--Писать по выровненному адресу (128 byte)
7--Стирать по выровненному адресу (8k Byte)
8--Собирать код на Си
9--Рациональное использование Nor-Flash памяти (endurance optimization). Не писать всегда в одно место
10--Должна быть зашита данных от внезапного пропадания электропитания (power off tolerance).
11--Простота реализации чтобы нечему было ломаться.
12--Файл должен быстро находится по имени (<100ms)
13--Файл должен быстро записываться (<100ms)
14--Файл должен быстро стираться (<100ms)
15--Компактность кода, который реализует этот алгоритм NVRAM. Чем меньше кода, тем больше файлов получится загрузить.

Вот такие ограничения. Нормально так, правда?

Определения

Чтобы правильно понять сорцы LittleFs надо четко осознавать вот эти понятия.

third-party - код со стороны. Программный компонент который вы скачали из интернета.

Блок - это по сути и есть сектор flash памяти. LittleFs стирает блоками. В моём микроконтроллере блок получается равен 8kByte

Страница - минимальный возможный размер для записи. В моем нынешнем микроконтроллере страница равна 128 байт.

NVRAM - энергонезависимая память (NV) с произвольным доступом (RAM). По сути Key Val-Map(ка). В ней могут храниться любые бинарные данные, ассоциированные со своим ID числом.

dword (double word) — это 4х байтная переменная (32 бит).

word — это двухбайтная переменная (16 бит)

byte — это 8ми битная переменная

данные - массив hex байтов с указанной длинной

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

Lazy write - это алгоритм записи, который записывает данные только в том случае, если они реально отсутствуют в памяти. Если такие данные уже там лежат, то процедура write просто ничего не делает, и возвращает OK. Это как если вы хотите записать новый номер в телефон, а он уже там есть, то вы не будете записывать его снова.

Исследование вопроса
FatFs не подходит так как она не обеспечивает устойчивости к отключению питания. B FatFs отсутствует механизм выравнивая износа массивов Flash памяти. Обычно FatFs запускают на SD картах которые и реализуют механизм износа Flash памяти.

Реализация

Десять лет назад я был свидетелем как у нас выкручиваются от отсутствия полноценного NVRAM. Выглядит это так. Схемотехники в плату закладывают 3 или 6 GPIO пинов и джамперами выставлять бинарный код на GPIO, чтобы при старте дать прошивке какую-то команду. Получается 1х3х6=18 кубических миллиметров на 1 бит. Нормально так, да? NVRAM на джамперах - это ярчайший пример лютого технического отчаяния. В этом же тексте я написал, как запрограммировать полноценную NVRAM c минимальными усилиями.

У меня уже были тексты про различные алгоритмические реализации NVRAM. Вот они

Название

URL

Тип

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

https://habr.com/ru/articles/706972/

on-chip

NVRAM Поверх off-chip SPI-NOR Flash

https://habr.com/ru/articles/732442/

off-chip

NVRAM из EEPROM

https://habr.com/ru/articles/815639/

on-chip

Все эти алгоритмы реализации NVRAM работают только при условии, что в данном микроконтроллере можно взять и до-записывать адреса Flash памяти. Однако надо отметить, что такого люкса на многих семействах микроконтроллеров просто нет в помине! Да... Во накануне мне попался MCU с одноразовой Flash памятью. Если бы не ограничения Flash я бы выбрал имеющийся отлаженный алгоритм и дело с концом. Однако ограничения сподвигли искать различные альтернативные механизмы построения файловых систем на микроконтроллере.

После широчайшего и интенсивного исследования вопроса я обнаружил, что существует такой open-source проект, как LittleFs. Решил попробовать его освоить.

Ликбез по LittleFs

Little Fs - это небольшая отказоустойчивая файловая система, разработанная специально для микроконтроллеров и написана на чистом Си (С99). Обладает тремя ключевыми характеристиками:

Устойчивость к сбоям питания — littlefs разработана для обработки случайных сбоев питания. Все файловые операции имеют надежную гарантию копирования при записи, и в случае отключения питания файловая система возвращается к последнему работоспособному состоянию. Динамическое выравнивание износа — littlefs разработана с учётом флэш-памяти и обеспечивает выравнивание износа динамических блоков. Кроме того, littlefs может обнаруживать сбойные блоки и обходить их. Ограниченный объём ОЗУ/ПЗУ — littlefs разработана для работы с небольшим объёмом памяти. Использование ОЗУ строго ограничено, что означает, что потребление ОЗУ не меняется по мере роста файловой системы. Файловая система не содержит неограниченной рекурсии, а динамическая память ограничена настраиваемыми буферами, которые могут быть предоставлены статически.

Сначала я попробовал погонять LittleFs в виде консольного Win приложения на своем LapTop-е. По началу не было похоже, чтобы Little FS вообще работал. После block_cycles раз записи и чтения файла, файловая система падает и перестает, чтобы бы то ни было делать. Оказалось я забыл добавлять к вычислению физического адреса слагаемое offset. Затем я обнаружил, что LittleFs успешно пишет читает до первого переполнения сектора. Оказывается, что при чтении и записи надо было домножить блок не на 128 байта, а на 8192 байта. В результате кристаллизировался корректный код конфига для МК FC7300. Вот он.

#include "little_fs_config.h"

#include "data_utils.h"
#include "sw_nor_flash.h"
#include "log.h"

#define LITTLE_FS_NOR_FLASH_NUM 1
#define LITTLE_FS_BLOCK_SIZE  8192
#define LITTLE_FS_PAGE_SIZE  128
#define LITTLE_FS_CACHE_SIZE (LITTLE_FS_PAGE_SIZE)


#ifdef LFS_NO_MALLOC
// Optional statically allocated read buffer.
// Must be cache_size.
// By default lfs_malloc is used to allocate this buffer.
static uint8_t readBuffer[LITTLE_FS_CACHE_SIZE] = {0};
static uint8_t progBuffer[LITTLE_FS_CACHE_SIZE] = {0};
static uint8_t lookaheadBuffer[LITTLE_FS_PAGE_SIZE] = {0};
#endif

static int FlashToLittleFsRet(bool res) {
    int ret = LFS_ERR_OK;
    if (false == res) {
        ret = LFS_ERR_CORRUPT;
    }
    return ret;
}

static int user_provided_block_device_prog(const struct lfs_config* c,
                                           lfs_block_t block,
                                           lfs_off_t off,
                                           const void* buffer,
                                           lfs_size_t size) {
    int ret = LFS_ERR_CORRUPT;
    bool res = false;
    LOG_NOTICE(LITTLE_FS, "Write,Block:%u,OffSet:%u,Size:%u", 
               block, off, size);
    uint32_t phy_address = 0;
    phy_address = block * LITTLE_FS_BLOCK_SIZE + off;
    res = sw_nor_flash_write(LITTLE_FS_NOR_FLASH_NUM,
                             phy_address,
                             (uint8_t*) buffer,
                             size);
    ret = FlashToLittleFsRet(res);
    return ret;
}

static int user_provided_block_device_read(const struct lfs_config* c,
                                           lfs_block_t block,
                                           lfs_off_t off,
                                           void* buffer,
                                           lfs_size_t size) {
    int ret = 0;
    LOG_DEBUG(LITTLE_FS, "Read,Block:%u,OffSet:%u,Size:%u", 
              block, off, size);
    uint32_t phy_address = 0;
    phy_address = block * LITTLE_FS_BLOCK_SIZE + off;
    bool res = sw_nor_flash_read(LITTLE_FS_NOR_FLASH_NUM,
                                 phy_address,
                                 (uint8_t*) buffer,
                                 (uint32_t) size);
    ret = FlashToLittleFsRet(res);
    return ret;
}

static int user_provided_block_device_erase(const struct lfs_config* c,
                                            lfs_block_t block) {
    int ret = LFS_ERR_CORRUPT;
    LOG_WARNING(LITTLE_FS, "EraseBlock:%u", block);
    uint32_t phy_address = block * LITTLE_FS_BLOCK_SIZE;
    bool res = sw_nor_flash_erase_mem(LITTLE_FS_NOR_FLASH_NUM,
                                      phy_address,
                                      LITTLE_FS_BLOCK_SIZE);
    ret = FlashToLittleFsRet(res);
    return ret;
}

static int user_provided_block_device_sync(const struct lfs_config* c) {
    int ret = LFS_ERR_OK;
    LOG_INFO(LITTLE_FS, "Sync");
    return ret;
}

const LittleFsConfig_t LittleFsConfig[] = {
        { .num = 1, .valid = true, .name = "LittleFs1",
          .cfg = {
                 // block device operations
              .read = user_provided_block_device_read,
              .prog = user_provided_block_device_prog,
              .erase =                user_provided_block_device_erase,
              .sync = user_provided_block_device_sync,
        #ifdef LFS_NO_MALLOC
              .read_buffer = readBuffer,
              .prog_buffer = progBuffer,
              .lookahead_buffer = lookaheadBuffer,
        #endif
              // block device configuration
              .read_size = LITTLE_FS_PAGE_SIZE,
              .prog_size = LITTLE_FS_PAGE_SIZE,
              .cache_size = LITTLE_FS_CACHE_SIZE,
              .lookahead_size = LITTLE_FS_PAGE_SIZE,
              .block_cycles = 4,
              .block_size = LITTLE_FS_BLOCK_SIZE,
              .block_count = 4,
        },
    },
};

Скорее всего Вы захотите просматривать содержимое папок. Это можно сделать так.

bool little_fs_list(uint8_t num, const char* const path) {
    bool res = false;
    LOG_INFO(LITTLE_FS, "%u,path:[%s]", num, path);

    const table_col_t cols[] = { { 5, "N" }, { 17, "name" },
    { 9, "size" }, { 5, "type" }, { 5, "type" },

    };
    LittleFsHandle_t *Node = LittleFsGetNode(num);
    if(Node) {
        uint32_t cnt = 0;
        lfs_dir_t dir = { 0 };
        int err = lfs_dir_open(&Node->lfs, &dir, path);
        res = LittleFsRetToRes(err);
        if(err) {
            table_header(&(curWriterPtr->stream), cols, ARRAY_SIZE(cols));
            struct lfs_info FileInfo = { 0 };
            bool loop = true;
            while(loop) {
                int ret = lfs_dir_read(&Node->lfs, &dir, &FileInfo);
                if(ret < 0) {
                    LOG_ERROR(LITTLE_FS, "ReadDirErr");
                    loop = false;
                    break;
                } else {
                    LOG_DEBUG(LITTLE_FS, "ReadOk %s", 
                              LittleFsFileInfoToStr(&FileInfo));
                    cnt++;
                }

                if(0 == ret) {
                    loop = false;
                    break;
                }

                char temp[150] = { 0 };
                strcpy(temp, TSEP);
                snprintf(temp, sizeof(temp), "%s %3u " TSEP, temp, cnt);
                snprintf(temp, sizeof(temp), "%s %15s " TSEP, temp, FileInfo.name);
                snprintf(temp, sizeof(temp), "%s %7u " TSEP, temp, FileInfo.size);
                snprintf(temp, sizeof(temp), "%s %3u " TSEP, temp, FileInfo.type);
                snprintf(temp, sizeof(temp), "%s %3s " TSEP, temp, LittleFsTypeToStr(FileInfo.type));
                cli_printf("%s"CRLF, temp);
            }

            table_row_bottom(&(curWriterPtr->stream), cols, ARRAY_SIZE(cols));
            err = lfs_dir_close(&Node->lfs, &dir);
            if(err) {
                res = false;
            }
        } 
    } 

    return res;
}

Можно прямо в консоль печатать содержимое корневой директории

Пуск NVRAM

Файловая система запущена. Мы можем писать и читать именованные массивы в энерго-независимую Flash память микроконтроллера. Красота! Однако это далеко не всё. Теперь надо как-то интерпретировать сохраненные данные. Надо добавить NVRAM. Зачем нужен NVRAM? Задача NVRAM в следующем:
1—прописать значения по умолчанию, если их не оказалось в памяти файловой системы при пуске устройства.
2—указать как интерпретировать бинарный массив данных лежащий в файловой системе в реальную физическую переменную.

Основная идея в том, чтобы для каждой записи NVRAM создавать бинарный файл с расширением *.nvram (или *.nv). Имя файла NVRAM имеет однообразную структуру. Вот например файл ID_123.nvram - это NVRAM запись с индексом 123. Именно по номеру ID отдельный программный компонент NVRAM производит синтаксический разбор того, что записано внутри бинарного файла. Вот небольшая подсказка. Это по сути LookUp таблица по которой вы поймете как именно распознавать то, что вы прочитаете из LittleFs-файла.

ID

тип данных

размер

Назначение

Units

Начальное значение по умолчанию

60

UINT32

4

MaxUpTime

ms

0

1

UINT32

4

reboot counter

enum

0

12

UINT8

1

Booloader command

N

launch

25

UINT32

4

Serial number

N

1

11

UINT32

4

boot address

phy address

0x0800_0000

3

UINT8

1

Time Zone

N

+10

2

UINT32

4

core frequency

Hz

250MHz

4

Array

6

MAC address

array

00:00:00:00:00:00

5

UINT32

4

IP address

array

192.168.1.1

6

string

N

SSID for WiFi

text

pentagon

7

string

N

WiFi password

text

1234

71

UINT32

4

WatchDog period

ms

3000

70

UINT8

1

WatchDog ctrl

on/off

on

8

UINT32

4

App CRC32

N

0x55555555

9

UINT32

4

App LEN

bytes

1

Эти переменные можно пополнять по мере развития проекта и заносить в константный массив структур. Например при работе с радио трансиверами Вы можете хранить в NVRAM настройки модуляции и мощности. Или ключи шифрования. Тут важно лишь то, чтобы все ID были разные. Поэтому их надо определить в перечислении Си (enum).

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

Перенос кода на микроконтроллер

Вот я полностью отладил NVRAM на LapTop-е. Теперь я просто переписываю конфиг и этот же код начинает работать на MCU. Инициализация прошла успешно.

Это диагностика переменных в памяти.

Достоинства NVRAM
Благодаря наличию в прошивке NVRAM перед вами открываются следующие возможности:

1++Вы можете до-программировать устройство уже в Run-time просто поменять конфигурацию в NVRAM.
2++Вы можете обновлять прошивку просто взяв ее из NVRAM
3++Вы можете передавать команды от приложения в загрузчик и наоборот. Например команду остаться в загрузчике или запуститься по другому смещению.
4++Ваше устройство может писать в NVRAM наработки на отказ
5++Вы можете сохранять в NVRAM логи, например, одометр автомобиля.
6++Простой и универсальный API. Можно сохранять данные любого размера по любому адресу (ключу).
7++При работе с NVRAM вам не надо беспокоиться о вероятном нахлёсте адресов и данных друг на друга. Вы можете для ID назначить любое число, а для длинны любое натуральное число.

Достоинства LittleFs

1++Написана на Си. Легко портировать в любой Си проект.
2++Dynamic wear leveling.
3++Power-loss resilience.

Недостатки LittleFs

1--Не собирается c GCC ключом -Werror=shadow.. Это детская ошибка написания Cи- кода почему-то просочилась с сорцы LittleFs.

2--LittleFs использует непереносимые типы данных. Тот же int может быть и 2 байта и 4 байта.

#ifndef LFS_NO_MALLOC
int lfs_file_open(lfs_t *lfs, lfs_file_t *file, const char *path, int flags) {
    int err = LFS_LOCK(lfs->cfg);
    if (err) {
        return err;
    }
    LFS_TRACE("lfs_file_open(%p, %p, \"%s\", %x)",
            (void*)lfs, (void*)file, path, (unsigned)flags);
    LFS_ASSERT(!lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file));

    err = lfs_file_open_(lfs, file, path, flags);

    LFS_TRACE("lfs_file_open -> %d", err);
    LFS_UNLOCK(lfs->cfg);
    return err;
}
#endif

3--LittleFs - это open-source код. Поэтому 101% исходники LittleFs не будут соответствовать вашему внутреннему самобытному code-style.

4--У LittleFS нет Lazy Write. В переводе на кухонный, если вы будете последовательно записывать одно и то же (бит в бит), то Little FS будет буквально записывать это, вместо того чтобы просто вернуть OK и указать, что эти данные нет смысла записывать, так как они уже присутствуют в памяти. Это приводит к необоснованным жертвам ресурса полей Flash памяти.

Как видно, одни и те же данные прописываются дважды (((
Как видно, одни и те же данные прописываются дважды (((

5--LittleFs падает при попытке открыть на чтение удаленный файл. Чтобы не происходило заклинивания программы в assert-е, надо собирать код с ключом -DLFS_NO_ASSERT.

Приложения LitteFs
1--SIM карты. Можно хранить телефонные номера на SIM карте.
2--Прошивки для микроконтроллеров. Можно в LittleFS файлике хранить целую прошивку и обновлять ее в bootloader-ах.
3--In-memory файловые системы. Можно собирать образ файловой системы и прописывать готовую файловую систему вместе с прошивкой на какой-н микроконтроллер или SPI-Flash ASIC.

Итоги

Удалось научиться пользоваться программным компонентом Little-Fs. Удалось организовать NVRAM поверх LittleFs.

Достоинство программирования на Си в том что вы можете одну и ту же программу разрабатывать отлаживать как на PC так и на микроконтроллере.

Словарь

Акроним

Расшифровка

FS

File System

RAM (ОЗУ)

Random-Access Memory

NVRAM

Non-Volatile Random-Access Memory

ASIC

Application-Specific Integrated Circuit

BSD

Berkeley Software Distribution licenses

API

Application Programming Interface

COW

copy-on-write

Ссылки

Название

URL

littlefs technical specification

https://github.com/littlefs-project/littlefs/blob/master/SPEC.md

The design of littlefs

https://github.com/littlefs-project/littlefs/blob/master/DESIGN.md

FatFs - Generic FAT Filesystem Module

https://elm-chan.org/fsw/ff/00index_e.html

Сорцы LittleFs

https://github.com/littlefs-project/littlefs

Консольная утилита, чтобы погонять LittleFs как процесс в Windows

https://github.com/aabzel/Artifacts/tree/main/little_fs_on_pc

LittleFS File System with MCU Internal FLASH Memory

LittleFS File System with MCU Internal FLASH Memory | MCU on Eclipse

Мажоритарный элемент

https://ru.wikipedia.org/wiki/Мажоритарный_элемент

Дайте мне 15 минут, и я изменю ваш взгляд на GDB @Djivs

https://habr.com/ru/articles/824638/

LittleFS – компактная и экономичная файловая система для ARM микроконтроллеров в составе mbed os. Быстрый старт @Sub_Dia

https://habr.com/ru/articles/347348/

STM32 - LittleFS Flash File System Interfacing

https://merkles.com/wiki/index.php/STM32_-_LittleFS_Flash_File_System_Interfacing

Вопросы:

  1. Как отладить большой кусок кода, если нет возможности (не хочется) пройтись по коду пошаговым отладчиком?

  2. Как организовать NVRAM при условии одноразовой записи страниц Flash?Когда микроконтроллер не позволяет до-записать уже прописанную страницу flash памяти.

  3. Увидит ли ПК файлы, если запрограммировать LittleFs на SD карте?

  4. Есть ли в LittleFs Lazy Write?

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