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

image alt В декабре 2017 года компания ARM представила обществу новую версию операционной системы для ARM микроконтроллеров «arm mbed os v.5.7» (17 января 2018 вышла версия 5.7.3), которая получила в своем составе интегрированную авторскую файловую систему, незатейливо названную «LittleFileSystem», или просто «LittleFS». Предлагаю сегодня поговорить об этой новинке.

Основными достоинствами файловой системы «LittleFS» являются:

  • Низкая требовательность к ресурсам микроконтроллера. Код не использует рекурсивных вызовов и работает без динамического выделения памяти. В «LittleFS» объем потребляемой ОЗУ всегда остается постоянным, несмотря на то, какие объемы записываются на накопитель, и на объем самого накопителя;
  • Наличие программных средств для выравнивания износа носителя (так называемый wear leveling), позволяющих свести к минимуму повторное использование блоков носителя;
  • Устойчивость к сбоям по питанию. Данная особенность является штатно рассматривающей прекращение подачи питания носителя, и использует механизм «copy-on-write (COW)», при котором данные не перезаписываются, а сохраняются на новое место.

Система поддерживает полный набор POSIX функций для работы с файлами и директориями.

Напомню, что файловая система интегрирована в состав активно развиваемой и поддерживаемой производителем операционной системы arm mbed os, так что немного сказать об этой ОС для ARM микроконтроллеров придется и мне, несмотря на определенное количество материалов о ней, опубликованных на ресурсе. Поэтому здесь я немного отвлекусь на mbed os, и тот, кто имеет уже опыт работы с ней, могут пролистать следующие несколько абзацев.

Операционная система написана на C/C++, и основным достоинством ОС, и её же основным недостатком, по моему мнению (как бы парадоксально это ни звучало), является действительно высокий уровень абстракции от аппаратной части.

Те, кто при работе с микроконтроллерами STM32 (именно в привязке к ним я веду беседу) пользовался библиотеками HAL и SPL (а тем более те, кто копошился в регистрах вручную), помнят, сколько строк кода необходимо было создать для настройки, к примеру, прерывания по падающему фронту на выводе PA_5: разрешение тактирование порта, настройка режима работы конкретного вывода, настройка собственно прерывания, разрешение прерывания – и только потом описание обработчика.

В mbed os все делается намного проще.

        InterruptIn irq_button1(PA_5);
 	irq_button1.fall(led_blink);

Сначала я дал имя прерыванию и указал вывод, по изменению состояния которого я хочу вызвать прерывание. Во второй строке я указал признак, по которому мне нужно переходить в обработчик (fall, падающий фронт сигнала), и указал название самого обработчика прерывания (led_blink).

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

Самый простой способ начать работу с mbed os – это отправиться на портал mbed.com, и зарегистрироваться там в качестве разработчика (элементарная регистрация с подтверждением по e-mail). С этого момента вам доступен онлайн компилятор, который сыграет немаловажную роль для старта. Вкратце, что мы собираемся сделать: в качестве старта и чтобы нам было полегче, мы возьмем любой пример, включающий в свой состав mbed os из репозитория на портале os.mbed.com, и потом его импортируем в виде проекта для нашей IDE и в идеале, для нашего микроконтроллера. ОС, кстати, является также инструментом маркетинга, и для ее использования ARM рекомендует так называемые «mbed-enabled» отладочные платы, и на первый взгляд может показаться, что для mbed os подходят только платы из каталога на сайте. Но это, конечно же, не так.

image

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

image

После нажатия на кнопку, появится окошко выбора конкретной платы. Там же, в окошке, будет кнопка с большим зеленым плюсом «Add Platform».

image

В отдельном окне браузера откроется окно выбора платформ, среди множества которых выберем «STMicroelectronics», и подберем плату, на которой установлен такой же микроконтроллер, как тот, с которым мы собираемся работать. Мне повезло – на моей самодельной отладочной плате стоит микроконтроллер «STM32F103RBT6», как и на плате «Nucleo-F103RB».

В ближайшем будущем я планирую написать небольшой материал о том, как реализовать проект с arm mbed os под любой произвольный МК.

На странице с описанием платы, которую мы выбрали в селекторе, справа, жмем кнопку «Add to your Mbed Compiler».

image

Тут же, чуть ниже, расположены примеры. Мы выберем самый маленький и простой – «Nucleo_blink_led».

image

В открывшемся окне, справа вверху, жмем кнопку «Import Into Compiler».

image

Откроется Ваш онлайн компилятор и попросит подтверждения импорта проекта. После подтверждения мы увидим наш проект в дереве проектов слева.

