[S32K146 RT-Thread] LittleFS Adaptation on Internal PFLASH

RT-Thread IoT OS
9 min readJan 24, 2025

--

LittleFS is a lightweight file system designed for use with microcontroller internal flash and external NOR flash. It is particularly well-suited for small embedded systems compared to traditional FAT file systems, offering the following advantages:

  1. Power-Failure Resilience:
    LittleFS is built to handle unexpected power interruptions. All file operations provide strong copy-on-write guarantees, ensuring the file system can recover to the last known stable state after a power failure.
  2. Dynamic Wear Leveling:
    Specifically designed for flash memory, LittleFS provides dynamic block wear leveling. Additionally, it can detect bad blocks and work around them seamlessly.
  3. Minimal RAM/ROM Usage:
    LittleFS is optimized for memory-constrained environments. Its RAM usage is strictly bounded, meaning it does not grow with the size of the file system. Furthermore, it avoids unbounded recursion and limits dynamic memory allocation to configurable static buffers.

For more details, refer to the official documentation: LittleFS GitHub Repository.

In this experiment, we utilized the internal PFLASH of the S32K146 microcontroller, which offers 1MB of storage. Since this resource size leaves significant room for most embedded development projects, we allocated the last 512KB of PFLASH for the file system partition and managed it using LittleFS.

To achieve this, we modified the linker script to reserve the last 512KB of PFLASH for file system use. This experiment was conducted in the IAR environment, and the updated linker file is as follows:

The adaptation of LittleFS relies on the following configuration structure for the physical layer:



// Configuration provided during initialization of the littlefsstruct lfs_config {
// Opaque user provided context that can be used to pass
// information to the block device operations
void *context;

// Read a region in a block. Negative error codes are propagated
// to the user.
int (*read)(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, void *buffer, lfs_size_t size);

// Program a region in a block. The block must have previously
// been erased. Negative error codes are propagated to the user.
// May return LFS_ERR_CORRUPT if the block should be considered bad.
int (*prog)(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, const void *buffer, lfs_size_t size);
// Erase a block. A block must be erased before being programmed.
// The state of an erased block is undefined. Negative error codes
// are propagated to the user.
// May return LFS_ERR_CORRUPT if the block should be considered bad.
int (*erase)(const struct lfs_config *c, lfs_block_t block);


// Sync the state of the underlying block device. Negative error codes
// are propagated to the user.
int (*sync)(const struct lfs_config *c);


#ifdef LFS_THREADSAFE
// Lock the underlying block device. Negative error codes
// are propagated to the user.
int (*lock)(const struct lfs_config *c);


// Unlock the underlying block device. Negative error codes
// are propagated to the user.
int (*unlock)(const struct lfs_config *c);
#endif


// Minimum size of a block read in bytes. All read operations will be a
// multiple of this value.
lfs_size_t read_size;


// Minimum size of a block program in bytes. All program operations will be
// a multiple of this value.
lfs_size_t prog_size;


// Size of an erasable block in bytes. This does not impact ram consumption
// and may be larger than the physical erase size. However, non-inlined
// files take up at minimum one block. Must be a multiple of the read and
// program sizes.
lfs_size_t block_size;


// Number of erasable blocks on the device.
lfs_size_t block_count;

// Number of erase cycles before littlefs evicts metadata logs and moves
// the metadata to another block. Suggested values are in the
// range 100-1000, with large values having better performance at the cost
// of less consistent wear distribution.
//
// Set to -1 to disable block-level wear-leveling.
int32_t block_cycles;


// Size of block caches in bytes. Each cache buffers a portion of a block in
// RAM. The littlefs needs a read cache, a program cache, and one additional
// cache per file. Larger caches can improve performance by storing more
// data and reducing the number of disk accesses. Must be a multiple of the
// read and program sizes, and a factor of the block size.
lfs_size_t cache_size;


// Size of the lookahead buffer in bytes. A larger lookahead buffer
// increases the number of blocks found during an allocation pass. The
// lookahead buffer is stored as a compact bitmap, so each byte of RAM
// can track 8 blocks. Must be a multiple of 8.
lfs_size_t lookahead_size;


// Optional statically allocated read buffer. Must be cache_size.
// By default lfs_malloc is used to allocate this buffer.
void *read_buffer;

// Optional statically allocated program buffer. Must be cache_size.
// By default lfs_malloc is used to allocate this buffer.
void *prog_buffer;

// Optional statically allocated lookahead buffer. Must be lookahead_size
// and aligned to a 32-bit boundary. By default lfs_malloc is used to
// allocate this buffer.
void *lookahead_buffer;

// Optional upper limit on length of file names in bytes. No downside for
// larger names except the size of the info struct which is controlled by
// the LFS_NAME_MAX define. Defaults to LFS_NAME_MAX when zero. Stored in
// superblock and must be respected by other littlefs drivers.
lfs_size_t name_max;

