STM32 W25Qxx LittleFS
While working on STM32 W25Qxx I briefly experimented with getting FATFS up and running on the flash device. I quickly realised that this is a really poor idea. The problem is that W25Qxx flash devices has got a limited number of writes. FATFS works on some fixed tables and these tables will be updated on each file write. A typical W25Qxx got a number of guaranteed writes around 100000. If you imagine a system that update a file every second, this could mean that sector will wear out in < 2 days.
A much better approach is to use a file system with built-in wear levelling and fortunately LittleFS is just that.
This page describes how to get LittleFS on a w25qxx flash
Video
The thumbnail deserve a special mention. Up until this video, each and every thumbnail was hand crafted. This video however, I experimentally used Youtube's AI to generate the thumbnail and it managed to add 2 extra pins to the W25Q128 ic.
Adding littlefs source
Creating the interface between LittleFS and w25qxx
Header
/**
******************************************************************************
* @file : w25qxx_littlefs.h
* @brief : Littlefs on top of w25qxx header
******************************************************************************
* @attention
*
* Copyright (c) 2026 Lars Boegild Thomsen <lbthomsen@gmail.com>
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
#ifndef SRC_W25QXX_LITTLEFS_H_
#define SRC_W25QXX_LITTLEFS_H_
#include "lfs.h"
#include "w25qxx.h"
#define LITTLEFS_FORMAT_ON_ERROR 1 // when defined, littlefs will automatically reformat if any error
#ifdef DEBUGxxx
#define LFS_DBG(...) printf(__VA_ARGS__);
#else
#define LFS_DBG(...) ;
#endif
extern lfs_t littlefs;
int w25qxx_littlefs_init(W25QXX_HandleTypeDef *w25qxx_init, uint32_t reserved_mb);
#endif /* SRC_W25QXX_LITTLEFS_H_ */
/*
* vim: ts=4 sw=4 et nowrap
*/
Source
/**
******************************************************************************
* @file : w25qxx_littlefs.c
* @brief : Littlefs on top of w25qxx source
******************************************************************************
* @attention
*
* Copyright (c) 2026 Lars Boegild Thomsen <lbthomsen@gmail.com>
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
#include "w25qxx_littlefs.h"
#include "main.h"
#include "lfs.h"
#include "w25qxx.h"
// Define a structured context to cleanly isolate instance hardware and configuration offsets
typedef struct {
W25QXX_HandleTypeDef *hw_handle;
uint32_t flash_offset;
} LFS_DriverContext_t;
// Static driver context tracking runtime configurations
static LFS_DriverContext_t lfs_ctx;
// Maximum expected size for lookahead buffer tracking block usage states.
#define LFS_LOOKAHEAD_MAX_SIZE 16
static uint8_t lfs_lookahead_buf[LFS_LOOKAHEAD_MAX_SIZE];
// Static forward declarations for LittleFS block device operations
static int littlefs_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size);
static int littlefs_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size);
static int littlefs_erase(const struct lfs_config *c, lfs_block_t block);
static int littlefs_sync(const struct lfs_config *c);
// Global file system and configuration instances
lfs_t littlefs;
struct lfs_config littlefs_config = {
// Assign hardware callback function pointers
.read = littlefs_read,
.prog = littlefs_prog,
.erase = littlefs_erase,
.sync = littlefs_sync,
// Static tuning flags
.block_cycles = 500, // Increased to 500 for better wear leveling distribution
};
/**
* @brief Initializes and mounts LittleFS using the hardware driver dimensions,
* leaving a specified amount of megabytes untouched at the start of the flash.
* @param w25qxx_init: Pointer to the initialized hardware handle.
* @param reserved_mb: Number of Megabytes to reserve at the beginning of the flash (e.g., 2).
* @retval 0 on success, or a negative LittleFS error code on failure.
*/
int w25qxx_littlefs_init(W25QXX_HandleTypeDef *w25qxx_init, uint32_t reserved_mb) {
if (w25qxx_init == NULL) {
return LFS_ERR_INVAL;
}
LFS_DBG("LittleFS: Auto-configuring geometry...\n");
// 1. Populate custom context pointer boundaries
lfs_ctx.hw_handle = w25qxx_init;
lfs_ctx.flash_offset = reserved_mb * 1024 * 1024;
littlefs_config.context = &lfs_ctx;
// 2. Map geometry parameters dynamically
littlefs_config.block_size = w25qxx_init->sector_size;
// Calculate full physical block capabilities
uint32_t total_blocks = w25qxx_init->sectors_in_block * w25qxx_init->block_count;
uint32_t reserved_blocks = lfs_ctx.flash_offset / littlefs_config.block_size;
if (reserved_blocks >= total_blocks) {
LFS_DBG("LittleFS: Critical Error - Reserved space exceeds or equals flash capacity!\n");
return LFS_ERR_INVAL;
}
// Assign remaining flash blocks exclusively to LittleFS
littlefs_config.block_count = total_blocks - reserved_blocks;
// 3. Configure optimal I/O transactional boundaries
littlefs_config.read_size = 16; // Small reads prevent unnecessary SPI bus bloating
littlefs_config.prog_size = w25qxx_init->page_size; // Matches physical page layout (typically 256B), safe due to multi-page driver architecture
littlefs_config.cache_size = w25qxx_init->page_size;
// 4. Calculate lookahead size dynamically (1 bit per block, 32-bit aligned, min 8 bytes)
uint32_t lookahead = (littlefs_config.block_count + 7) / 8; // Convert blocks to bytes
lookahead = ((lookahead + 3) / 4) * 4; // Align up to a multiple of 4 bytes
if (lookahead < 8) {
lookahead = 8; // Enforce LittleFS minimum criteria
}
// Bounds check calculated lookahead up to allocated static limits
if (lookahead > LFS_LOOKAHEAD_MAX_SIZE) {
lookahead = LFS_LOOKAHEAD_MAX_SIZE;
}
littlefs_config.lookahead_size = lookahead;
littlefs_config.lookahead_buffer = lfs_lookahead_buf; // Assign the static lookup allocation map
LFS_DBG("LittleFS: Blk Size: %lu, Blk Count: %lu, Lookahead: %lu, Offset: %lu Bytes\n",
littlefs_config.block_size, littlefs_config.block_count, littlefs_config.lookahead_size, lfs_ctx.flash_offset);
// 5. Try mounting the filesystem
int err = lfs_mount(&littlefs, &littlefs_config);
#ifdef LITTLEFS_FORMAT_ON_ERROR // defined in header
// Reformat automatically if the filesystem is unreadable (unformatted or blank flash)
if (err) {
LFS_DBG("LittleFS: Mount failed (%d). Formatting flash partition...\n", err);
err = lfs_format(&littlefs, &littlefs_config);
if (err) {
LFS_DBG("LittleFS: Format critical failure (%d)\n", err);
return err;
}
// Remount following fresh layout formatting
err = lfs_mount(&littlefs, &littlefs_config);
}
#endif
if (err == LFS_ERR_OK) {
LFS_DBG("LittleFS: Filesystem mounted successfully.\n");
} else {
LFS_DBG("LittleFS: Initialization failed (%d)\n", err);
}
return err;
}
/**
* @brief LittleFS low-level read wrapper.
*/
static int littlefs_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) {
LFS_DriverContext_t *ctx = (LFS_DriverContext_t*) c->context;
LFS_DBG("LFS Rd: B=0x%04lx, O=0x%04lx, S=0x%04lx\n", block, off, size);
// Defensive Guardrail: Ensure requested operations do not spill outside allocated block partitions
if (block >= c->block_count) {
return LFS_ERR_INVAL;
}
// Factor in runtime offset partition shift
uint32_t raw_address = (block * c->block_size) + off + ctx->flash_offset;
if (w25qxx_read(ctx->hw_handle, raw_address, buffer, size) != W25QXX_Ok) {
return LFS_ERR_IO;
}
return LFS_ERR_OK;
}
/**
* @brief LittleFS low-level program (write) wrapper.
*/
static int littlefs_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) {
LFS_DriverContext_t *ctx = (LFS_DriverContext_t*) c->context;
LFS_DBG("LFS Prg: B=0x%04lx, O=0x%04lx, S=0x%04lx\n", block, off, size);
// Defensive Guardrail: Ensure requested operations do not spill outside allocated block partitions
if (block >= c->block_count) {
return LFS_ERR_INVAL;
}
// Factor in runtime offset partition shift
uint32_t raw_address = (block * c->block_size) + off + ctx->flash_offset;
// Safely discard the const attribute to comply with driver's signature interface
if (w25qxx_write(ctx->hw_handle, raw_address, (uint8_t*) buffer, size) != W25QXX_Ok) {
return LFS_ERR_IO;
}
return LFS_ERR_OK;
}
/**
* @brief LittleFS low-level sector erase wrapper.
*/
static int littlefs_erase(const struct lfs_config *c, lfs_block_t block) {
LFS_DriverContext_t *ctx = (LFS_DriverContext_t*) c->context;
LFS_DBG("LFS Ers: B=0x%04lx\n", block);
// Defensive Guardrail: Ensure requested operations do not spill outside allocated block partitions
if (block >= c->block_count) {
return LFS_ERR_INVAL;
}
// Force exact absolute hardware alignment starting positions
uint32_t target_sector_address = (block * c->block_size) + ctx->flash_offset;
// Clear exactly c->block_size (4096B) to safely align driver tracking parameters
if (w25qxx_erase(ctx->hw_handle, target_sector_address, c->block_size) != W25QXX_Ok) {
return LFS_ERR_IO;
}
return LFS_ERR_OK;
}
/**
* @brief LittleFS block device synchronization callback.
*/
static int littlefs_sync(const struct lfs_config *c) {
LFS_DriverContext_t *ctx = (LFS_DriverContext_t*) c->context;
// Explicitly check and block until physical chip completes internal operations
// to safeguard critical power-loss safe synchronization calls.
if (w25qxx_wait_for_ready(ctx->hw_handle, 1000) != W25QXX_Ok) {
return LFS_ERR_IO;
}
return LFS_ERR_OK;
}
/*
* vim: ts=4 sw=4 et nowrap
*/