image

Осталось дело за малым – импортировать проект под нашу десктопную IDE. Онлайн компилятор поддерживает массу IDE, среди которых «IAR for ARM», «KEIL uVision», «CooCox CoIDE», и много прочих сред. Я пользуюсь средой «IAR for ARM v.8.20.1», ARM же для работы с mbed os рекомендует IAR версии не ниже 7.5.

Справа, в блоке «Program Details», жмем на кнопку «Export».

image

В появившемся всплывающем окне «Export program» выбираем платформу и среду разработки, которая установлена на нашем компьютере.

image

После нажатия на кнопку «Export» онлайн компилятор немного подумает, и в браузере начнется закачка архива с проектом. Распаковываем в любом удобном для нас месте, открываем среду разработки, открываем проект, вычищаем все ненужное нам в главном файле «main.cpp», начинаем работать.

Кстати, все в том же блоке «Program details», прежде чем экспортировать проект, можно оценить степень нагрузки проекта на на встроенную flash и оперативную память нашего тарджет-контроллера. Откроем вкладку «Build».

image

На этом, пожалуй, я закончу и без того затянувшийся экскурс в быстрый старт с mbed os, и вернусь к разговору о LittleFS.

Для корректной работы с файловой системой мы должны включить в проект заголовочные файлы «LittleFileSystem.h» и, обязательно, заголовочный файл носителя. Я предполагаю работу с SD-картой, поэтому включаю в проект файл «SDBlockDevice.h». Среди прочих устройств доступны MBRBlockDevice, HeapBlockDevice (очень удобен для тренировки, так как не требует присутствия физического носителя), и другие типы устройств.

// Block device
#include "SDBlockDevice.h"
// File system
#include "LittleFileSystem.h"

Итак, я предлагаю рассмотреть нашу последовательность действий при построении стартового проекта для работы с «LittleFS». Сначала мы должны создать объекты устройства (и инициализировать его выводы) и файловой системы. Затем мы смонтируем файловую систему, создадим пару файлов на носителе, откроем корневую директорию и считаем из нее список файлов, содержащихся в ней.

Обратим внимание на древо наследования классов для работы с потоками, реализованных в mbed os.

image

Как видим, для работы с файлами у нас реализован целый класс «File». Для работы с директориями существует класс «Dir». Их описание можно почитать на страницах File и Dir.

Однако на практике проявился один интересный нюанс.
При попытке объявить объект класса «File» и использовать метод File (FileSystem *fs, const char *path, int flags=O_RDONLY), призванный создавать файлы, компилятор отказался признать объект.

image

А при использовании стандартных методов работы с потоками («stdio.h») для создания файла, и при попытке дальнейшей работы с ним с помощью методов класса «File», таких, к примеру, как read или write, операционная система весело сваливалась в mbed_die(), обработчик неисправимой системной ошибки.

Поэтому пока что я ограничусь написанием письма в саппорт ARM, и работать с файлами будем с помощью средств stdio, что, возможно, для кого-то окажется делом более привычным. Полное описание функционала «stdio» можно найти, к примеру, на этом сайте.

Итак, код:

#include "mbed.h"
#include <stdio.h>
#include <errno.h>

// Block device
#include "SDBlockDevice.h"
// File systems
#include "LittleFileSystem.h"

SDBlockDevice bdsd(PB_15, PB_14, PB_13, PB_12); // mosi, miso, sclk, cs

LittleFileSystem fs("fs");

