LittleFS is a file system designed for flash memory and power-loss tolerant that provides files and directories over a partition.
In the previous article, we saw how NVS allows us to efficiently save configuration variables (SSID, passwords). But NVS has a limitation: it works with numeric IDs, not file names.
What if you want to save an event log (log.txt), a CSV file with sensor data (data.csv), or even web resources (index.html)? NVS falls short.
We need a real file system. In the PC world, we use NTFS or FAT32. In the microcontroller world, the undisputed king is LittleFS.
Why LittleFS and not FAT?
You might think: “Why not use FAT32? That way I could read the memory from Windows”.
FAT32 is terrible for microcontroller Flash memory:
- Corruption: If power is lost while writing, the file system breaks.
- Wear: FAT constantly writes to the allocation table (the same sectors), wearing out the Flash at that point.
LittleFS is designed by ARM specifically for this:
- Fail-Safe: It is resistant to power outages. It uses a technique called Copy-on-Write. It never overwrites data; it writes to a new block and only updates the pointer when finished.
- Wear Leveling: Distributes data across the entire memory to make it last longer.
- RAM: Consumes very little RAM.
NVS vs LittleFS: Which one to choose?
| Feature | NVS (Non-Volatile Storage) | LittleFS |
|---|---|---|
| Paradigm | Key-Value (Numeric ID) | Files and Directories (POSIX) |
| Ideal Use | Configuration, calibration, counters | Logs, web resources, complex data |
| Overhead | Very low (fast) | Medium (needs buffers) |
| API | Zephyr NVS API (nvs_read) | Zephyr VFS API (fs_open, fs_read) |
| Structure | Flat | Hierarchical (Folders) |
Devicetree Definition
As with NVS, we need a Flash partition. But this time, we will tell Zephyr that this partition belongs to a file system.
In your .overlay file (or using the default storage_partition), we add the auto-mount configuration.
/ {
fstab {
compatible = "zephyr,fstab";
lfs1: lfs1 {
/* Automatically mount at /lfs1 */
compatible = "zephyr,fstab,littlefs";
mount-point = "/lfs1";
partition = <&storage_partition>;
automount;
/* Block configurations (optional, automatic if not set) */
read-size = <16>;
prog-size = <16>;
cache-size = <64>;
lookahead-size = <32>;
block-cycles = <512>;
};
};
};The fstab (File System Table) node is a modern Zephyr feature. It allows the system to automatically mount the disk at boot, just like Linux does with /etc/fstab.
Configuration (prj.conf)
We enable the file system subsystem and the LittleFS driver.
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_FILE_SYSTEM=y
CONFIG_FILE_SYSTEM_LITTLEFS=y
# Optional: Enable Shell commands for testing (Highly recommended!)
CONFIG_FILE_SYSTEM_SHELL=yUsing the File API
Here comes the good news: Zephyr uses the standard POSIX API.
If you’ve ever programmed in C for Linux or Windows (fopen, fwrite…), you already know how to use LittleFS in Zephyr.
The only difference is that we use Zephyr’s versions (fs_open, fs_write…) and struct fs_file_t structures.
Example: Writing a Boot Log
#include <zephyr/kernel.h>
#include <zephyr/fs/fs.h>
#include <zephyr/logging/log.h>
#include <string.h>
LOG_MODULE_REGISTER(demo_fs, LOG_LEVEL_INF);
/* File path (matches the mount-point from the overlay) */
#define LOG_FILE_PATH "/lfs1/boot_log.txt"
int main(void)
{
struct fs_file_t file;
int rc;
/* 1. Initialize the file structure */
fs_file_t_init(&file);
/* 2. Open (or create) the file */
/* Flags: CREATE (if it doesn't exist), WRITE (writing), APPEND (add to end) */
rc = fs_open(&file, LOG_FILE_PATH, FS_O_CREATE | FS_O_WRITE | FS_O_APPEND);
if (rc < 0) {
LOG_ERR("Error opening file: %d", rc);
return rc;
}
/* 3. Write data */
/* We can use snprintk to format text before writing */
char boot_msg[] = "System booted\n";
rc = fs_write(&file, boot_msg, strlen(boot_msg));
if (rc < 0) {
LOG_ERR("Error writing: %d", rc);
} else {
LOG_INF("Log saved successfully.");
}
/* 4. Close the file (This ensures data is saved to flash) */
fs_close(&file);
/* --- Reading to verify --- */
/* Reopen in read mode */
fs_open(&file, LOG_FILE_PATH, FS_O_READ);
char buffer[64];
ssize_t bytes_read;
LOG_INF("Log contents:");
while ((bytes_read = fs_read(&file, buffer, sizeof(buffer) - 1)) > 0) {
buffer[bytes_read] = '\0'; // Null terminator
printk("%s", buffer);
}
fs_close(&file);
return 0;
}Listing Files (ls)
We can also list the contents of a directory using fs_opendir and fs_readdir.
struct fs_dir_t dir;
struct fs_dirent entry;
fs_dir_t_init(&dir);
fs_opendir(&dir, "/lfs1");
while (1) {
fs_readdir(&dir, &entry);
/* If the name is empty, we are done */
if (entry.name[0] == 0) {
break;
}
printk("File: %s (Size: %zu bytes)\n", entry.name, entry.size);
}
fs_closedir(&dir);Exploring from the Shell
If you enabled CONFIG_FILE_SYSTEM_SHELL=y, you can manage files directly from the UART console without reprogramming. This is extremely useful for debugging.
Available commands:
fs mount: Shows mount points.fs ls /lfs1: Lists files.fs cd /lfs1: Changes directory.fs cat log.txt: Displays the contents of a file.fs rm log.txt: Deletes a file.
uart:~$ fs ls /lfs1
boot_log.txt 18