// Optional upper limit on files in bytes. No downside for larger files
// but must be <= LFS_FILE_MAX. Defaults to LFS_FILE_MAX when zero. Stored
// in superblock and must be respected by other littlefs drivers.
lfs_size_t file_max;

// Optional upper limit on custom attributes in bytes. No downside for
// larger attributes size but must be <= LFS_ATTR_MAX. Defaults to
// LFS_ATTR_MAX when zero.
lfs_size_t attr_max;

// Optional upper limit on total space given to metadata pairs in bytes. On
// devices with large blocks (e.g. 128kB) setting this to a low size (2-8kB)
// can help bound the metadata compaction time. Must be <= block_size.
// Defaults to block_size when zero.
lfs_size_t metadata_max;

};

The configuration primarily includes the setup for reading, writing, and erasing operations on the physical layer device, as well as defining the minimum programming block properties of the flash memory.

Referring to the S32K146 features, the minimum erase sector size is 4096 bytes, and the minimum write operation size is 8 bytes. Based on LittleFS’s required configuration structure, we implement the corresponding functions.

Implementation of the read interface:





int lfs_mflash_read(const struct lfs_config *lfsc, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size){

struct lfs_mflash_ctx *ctx;
uint32_t flash_addr;

assert(lfsc);
ctx = (struct lfs_mflash_ctx *)lfsc>context; assert(ctx);


flash_addr = ctx->start_addr + block * lfsc->block_size + off;

for(lfs_size_t i=0; i < size; i++)
{

((int8_t *)buffer)[i] = *((__IO int8_t*)flash_addr);
flash_addr++;

}

return LFS_ERR_OK;


}

Implementation of the prog interface:


int32_t mflash_drv_program(uint32_t addr,uint32_t size,uint8_t * pdata)
{

status_t ret;

ret = FLASH_DRV_Program(&pSSDConfig,addr,size,pdata);

return ret == STATUS_SUCCESS ? 0 : -1;

}


int lfs_mflash_prog(
const struct lfs_config *lfsc, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size)
{
struct lfs_mflash_ctx *ctx;
uint32_t flash_addr;
int32_t ret;

ctx = (struct lfs_mflash_ctx *)lfsc->context;
flash_addr = ctx->start_addr + block * lfsc->block_size + off;
ret = mflash_drv_program(flash_addr,size,(uint8_t *)buffer);
return (ret == 0) ? LFS_ERR_OK : LFS_ERR_IO;
}

Implementation of the erase interface:

<blockquote>int32_t mflash_drv_erase(uint32_t dest,uint32_t size)

The corresponding configuration structure is as follows:

The adaptation interfaces have been fully implemented. Since LittleFS relies on dynamic malloc/free memory operations, some modifications were required in lfs_util.h to integrate with the RT-Thread system.

With these adjustments, all necessary adaptation interfaces for LittleFS have been completed. To validate its functionality, we added shell test commands to verify basic operations such as creating, deleting files and folders, reading, writing, and deletion tests. The shell test command implementation is as follows:

#include "drv_mflash.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <rtthread.h>

#define SHELL_Printf rt_kprintf
#define PRINTF rt_kprintf

/*******************************************************************************
* Variables ******************************************************************************/


lfs_t lfs;
struct lfs_config cfg;
int lfs_mounted;

static void format(int argc, char **argv)
{
int res;
if (lfs_mounted)
{
SHELL_Printf("LFS is mounted, please unmount it first.\r\n");
return;

}
if (argc != 2 || strcmp(argv[1], "yes"))
{
SHELL_Printf("Are you sure? Please issue command "format yes" to proceed.\r\n"); return;

}
res = lfs_format(&lfs, &cfg);
if (res)
{
PRINTF("\rError formatting LFS: %d\r\n", res);
}


return;

}
MSH_CMD_EXPORT(format,"lfs format api");


static void mount(int argc, char **argv)
{

int res;
if (lfs_mounted)
{
SHELL_Printf("LFS already mounted\r\n");
return;
}
res = lfs_mount(&lfs, &cfg);
if (res)
{
PRINTF("\rError mounting LFS\r\n");
}
else
{
lfs_mounted = 1;
}

return;
}
MSH_CMD_EXPORT(mount,lfs mount api);


static void unmount(int argc, char **argv)
{

int res;
if (!lfs_mounted)
{
SHELL_Printf("LFS not mounted\r\n");
return;
}
res = lfs_unmount(&lfs);
if (res)
{
PRINTF("\rError unmounting LFS: %i\r\n", res);
}

lfs_mounted = 0;
return;
}
MSH_CMD_EXPORT(unmount,lfs unmount api);

static void cd(int argc, char **argv)
{

SHELL_Printf(
"There is no concept of current directory in this example.\r\nPlease always specify the full path.\r\n");
return;
}
MSH_CMD_EXPORT(cd,lfs cd api);