/******************************************************************************/
// main() runs in its own thread in the OS
int main() {
  
    printf("--- Mbed OS filesystem example ---\n");
    
    // Try to mount the filesystem
    printf("Mounting the filesystem... ");
    fflush(stdout);
    int err = fs.mount(&bdsd);
    printf("%s\n", (err ? "Fail :(" : "OK"));
    if (err) {
        // Reformat if we can't mount the filesystem
        // this should only happen on the first boot
        printf("No filesystem found, formatting... ");
        fflush(stdout);
        err = fs.reformat(&bdsd);
        printf("%s\n", (err ? "Fail :(" : "OK"));
        if (err) {
            error("error: %s (%d)\n", strerror(-err), err);
        }
    }
    
     // Open the LittleFS.txt file
    FILE *f = fopen("/fs/LittleFS.txt", "r+");
    printf("%s\n", (!f ? "Fail :(" : "OK"));
      if (!f) {
        // Create the LittleFS file if it doesn't exist
        printf("No file found, creating a new file... ");
        fflush(stdout);
        f = fopen("/fs/LittleFS.txt", "w+");
        printf("%s\n", (!f ? "Fail :(" : "OK"));
          if (!f) {
            error("error: %s (%d)\n", strerror(errno), -errno);
              }
      }
    
    printf("\r Closing \"/fs/LittleFS.txt\"... ");
    fflush(stdout);
    err = fclose(f);
    printf("%s\n", (err < 0 ? "Fail :(" : "OK"));
    if (err < 0) {
        error("error: %s (%d)\n", strerror(errno), -errno);
    }
    
    // Open the Habrahabr.txt file
    f = fopen("/fs/Habrahabr.txt", "r+");
    printf("%s\n", (!f ? "Fail :(" : "OK"));
      if (!f) {
        // Create the LittleFS file if it doesn't exist
        printf("No file found, creating a new file... ");
        fflush(stdout);
        f = fopen("/fs/Habrahabr.txt", "w+");
        printf("%s\n", (!f ? "Fail :(" : "OK"));
          if (!f) {
            error("error: %s (%d)\n", strerror(errno), -errno);
              }
      }
    
    printf("\r Closing \"/fs/Habrahabr.txt\"... ");
    fflush(stdout);
    err = fclose(f);
    printf("%s\n", (err < 0 ? "Fail :(" : "OK"));
    if (err < 0) {
        error("error: %s (%d)\n", strerror(errno), -errno);
    }
    
    // Display the root directory
    printf("Opening the root directory... ");
    fflush(stdout);
    DIR *d = opendir("/fs/");
    printf("%s\n", (!d ? "Fail :(" : "OK"));
    if (!d) {
        error("error: %s (%d)\n", strerror(errno), -errno);
    }
 
    printf("root directory:\n");
    while (true) {
        struct dirent *e = readdir(d);
        if (!e) {
            break;
        }
        printf("    %s\n", e->d_name);
    }
    printf("Closing the root directory... ");
    fflush(stdout);
    err = closedir(d);
    printf("%s\n", (err < 0 ? "Fail :(" : "OK"));
    if (err < 0) {
        error("error: %s (%d)\n", strerror(errno), -errno);
    }
 
    // Unmounting
    printf("Unmounting... ");
    fflush(stdout);
    err = fs.unmount();
    printf("%s\n", (err < 0 ? "Fail :(" : "OK"));
    if (err < 0) {
        error("error: %s (%d)\n", strerror(-err), err);
    }
        
    printf("LittleFS tested successfully!\n");
}

Сначала мы создаем объекты классов SDBlockDevice (и тут же его инициализируем, передавая названия выводов в строгой последовательности в соответствии с назначением: mosi, miso, sclk, cs) и LittleFileSystem (fs).
Затем, уже в функции «main», пробуем смонтировать файловую систему (что у нас, конечно же, не получится при использовании новой SD карты). Форматируем носитель, и монтируем файловую систему снова.

Далее, пробуем поочередно открыть файлы «LittleFS.txt» и «Habrahabr.txt», и, не находя их, создаем. После открытия файл обязательно должен быть закрыт.

После благополучных операций с файлами, мы вычитываем содержимое корневой директории и выводим названия файлов в терминал ввода/вывода. Закрываем директорию.

Демонтируем файловую систему и докладываем в терминал о благополучной проверке работы файловой системы.

А вот скриншот сообщений в терминале отладчика.

image

Итак, сегодня мы поговорили о перспективной и, на мой взгляд, весьма современной файловой системе для микроконтроллеров под названием «LittleFileSystem».

Когда разрешится вопрос с использованием «родных» средств работы с файлами и директориями (с помощью классов File и Dir), обещаю апдейт к статье.
Благодарю за внимание.

UPDATE.


Не успел материал пройти премодерацию, как меня осенило — я неверно пытался использовать методы класса File. Для создания (или открытия существующего) файла необходимо использовать метод open(FileSystem *fs, const char *path, int flags=O_RDONLY);.
В итоге, код дополнится новыми объектами классов File и Dir для работы с файлами и директориями, к примеру,
File fhandle;
Dir dhandle;


Так что весь пример работы с помощью интегрированных в ОС mbed os средств работы с файлами и директориями несколько преобразится. Также, кроме того, что мы создаем и/или открываем файлы, давайте уже туда что-нибудь запишем и считаем. Для этих целей создадим два буфера: из одного будет записываться в файл строка, а во второй мы считаем содержимое файла.
#define BLOCK_SIZE 16
char block[BLOCK_SIZE] = "tasty cake";
char rblock[BLOCK_SIZE];


