Writable File System with LittleFS

Microcontrollers often need a writable filesystem to keep log files with wear leveling, persist device configuration files, upload or delete AI model files, and so on. For this, Mongoose provides LittleFS (LFS) integration, which makes the standard file API work in your firmware: fopen(), fread(), etc.

If you only need to serve static web files, see the HTML, CSS, JS from Flash guide, which covers a read-only "embedded filesystem".

This video is a detailed step-by-step guide on how to enable LFS integration on the Nucleo-H563ZI board:

How to enable LittleFS integration

  • Add this to your mongoose_config.h:
    #define MG_ENABLE_LFS 1
    #define MG_LFS_SIZE (128 * 1024)
    #define MG_OTA MG_OTA_STM32H5  // Change if required. Adds flash API
    
  • Copy four files lfs.{c,h} and lfs_util.{c,h} from the LittleFS repo to your firmware code
  • Create an empty sys/dirent.h to signal to GCC that we have POSIX file support

How it works

The LittleFS project is located at https://github.com/littlefs-project/littlefs. It provides its own API, such as lfs_file_open(), lfs_file_read().

The ARM GCC compiler is usually bundled with the Newlib C library, which provides a way to override the standard C file API via a set of so-called syscalls. For example, all file opening functions like fopen() and open() eventually call _open(), which Newlib defines as weak. That means our firmware can define its own _open() syscall, and the linker will pick our implementation over Newlib's weak one.

The same goes for other syscalls. For example, by overriding _write(), we can use LittleFS for standard file writing functions like fprintf(), fputs(), fwrite() and so on. Standard descriptors 0-2 (stdin, stdout, stderr) do not go to LittleFS. Here is how Mongoose does it for _write():

int _write(int fd, char *ptr, int len) {
  struct mg_lfs_fd *f = find_fd(fd);
  return fd < 3 ? len
                : f == NULL ? -1 : lfs_file_write(&s_lfs, &f->file, ptr, len);
}

You can see the full source code of the LFS integration at https://github.com/cesanta/mongoose/blob/master/src/lfs.c

There, we override _open(), _read(), and other syscalls using the native LittleFS API, making standard C file calls work via LittleFS.

This mechanism is called "IO retargeting".

Note that the LFS integration uses the struct mg_flash Mongoose API, which is also used for OTA. The OTA code needs to know how to erase sectors and write to flash, what the flash size is, etc. That is why OTA support must be enabled even if you don't use OTA itself: we need it for the flash operations.

Virtual file system

The IO retargeting mechanism can be used to implement a VFS (virtual file system). Say we use LFS on internal flash and FatFS on an SD card. Then we can make _open() use LFS for all paths that start with /lfs/, and FatFS for all paths that start with /fat/. We can do the same for other functions that take a file name, like remove().

We can make LFS and FatFS file descriptors occupy different ranges, so we can tell which filesystem to use based on the file descriptor.

Then in our firmware, we can use the standard file API: fopen("/lfs/settings.json", "r") or fopen("/fat/log_1.txt", "a").