static void lls(int argc, char **argv)
{
int res;
char *path;
lfs_dir_t dir;
struct lfs_info info;
int files;
int dirs;

if (!lfs_mounted)
{
SHELL_Printf("LFS not mounted\r\n");
return;
}

if (argc > 2)
{
SHELL_Printf("Invalid number of parameters\r\n");
return;
}

if (argc < 2)
{
path = "/";
}
else
{
path = argv[1];
}

/* open the directory */
res = lfs_dir_open(&lfs, &dir, path);
if (res)
{
PRINTF("\rError opening directory: %i\r\n", res);
return;
}

PRINTF(" Directory of %s\r\n", path);
files = 0;
dirs = 0;

/* iterate until end of directory */
while ((res = lfs_dir_read(&lfs, &dir, &info)) != 0)
{
if (res < 0)
{
/* break the loop in case of an error */
PRINTF("\rError reading directory: %i\r\n", res);
break;
}
if (info.type == LFS_TYPE_REG)
{
SHELL_Printf("%8d %s\r\n", info.size, info.name);
files++;
}
else if (info.type == LFS_TYPE_DIR)
{
SHELL_Printf("% DIR %s\r\n", info.name);
dirs++;
}
else
{
SHELL_Printf("%???\r\n");
}


}
res = lfs_dir_close(&lfs, &dir);
if (res)
{
PRINTF("\rError closing directory: %i\r\n", res);
return;
}
PRINTF(" %d File(s), %d Dir(s)\r\n", files, dirs);

return;
}
MSH_CMD_EXPORT(lls,lfs ls api);

static void rm(int argc, char **argv)
{
int res;

if (!lfs_mounted)
{
SHELL_Printf("LFS not mounted\r\n");
return;
}

res = lfs_remove(&lfs, argv[1]);

if (res)
{
PRINTF("\rError while removing: %i\r\n", res);
}
return;
}
MSH_CMD_EXPORT(rm,lfs rm api);

static void lmkdir(int argc, char **argv)
{
int res;
if (!lfs_mounted)
{
SHELL_Printf("LFS not mounted\r\n");
return;
}

res = lfs_mkdir(&lfs, argv[1]);

if (res)
{
PRINTF("\rError creating directory: %i\r\n", res);
}
return;
}
MSH_CMD_EXPORT(lmkdir,lfs mkdir api);

static void write(int argc, char **argv)
{
int res;
lfs_file_t file;

if (!lfs_mounted)
{
SHELL_Printf("LFS not mounted\r\n");
return;
}
res = lfs_file_open(&lfs, &file, argv[1], LFS_O_WRONLY | LFS_O_APPEND | LFS_O_CREAT); if (res)
{
PRINTF("\rError opening file: %i\r\n", res);
return;
}
res = lfs_file_write(&lfs, &file, argv[2], strlen(argv[2]));
if (res > 0)
res = lfs_file_write(&lfs, &file, "\r\n", 2);

if (res < 0)
{
PRINTF("\rError writing file: %i\r\n", res);
}

res = lfs_file_close(&lfs, &file);
if (res)
{
PRINTF("\rError closing file: %i\r\n", res);
}
return;
}
MSH_CMD_EXPORT(write,lfs write api);


static void cat(int argc, char **argv)
{
int res;
lfs_file_t file;
uint8_t buf[16];
if (!lfs_mounted)
{
SHELL_Printf("LFS not mounted\r\n");
return;
}

res = lfs_file_open(&lfs, &file, argv[1], LFS_O_RDONLY);
if (res)
{
PRINTF("\rError opening file: %i\r\n", res);
return;
}

do
{
res = lfs_file_read(&lfs, &file, buf, sizeof(buf));
if (res < 0)
{
PRINTF("\rError reading file: %i\r\n", res);
break;
}
if(res > 0)
{
buf[res] = '\0';
PRINTF("%s",(char *)buf);
}
} while (res);

res = lfs_file_close(&lfs, &file);
if (res)
{
PRINTF("\rError closing file: %i\r\n", res);
}

return;

}
MSH_CMD_EXPORT(cat,lfs cat api);

static void lfsinit(int argc, char **argv)
{
mflash_drv_init();
lfs_get_default_config(&cfg);
return;
}
MSH_CMD_EXPORT(lfsinit,lfs init api);


static void df(int argc, char **argv)
{
printf("used block %d total %d\r\n",lfs_fs_size(&lfs),cfg.block_count);
return;
}
MSH_CMD_EXPORT(df,lfs init api);

At this point, all preparations are complete. We used the shell to validate the file system’s functionality, and the shell commands successfully demonstrated file and folder operations.

Video link: S32K146 LittleFS Demo

Additionally, RT-Thread provides a LittleFS software package. However, this experiment did not use the package. In the future, we plan to validate LittleFS functionality using the RT-Thread software package.

Connect with RT-Thread!
Github | Linkedin | Twitter | Facebook | Youtube

--

--

RT-Thread IoT OS
RT-Thread IoT OS

Written by RT-Thread IoT OS

An Open-Source Community-Powered Real-Time Operating System (RTOS) Project! Let’s develop, DIY, create, share, and explore this new IoT World together!

No responses yet