Итак, код для операций с содержимым SD-карты (между тем, как мы смонтируем и демонтируем файловую систему).

/******************************************************************************/    
   err = fhandle.open(&fs,"testing.txt", (O_RDWR|O_TRUNC));
   if (err<0){
    printf("No file found, creating a new file...\n");
    fflush(stdout);
    err = fhandle.open(&fs,"testing.txt", (O_RDWR|O_CREAT));}
   err = fhandle.write(block,10);
   if (err<0) printf("Writing error!\n"); 
   printf("Written bytes (%d) ones\n", err);
 
   fhandle.rewind();                            //go to the file beginning
   
   err = fhandle.read(rblock,10);
   if (err<0) printf("Reading error!\n");
   printf("Read bytes (%d) ones\n", err);
   printf("Read from file: %s\n", rblock);
   err = fhandle.size();
   printf("File size (%d) bytes\n", err);
   printf("Closing file...");
   err = fhandle.close();
   if (err<0) printf("Closing file failure!\n");
   else printf("...OK\n");
/******************************************************************************/    
    // Display the root directory
    printf("Opening the root directory... ");
    fflush(stdout);
    err = dhandle.open(&fs,"/");
    if (err<0) printf("Opening directory error\n");
    else printf("OK\n");
    
    printf("root directory:\n");
    struct dirent *e = dhandle.readdir();             //Get the directory entry
    while (true) {
        err = dhandle.read(e);                        //Read while != 0 (end)  
        if (!err) {
            break;
        }
        printf("    %s\n", e->d_name);
    }
    printf("Closing the root directory... ");
    fflush(stdout);
    err = dhandle.close();
    if (err<0) printf("Closing directory error\n");
    else printf("OK\n");
/******************************************************************************/    

В самом начале мы пытаемся открыть файл testing.txt для чтения/записи. Обратите внимание, что кроме основных параметров (файловая система и название файла), мы передаем методу флаги O_RDWR и O_TRUNC, объединенные с помощью битовой операции «или»: так предполагают правила работы с ОС. Первый означает, что мы создаем или открываем файл для записи/чтения, второй — что файл будет полностью перезаписан, даже если он существует, и наполнен какой-либо информацией. Полный список флагов приведен с заголовочном файле "mbed_retarget.h":
#define O_RDONLY 0      ///< Open for reading
#define O_WRONLY 1      ///< Open for writing
#define O_RDWR   2      ///< Open for reading and writing
#define O_CREAT  0x0200 ///< Create file if it does not exist
#define O_TRUNC  0x0400 ///< Truncate file to zero length
#define O_EXCL   0x0800 ///< Fail if file exists
#define O_APPEND 0x0008 ///< Set file offset to end of file prior to each write
#define O_BINARY 0x8000 ///< Open file in binary mode

В случае успешного открытия (создания) файла мы пишем в него то, что содержится в буфере block, а точнее, первые 10 байт. Результатом выполнения операции будет количество записанных байт при успешной записи, и это значение мы выводим в терминал при отладке.
Чтобы успешно считать данные из файла, нам нужно вернуться в начало файла, и мы это делаем с помощью fhandle.rewind(). Считанные байты мы тоже считаем, и их число выводим в терминал, как и объем файла (fhandle.size()).
В процессе отладки заглядываем в буфер rblock, в который мы считывали строку из файла. Все в порядке:
image

С файлом мы побаловались, теперь посмотрим, что же находится в корневом каталоге файловой системы. Открываем директорию и получаем номер записи в каталоге: dhandle.readdir(). Теперь мы считываем название всех файлов, пока dhandle.read(e) не вернёт «0» — записей в директории больше нет. Выводим названия файлов, закрываем каталог.
image

Любуемся строками в отладочном терминале.
Еще раз всем спасибо.
  • +14
  • 6,3k
  • 4
Поделиться публикацией

Комментарии 4

    0
    Всё это прекрасно и замечательно, но только ~50KB флэша надо mbed отдать.
    Конечно если брать процессор с 128KB и более, то не жалко.
      0
      а эта ОС будет работать с графическими библиотеками, например с emWin from Segger?
        0

        Честно говоря, я с emWin не работал. Но не вижу ни малейшего препятствия — добавляете библиотеку в проект и работаете как обычно.
        При необходимости, выносите обработку графики в отдельный таск. Чтобы разнести ресурсы на всякий случай.

          0
          Спасибо за ответ, но если вы с emWin не работали, то тут не так все просто как вы описываете.

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

      Самое читаемое