LittleFS port layer for the W25Q64 SPI NOR flash driver. Provides a complete lfs_config implementation using static buffers — no heap allocation required.
- Single-call setup:
W25Q_LFS_BuildConfigfills a ready-to-uselfs_config - No dynamic memory — all three LittleFS working buffers live inside
W25Q_LFS_Context - Inherits the transport-agnostic design of W25Q64-flash: works with any SPI peripheral via user-supplied callbacks
- Full 8 MB capacity: 2048 × 4 KB blocks, 256-byte program granularity
- Wear levelling enabled (
block_cycles = 500, matching the W25Q64's 100 000-cycle rating) - Power-loss safe writes: LittleFS never leaves the filesystem in an inconsistent state after a sudden power failure
- Tunable cache and lookahead sizes via compile-time macros
include/w25q64_lfs.h— public API andW25Q_LFS_Contextdefinitionsrc/w25q64_lfs.c— block device callback implementations andW25Q_LFS_BuildConfigexamples/mount_format.c— setup, mount, and auto-format on first bootexamples/file_rw.c— write, read, verify, and list directory contentsexamples/persistent_log.c— append-only sensor log with rotation across power cycles
Managed automatically by PlatformIO:
anurag3301/W25Q64-flash— W25Q64 SPI NOR flash driveranurag3301/littlefs— LittleFS filesystem
Add to your platformio.ini:
lib_deps = anurag3301/W25Q64-lfs@^0.1.4#include "w25q64_lfs.h"
/* -- Hardware ------------------------------------------------------------ */
#define FLASH_CS_PORT GPIOA
#define FLASH_CS_PIN GPIO_PIN_4
extern SPI_HandleTypeDef hspi1;
/* ------------------------------------------------------------------------ */
static int32_t flash_spi_transfer(void *user_context,
const uint8_t *tx, uint8_t *rx, size_t length)
{
SPI_HandleTypeDef *spi = (SPI_HandleTypeDef *)user_context;
HAL_GPIO_WritePin(FLASH_CS_PORT, FLASH_CS_PIN, GPIO_PIN_RESET);
HAL_StatusTypeDef r = HAL_SPI_TransmitReceive(spi, (uint8_t *)tx, rx,
(uint16_t)length, HAL_MAX_DELAY);
HAL_GPIO_WritePin(FLASH_CS_PORT, FLASH_CS_PIN, GPIO_PIN_SET);
return (r == HAL_OK) ? W25Q_OK : W25Q_ERR_IO;
}
static void flash_delay_ms(void *user_context, uint32_t delay_ms)
{
(void)user_context;
HAL_Delay(delay_ms);
}
/* Must remain valid for the lifetime of the mounted filesystem */
static W25Q_LFS_Context lfs_ctx;
int main(void)
{
/* HAL init, clock, GPIO, SPI go here */
W25Q_Config flash = {
.transfer_fn = flash_spi_transfer,
.delay_ms_fn = flash_delay_ms,
.user_context = &hspi1
};
struct lfs_config lfs_cfg;
W25Q_LFS_BuildConfig(&flash, &lfs_ctx, &lfs_cfg);
lfs_t lfs;
if (lfs_mount(&lfs, &lfs_cfg) != LFS_ERR_OK) {
lfs_format(&lfs, &lfs_cfg);
lfs_mount(&lfs, &lfs_cfg);
}
/* Use the LittleFS API normally */
lfs_file_t file;
lfs_file_open(&lfs, &file, "/hello.txt", LFS_O_WRONLY | LFS_O_CREAT);
lfs_file_write(&lfs, &file, "hello", 5);
lfs_file_close(&lfs, &file);
lfs_unmount(&lfs);
while (1) {}
}typedef struct {
const W25Q_Config *flash;
uint8_t read_buf [W25Q_LFS_CACHE_SIZE];
uint8_t prog_buf [W25Q_LFS_CACHE_SIZE];
uint8_t lookahead_buf[W25Q_LFS_LOOKAHEAD_SIZE];
} W25Q_LFS_Context;Holds the W25Q_Config pointer and the three RAM buffers LittleFS requires.
Allocate one instance per mounted filesystem. It must remain valid (not go out
of scope) for the entire time the filesystem is mounted — declare it static
or at global scope.
int32_t W25Q_LFS_BuildConfig(const W25Q_Config *flash,
W25Q_LFS_Context *ctx,
struct lfs_config *out_cfg);Fills out_cfg so it is ready for lfs_mount() or lfs_format(). Wires up
the four block device callbacks, sets flash geometry constants, and points
LittleFS at the buffers inside ctx.
| Parameter | Description |
|---|---|
flash |
Initialised W25Q_Config with SPI callbacks set up |
ctx |
Caller-allocated W25Q_LFS_Context; must remain valid while mounted |
out_cfg |
Output: filled lfs_config ready for lfs_mount / lfs_format |
Returns W25Q_OK on success, W25Q_ERR_INVALID_ARG if any pointer is NULL.
Override before including w25q64_lfs.h or via compiler flags:
| Macro | Default | Description |
|---|---|---|
W25Q_LFS_CACHE_SIZE |
256 |
Read and prog cache in bytes. Must be a multiple of 256 (one W25Q64 page). Larger values reduce flash reads but cost RAM. |
W25Q_LFS_LOOKAHEAD_SIZE |
64 |
Lookahead bitmap in bytes. Each byte tracks 8 blocks; 64 bytes covers 512 blocks (2 MB). Must be a multiple of 8. |
Example — double the cache to two pages:
#define W25Q_LFS_CACHE_SIZE 512
#include "w25q64_lfs.h"These values are fixed to match the W25Q64's physical layout and are set
inside W25Q_LFS_BuildConfig:
lfs_config field |
Value | Source |
|---|---|---|
read_size |
1 | NOR flash supports byte-granular reads |
prog_size |
256 | W25Q64 page size |
block_size |
4096 | W25Q64 sector size (minimum erase unit) |
block_count |
2048 | 8 MB ÷ 4 KB |
block_cycles |
500 | Wear-levelling aggressiveness (100 000-cycle chip ÷ ~200 headroom) |
W25Q_LFS_Context lifetime. LittleFS holds raw pointers to the buffers
inside W25Q_LFS_Context. If the context is a local variable and goes out of
scope while the filesystem is still mounted, the result is undefined behaviour.
Declare it static or at file scope.
Mount / format once. On a freshly erased or never-formatted chip
lfs_mount returns a negative error code. Call lfs_format once, then
lfs_mount again. After a successful format the chip always mounts cleanly on
subsequent boots without reformatting.
Always unmount before power-down. lfs_unmount flushes pending writes.
Skipping it is safe in the sense that LittleFS will recover on the next mount,
but the last in-progress write will be rolled back.
W25Q_Write does not erase. The block device callbacks erase at the
sector level before every write. You do not need to call W25Q_EraseSector
manually when using LittleFS — the erase callback handles it.
Wear levelling. block_cycles = 500 tells LittleFS to move data between
blocks to spread erase cycles evenly. The W25Q64 is rated for 100 000 erase
cycles per sector; with wear levelling enabled all 2048 sectors age together,
giving an effective endurance of roughly 100 000 × 2048 write operations
before any single sector wears out.