1. Introduction to Flash Memory
2. Introduction to Little File System (LittleFS)
3. Integrate LittleFS on External Flash Storage
4. LittleFS Usage
5. Conclusion
1. Introduction to Flash Memory
All computer-based systems contain memory. Memory stores information while the CPU of the computer prepares to operate on it. We have two types of memory: volatile memory and non-volatile memory:
– Volatile memory: Volatile memory loses its contents when the device loses power. People commonly refer to this as Random Access Memory (RAM). Two examples of volatile memory with this characteristic are Static RAM (SRAM) and Dynamic RAM (DRAM).
– Non-volatile memory: This type of memory retains its contents even if the power is lost. Originally known as Read Only Memory (ROM), it was loaded with contents during manufacturing, allowing reading but not erasing or reprogramming. Over time, the ability to erase and reprogram ROM was added in different ways and referred to as Electrically Programmable ROM (EPROM), Electrically Erasable and Programmable ROM (EEPROM), and Flash EEPROM, commonly referred to simply as Flash Memory.
Flash memory is an electronic non-volatile computer memory storage medium that can be electrically erased and reprogrammed.
The two main architectures dominate flash memory: they are NOR and NAND.
- Page Write
- Block Erase
- More dense
- Fast (required) sequential access
- Used as file storage memory
- Byte / Word Write
- Block Erase
- Less dense
- Fast random access
- Used as program memory
Basic operation
Flash memory involves three fundamental operations: reading (a byte or a word), programming (a byte or a word), and erasing (one or more sectors).
-
Read: Normally, there are three types of read operations that the device provides: asynchronous read, asynchronous page read, and synchronous burst read
-
Write: The unprogrammed state of a flash memory cell is a high signal level or logical one. Changing a flash memory cell (or bit) to a low voltage level or zero is called programming.
-
Erase: The erasure of a flash device is done through multiple write accesses that form an erase command. The erase completion time is dependent upon the sector size and technology. The erase command sequence initiates the embedded erase algorithm – an internal algorithm that automatically preprograms the memory array (if it is not already programmed) before executing the erase operation simultaneously on all bits of the sector
2. Introduction to Little File System (LittleFS)
The Little File System (LittleFS) is a fail-safe file system designed for embedded systems, specifically for microcontrollers that use external flash storage. Microcontrollers don’t usually have a built-in file system. Projects that need a file system have a choice of which file system to use. Popular choices include the FAT file system and SPIFFS. These file systems work well but have limitations that make them difficult for developers to work with, especially developers more familiar with the full-featured file systems on computers. The LittleFS file system created by ARM is an excellent alternative to both SPIFFS and FAT, which is also easier for developers to use.
| | | .---._____
.-----. | |
--|o |---| LittleFS |
--| |---| |
'-----' '----------'
| | |
Microcontrollers and flash storage present three challenges for embedded storage: power loss, wear, and limited RAM and ROM. This file system provides a solution to all three of these problems.
- Bounded RAM/ROM:
This file system works with a limited amount of memory. It avoids recursion and limits dynamic memory to configurable buffers that can be provided statically.
- Power-loss resilient:
This design is tailored for operating systems that may have random power failures. It has strong copy-on-write guarantees and keeps storage on disk in a valid state.
- Wear leveling:
Because the most common form of embedded storage is erodible flash memories, the file system provides a form of dynamic wear leveling for systems that cannot fit a full flash translation layer.
Detailed documentation can be found in littlefs
Benefits of LittleFS
Both the design and implementation of LittleFS meet the needs of today’s embedded projects, combining essential features with ease of use.
- Real Directories and Long File Names:
LittleFS supports real directories, long file names (up to 254 bytes), and UTF-8 encoding for global projects.
- High Reliability:
LittleFS safeguards against power loss and wear using resilient data structures. It prevents corruption from sudden power cuts and extends flash memory life through even write distribution.
- Small Size:
LittleFS has a very small code size and uses surprisingly little RAM
- Thread Safe:
LittleFS provides hooks that allow it to be thread-safe.
- Configurable:
The light RAM use of LittleFS comes at a price: performance is not as fast as it could be. Fortunately, LittleFS can be configured to use a little more memory, with the result being greatly improved performance. As little as 1 KB more memory makes a significant difference.
3. Integrate LittleFS on External Flash Storage
Writing a Driver for External Flash Memory:
Choose a suitable flash memory for your project and write a driver for it. Ensure the driver includes basic functions like open, write, read, erase, and close to interact with the flash memory. Make sure the driver functions properly with the chosen flash memory.
The example below consists of several functions that perform fundamental operations for the flash memory MX25LM51245G. It encompasses basic flash operations such as open, read, write, erase, and more. You can use this as a reference.
fsp_err_t MX25LM51245G_FlashInit(spi_flash_ctrl_t * p_ctrl, spi_flash_cfg_t const * const p_cfg);
fsp_err_t MX25LM51245G_FlashClose(spi_flash_ctrl_t *p_ctrl);
fsp_err_t MX25LM51245G_FlashWrite(ospi_instance_ctrl_t * p_instance_ctrl, uint8_t * const addr, uint8_t const * const ptrBuff, uint32_t writeSize);
void MX25LM51245G_FlashRead(spi_flash_ctrl_t * p_ctrl, uint8_t * const addr, uint8_t *ptrBuff, uint32_t readSize);
fsp_err_t MX25LM51245G_Erase(spi_flash_ctrl_t * p_ctrl, uint8_t *const addr, uint32_t byte_count);
fsp_err_t MX25LM51245G_SectorErase(spi_flash_ctrl_t * p_ctrl, uint8_t *const addr);
fsp_err_t MX25LM51245G_BlockErase(spi_flash_ctrl_t * p_ctrl, uint8_t *const addr);
fsp_err_t MX25LM51245G_ChipErase(spi_flash_ctrl_t * p_ctrl);
Integrating the LittleFS Library:
Download the LittleFS Library from this repo. The library consists of the files lfs_util.c, lfs_util.h, lfs.c, and lfs.h
Add the 4 downloaded files to your project
Open the lfs.h file now, and you’ll encounter the struct lfs_config. We’ll pass the flash-related functions to LittleFS using the lfs_config structure.
When you intend to use LittleFS, add the lfs.h library (#include "lfs.h"
) to your code. And create a struct config to pass the flash-related functions into that configuration structure.
const struct lfs_config cfg =
{
// block device operations
.context = opaque_user_provided_context,
.read = user_provided_block_device_read,
.prog = user_provided_block_device_program,
.erase = user_provided_block_device_erase,
.sync = user_provided_block_device_sync,
// block device configuration
.read_size = 16,
.prog_size = 16,
.block_size = 4096,
.block_count = 128,
.block_cycles = 500,
.cache_size = 16,
.lookahead_size = 16,
};
lfs_config | Description |
---|---|
.context | An opaque user-provided context that can be used to pass information to the block device operations. |
.read | Read a region in a block. |
.prog | Program a region in a block. The block must have previously been erased. |
.erase | Erase a block. A block must be erased before being programmed. The state of an erased block is undefined. |
.sync | Sync the state of the underlying block device |
.read_size | Minimum size of a block read. All read operations will be a multiple of this value |
.prog_size | Minimum size of a block program. All program operations will be a multiple of this value |
.block_size | Size of an erasable block. 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/program size |
.block_count | Number of erasable blocks on the device |
.block_cycles | Number of erase cycles before littlefs evicts metadata logs and moves the metadata to another block |
.cache_size | Size of block caches. 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 |
.lookahead_size | Size of the lookahead buffer in bytes. A larger lookahead buffer increases the number of blocks found during an allocation pass. |
4. LittleFS Usage
Littlefs takes in a configuration structure that defines how the filesystem operates. The configuration struct provides the filesystem with the block device operations and dimensions, tweakable parameters that tradeoff memory usage for performance, and optional static buffers if the user wants to avoid dynamic memory.
Here’s a simple example that writes “Hello” to a file named “myFile” inside the “myFolder” directory
#include “lfs.h”
lfs_t lfs;
lfs_file_t file;
// configuration of the filesystem is provided by this struct
const struct lfs_config cfg = {
// block device operations
.read = user_provided_block_device_read,
.prog = user_provided_block_device_prog,
.erase = user_provided_block_device_erase,
.sync = user_provided_block_device_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,
};
int main(void)
{
const char *dirName = “myFolder”;
const char *path = “myFolder/myFile”;
const char *content = “Hello”;
/* Open / Initial your external flash.*/
user_provided_flash_open_func();
/* Mount the filesystem. */
int lfs_err = lfs_mount (&lfs, &cfg);
if (lfs_err)
{
printf(“\r\n**Failed to mount littlefs flash port**\r\n”);
}
/* Create a directory. */
lfs_err = lfs_mkdir (&lfs, dirName);
if (lfs_err)
{
printf(“\r\n**Failed to create directory**\r\n”);
}
/* Open the file */
lfs_err = lfs_file_open(&lfs, &file, path, LFS_O_RDWR | LFS_O_CREAT | LFS_O_APPEND);
if (lfs_err)
{
printf(“\r\n**Failed to open the file**\r\n”);
}
/* Seek relative to the end of the file */
lfs_file_seek (&lfs, &file, 0, LFS_SEEK_END);
/* Apply “Hello” to “myFile” 10 times */
for (int i = 0; i < 10; i++) {
lfs_err = lfs_file_write(&lfs, &file, content, strlen(content));
if (lfs_err) {
printf(“\r\n**Failed to write content to file**\r\n”);
}
}
/* Close the file. Remember the storage is not updated until the file is closed successfully */
lfs_err = lfs_file_close (&lfs, &file);
if (lfs_err)
{
printf(“\r\n**Failed to close littlefs flash port**\r\n”);
}
/* Unmount the filesystem. */
lfs_err = lfs_unmount (&lfs);
if (lfs_err)
{
printf(“\r\n**Failed to unmount littlefs flash port**\r\n”);
}
}
Format: int lfs_format(lfs_t *lfs, const struct lfs_config *config);
Formatting a file system refers to preparing the storage medium (such as flash memory) to be used by the file system. It involves initializing the necessary data structures, setting up metadata, and clearing any existing data. Formatting is usually done when setting up the file system for the first time or when you want to erase all data and start fresh. Care must be taken when performing a format, as it will result in the complete removal of all data stored on the storage medium
Mount: int lfs_mount(lfs_t *lfs, const struct lfs_config *config);
Mounting a file system means making it accessible and usable by the operating system or application. When you “mount” a file system, you establish a connection between the file system’s data structures and the storage medium. It allows you to access files and directories stored on that file system. Mounting is a necessary step before you can interact with files and directories.
Unmount: int lfs_unmount(lfs_t *lfs);
Unmounting a file system means disconnecting or detaching the file system from the storage medium or device it is associated with. When you unmount a LittleFS instance, you are effectively closing the connection between the file system and the physical storage device, such as flash memory.
Remove: int lfs_remove(lfs_t *lfs, const char *path);
Removes a file or directory. If removing a directory, the directory must be empty
Rename: int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath);
Rename or move a file or directory. If the destination exists, it must match the source in type. If the destination is a directory, the directory must be empty
Get status: int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info);
Find information about a file or directory. Fills out the info structure, based on the specified file or directory
Open a file: int lfs_file_open(lfs_t *lfs,lfs_file_t *file,const char *path,int flags);
The mode that the file is opened in is determined by the flags:
LFS_O_RDONLY | : Open a file as read only |
---|---|
LFS_O_WRONLY | : Open a file as write only |
LFS_O_RDWR | : Open a file as read and write |
LFS_O_CREAT | : Create a file if it does not exist |
LFS_O_EXCL | : Fail if a file already exists |
LFS_O_TRUNC | : Truncate the existing file to zero size |
LFS_O_APPEND | : Move to end of file on every write |
Close a file: int lfs_file_close(lfs_t *lfs, lfs_file_t *file);
Any pending writes are written out to storage as through. Sync had been called and release any allocated resources
Read: lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, void *buffer, lfs_size_t size);
Read data from a file. Takes a buffer and size indicating where to store the read data. Returns the number of bytes read, or a negative error code on failure
Write: lfs_soff_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, const void *buffer, lfs_size_t size);
Write data to file. Takes a buffer and size indicating the data to write. The file will not actually be updated on the storage until either sync or close is called
Seek: lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, lfs_soff_t off, int whence);
The change in position is determined by the offset and whence flag.
LFS_SEEK_SET | : Seek relative to an absolute position |
---|---|
LFS_SEEK_CUR | : Seek relative to the current file position |
LFS_SEEK_END | : Seek relative to the end of the file |
Create a directory: int lfs_mkdir(lfs_t *lfs, const char *path);
Open a directory: int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path);
Once open a directory can be used with read to iterate over files
Close a directory: int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir);
Release any allocated resources
5. Conclusion
There’s no single best choice for a file system in an embedded project. Each project has its own requirements. For example, if reading data from an SD Card is a requirement, then FAT32 is the only choice. That said, for many projects, LittleFS is now the best choice. Its compact code size, minimal use of RAM, support for directories and long file names, and error resilience make it an easy choice. Because LittleFS behaves more like the file systems on computers, it is also easier for developers to work with.
If you encounter any issues, don’t hesitate to contact us at [email protected]
Thanks for sharing this helpful tutorial. Looking forward to your new post.