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}andlfs_util.{c,h}from the LittleFS repo to your firmware code - Create an empty
sys/dirent.hto 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").