Backlinks Graph
Backlinks
Table of Contents

LittleFS

  2026-05-24

  Edited: 2026-05-29

Introduction

Pretty lightweight. You have to write your own read, prog, erase, and sync functions for your flash first and then pass in into a struct like the one below here. LittleFS also provides a context object which you can use to store extra information.

const struct lfs_config cfg = {
    // block device operations
    .context = your_context // this of type void*
    .read  = lfs_read,
    .prog  = lfs_prog,
    .erase = lfs_erase,
    .sync  = lfs_sync,

    // block device configuration
    .read_size = 16,
    .prog_size = 16,
    .block_size = 4096,
    .block_count = 128,
    .cache_size = 16,
    .lookahead_size = 16,
    .block_cycles = 500,
};

I have seen sync implement as simply a no-op, but I think a better idea might be to utilize the fact that the flash programming call is separated into two instructions, load and then execute. But that also takes more work but it is probably more performant as you are caching larger writes.

Implementing Instructions

I found this part actually quite hard, to write wrapper functions for little-fs. So for starters, little-fs was designed for NOR flash in mind, hence its block-heavy signatures for its read and prog functions.

static int flash_sync(const struct lfs_config *c);
static int lfs_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t offset,
                    void *data, lfs_size_t size);
static int lfs_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t offset,
                    const void *data, lfs_size_t size);
static int lfs_erase(const struct lfs_config *c, lfs_block_t block);

NOR flash allows you to index any byte using a 4-byte address (for 256 Mb and up) or using a 3-byte address (for 128 Mb and below). In this model, you use the address to index the nth-byte, so converting from blocks and offsets to this address is very easy

// Easy right? You can visualize why this works
uint32_t address = block * FLASH_BLOCK_SIZE + offset;

But for NAND flash, the address is actually quite different. Very different in fact. Now when you can separate addresses into row address (handles block and page addressing) and column addresses (handles byte offsets within a page). In fact reading and writing is now separated into two separate calls

  1. reading: first read a page into cache, then you can read offsets into the page
  2. writing: write bytes onto a page (offsets allowed), then you write the page onto the storage

So you handle the block and page addressing, and then you handle the byte offsets of the page. The addressing of course becomes a bit more difficult since little-fs is focused on NOR flash. It is not too hard but it takes a bit of thinking

// If we have 64 pages per block, then we need 6 bits (2^6 = 64) to index all
// the pages. We can use the rest to index blocks, but sometimes manufactors
// won't use all of them because the storage simply doesn't contain that many
// blocks.

// Notice that block * PAGES_PER_BLOCK is equivalent to (block << 6)
// since the pages are always multiples of 2.
//
// The offset can be more than a page (remember that offsets is actually
// offsets into the block) so we divide by the page_size to convert
// into page address.
//
// We then just add to combine the two addresses into a row address
uint32_t row_address = block * PAGES_PER_BLOCK + offset / PAGE_SIZE;
// This one is simpler, the column address is whatver is left from
// the page division, as that is the offset into the page itself.
uint32_t col_address = offset % PAGE_SIZE;

Then you can just pass these into the respective instructions, for instance reading requires READ_PAGE (row address) followed by a READ_PAGE_FROM_CACHE (col address) and writing requires PROGRAM_LOAD (col address) followed by a PROGRAM_EXECUTE (row address).

In particular, both READ_PAGE and PROGRAM_EXECUTE requires a delay of some kind. The delay is given as a maximum which you can either wait for or use the query the status registers of the flash to see if its completed before then. Sometimes these instructions might be called as part of lfs_sync but I find that makes things kind of complicated, so instead I just have them shoved into lfs_prog and lfs_read directly. Note that you still have to call lfs_file_sync periodically if you write to a file without closing it.

Pulling Data Out

You can use a couple of ways to get the data out of the microcontroller, I have been using the STM32 USB Mass Storage Device interface with some level of success and many levels of confusion to pull the data out. Then on my desktop I use littlfs-python extract --blocksize ... to pull the files out to another directory. Note that the blocksize must match for both the python one and in the C code. Note that the file content is gibberish, because of different encodings.

EDIT: I have switched to pulling data out using an UART communication channel. So if send a command like FILE over UART, it will send me a specific file. You can also implement other stuff on top, like mass erase or making files. I think this way might be a bit better compared to a mass storage device simply because you don't have to run through the entire storage.

EDIT2: I have switched to using USB CDC via the virtual COM port. It is basically like a faster version of the UART technique, just without the UART of course.

References