简介
LittleFS 由ARM官方发布,ARM mbedOS的官方推荐文件系统,具有轻量级,掉电安全的特性。主要用在微控制器和flash上
-
掉电恢复,在写入时即使复位或者掉电也可以恢复到上一个正确的状态。
-
擦写均衡,有效延长flash的使用寿命。例如W25QXX系列的spi接口的flash,擦写次数大概在10万次,如果是操作flash比较频繁那么这10万次很快就会到达上限从而导致芯片废掉。
-
有限的RAM/ROM,相对于FATFS节省ROM和RAM空间
-
缺点:不兼容windows。
-
LFS_NO_MALLOC:决定使用动态内存还是静态内存,这个宏可以不定义
-
LFS_NO_ASSERT:决定是否使用断言
-
LFS_YES_TRACE:决定是否使用LFS函数调用跟踪,调试时候可以打开
-
LFS_NO_DEBUG:决定是否使用调试信息输出
-
LFS_NO_ERROR:决定是否使用错误信息输出
-
LFS_NO_WARN:决定是否使用警告信息输出
参考链接:https://blog.csdn.net/qq153471503/article/details/120303225
核心数据结构
// Configuration provided during initialization of the littlefs
struct lfs_config {
//用户提供的上下文,可用于将信息传递给块设备操作
void *context;
// 读块接口,用于从块内读取一个块数据
int (*read)(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, void *buffer, lfs_size_t size);
// 写块接口,用于将一段数据写入到块中,这个块必须是已经被擦除的
int (*prog)(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, const void *buffer, lfs_size_t size);
// 擦块接口,用于擦除一个块
int (*erase)(const struct lfs_config *c, lfs_block_t block);
// 有的块设备有缓存需要进行同步操作才能将缓存里的内容写出
int (*sync)(const struct lfs_config *c);
#ifdef LFS_THREADSAFE //多线程使用
// 加锁
int (*lock)(const struct lfs_config *c);
// 解锁
int (*unlock)(const struct lfs_config *c);
#endif
//读取的块的最小大小(以字节为单位)。所有读取操作都将是这个值的倍数。
lfs_size_t read_size;
// 每次写入的字节数,可以比物理写单元大以改善性能,这个数值决定了写缓存的大小,必须是read_size的整数倍,但值太大会带来更多的内存消耗.
lfs_size_t prog_size;
// 每个擦除块的字节数,可以比物理擦除单元大,但此数值应尽可能小因为每个文件至少会占用一个块。必须是prog_size的整数倍.
lfs_size_t block_size;
// 可以被擦除的块数量,这取决于块设备的容量及擦除块的大小
lfs_size_t block_count;
// littlefs清除元数据日志和移动之前的擦除周期数
//将元数据转移到另一个块。建议值在100-1000的范围内,较大的值以较低的成本获得更好的性能
//磨损分布不太一致。
//
//设置为-1可禁用块级磨损水平.
int32_t block_cycles;
// 块缓存大小.
lfs_size_t cache_size;
// 先行缓存大小.
lfs_size_t lookahead_size;
lfs_size_t compact_thresh;
// 可选参数,用于静态分配读缓存,这个缓存的大小应该等于read_size
void *read_buffer;
// 可选参数,用于静态分配写缓存,这个缓存的大小应该等于prog_size.
void *prog_buffer;
// 可选参数,用于静态分配预测缓存,这个缓存的大小应该等于预测深度lookahead/8,因为每个bit表示一个块
void *lookahead_buffer;
// 文件名长度(以字节为单位)的可选上限。没有缺点
//较大的名称,但信息结构体的大小由控制
//LFS_NAME_MAX定义。默认为存储在上的LFS_NAME_MAX或NAME_MAX
//磁盘为零。
lfs_size_t name_max;
lfs_size_t file_max;
lfs_size_t attr_max;
lfs_size_t metadata_max;
lfs_size_t inline_max;
#ifdef LFS_MULTIVERSION
uint32_t disk_version;
#endif
};
lfs_config结构体
结构体成员变量 | 说明 |
---|---|
void *context | 用于给底层块设备传参,比如:要写入的文件信息、位置、大小、是否有坏块等。该参数的数据结构由底层块设备驱动来定义。如果不需要传参的话可以不赋值 |
int (*read) | 块设备读取函数指针 |
int (*prog) | 块设备写入函数指针,写入前必须保证该写入块已经被擦除过 |
int (*erase) | 块设备擦除函数指针 |
int (*sync) | 块设备同步函数指针,若块设备不需要同步操作,可以直接返回成功 |
lfs_size_t read_size | 最小读取字节数,所有的读取操作字节数必须是它的整数倍 |
lfs_size_t prog_size | 最小写入字节数,所有的写入操作字节数必须是它的整数倍 |
lfs_size_t block_size | 擦除块操作的字节数,该选项不影响 RAM 消耗,可以比物理擦除尺寸大,但是每个文件至少占用一个块,必须是读取和写入操作字节数的整数倍 |
lfs_size_t block_count | 设备上可擦除块的数量,block_size x block_count = 块设备容量 |
int32_t block_cycles | littlefs 系统删除元数据日志并将元数据移动到另一个块之前的擦除周期数。建议取值范围为 100 ~ 1000,较大数值有较好的性能但是会导致磨损分布不一致,若取值 -1 的话,即为禁用块级磨损均衡 |
lfs_size_t cache_size | 块缓存大小,每个缓存都会在 RAM 中缓冲一部分块数据,littlefs 系统需要一个读取缓存、一个写入缓存,每个文件还需要一个额外的缓存。更大的缓存可以通过存储更多的数据并降低磁盘访问数量等手段来提高性能 |
lfs_size_t lookahead_size | 先行缓冲大小,更大的先行缓冲可以提高分配操作中可被发现的块数量。即分配块时每次分配多少个块,16就表示每次分配16个块。先行缓冲以 bit 位形式来存储,故 RAM 中的一个字节对应8个块。该值必须是8的整数倍 |
int (*lock) | 块设备加锁函数指针,需定义 LFS_THREADSAFE 宏才可用,若不需要多线程操作可不赋值 |
int (*unlock) | 块设备解锁函数指针,需定义 LFS_THREADSAFE 宏才可用,若不需要多线程操作可不赋值 |
void *read_buffer | 可选参数。读取静态缓冲区指针。若目标MCU不支持动态分配堆内存的话,就需要定义这个参数,本人建议静态分配时手动指定一个内存空间,大小是cache_size |
void *prog_buffer | 可选参数。写入静态缓冲区指针。若目标MCU不支持动态分配堆内存的话,就需要定义这个参数,本人建议静态分配时手动指定一个内存空间,大小是cache_size |
void *lookahead_buffer | 可选参数。先行分配静态缓冲区指针,需要32bit对齐。若目标MCU不支持动态分配堆内存的话,就需要定义这个参数,本人建议静态分配时手动指定一个内存空间,,大小是lookahead_size |
注意事项
-
在创建文件的时候,如果该文件已经创建过了,你再次创建同样的名字,没有加LFS_O_EXCL属性,文件同样会创建成功,但是不会返回文件已存在的错误码,创建的次数越多,后面文件系统内存会出现泄漏,最后导致写入文件都会报内存不足的问题,需要注意
-
内外文件系统需要自己注意函数的调用,不要混乱使用。
-
lfs_config配置表看自己的需要的大小适配,不过如果配置不对,很容易报错,推荐用的我的配置表。
-
挂载文件系统时候,如果没有实现read、erase和write的操作,会出现错误码:-84;
-
没有挂载成功,调用卸载函数会导致代码卡死。
-
在lfs_util.h中有两个函数lfs_malloc和lfs_free,虽然可以用宏定义定义LFS_NO_MALLOC不使用动态内存,但是在文件系统中打开文件时仍然调用了这个两个函数,在不使用动态内存时需要自己定义一个cache_size大小数组在lfs_malloc中返回这个数组,在不使用动态内存的情况下只能打开一个文件进行读写。
参考链接:https://blog.csdn.net/weixin_43908815/article/details/130179573
参考链接:https://blog.csdn.net/weixin_45270358/article/details/126306342
移植
1.下载文件
文件内容如下:
2.将这4个文件添加到工程中,并添加头文件路径
3.添加lfs_port.c文件和lfs_user_cfg.h文件
4.在lfs_port.c文件中添加flash操作接口函数
flash接口
这里使用了sfud开源的nor flash组件
#include <lfs.h>
#include <sfud.h>
/**
* @description: 块读取函数
* @param {lfs_config} *c 文件系统提供的上下文操作句柄
* @param {lfs_block_t} block 第几块,也就是块的编号
* @param {lfs_off_t} off 这一块的偏移
* @param {void} *buffer 读取数据存放的地址
* @param {lfs_size_t} size 读取数据的大小,以字节为单位
* @return {*}
*/
static int user_provided_block_device_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size)
{
sfud_err result = SFUD_SUCCESS;
const sfud_flash *flash = sfud_get_device_table() + 0;
result = sfud_read(flash, c->block_size * block + off, size, buffer);
return LFS_ERR_OK;
}
/**
* @description: 块写/编程函数--前提是已经擦除的位置
* @param {lfs_config} *c 文件系统提供的上下文操作句柄
* @param {lfs_block_t} block 第几块,也就是块的编号
* @param {lfs_off_t} off 这一块的偏移
* @param {void} *buffer 读取数据存放的地址
* @param {lfs_size_t} size 读取数据的大小,以字节为单位
* @return {*} 编程完成返回LFS_ERR_OK ;编程失败返回其他
*/
static int user_provided_block_device_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size)
{
sfud_err result = SFUD_SUCCESS;
const sfud_flash *flash = sfud_get_device_table() + 0;
result = sfud_write(flash, c->block_size * block + off, size, buffer);
return LFS_ERR_OK;
}
/**
* @description: 块擦除函数
* @param {lfs_config} *c 文件系统提供的上下文操作句柄
* @param {lfs_block_t} block 第几块,也就是块的编号
* @return {*}
*/
static int user_provided_block_device_erase(const struct lfs_config *c, lfs_block_t block)
{
sfud_err result = SFUD_SUCCESS;
const sfud_flash *flash = sfud_get_device_table() + 0;
result = sfud_erase(flash, c->block_size * block, c->block_size);
return LFS_ERR_OK;
}
/**
* @description: 数据同步函数,将缓存数据写入硬盘,这里没有缓存直接返回 LFS_ERR_OK
* @param {lfs_config} *c
* @return {*}
*/
static int user_provided_block_device_sync(const struct lfs_config *c)
{
return LFS_ERR_OK;
}
5.构造结构对象lfs_config
#define CHACE_SIZE 16
#define LOOKAHEAD_SIZE 16
static uint8_t _read_buff[CHACE_SIZE];
static uint8_t _write_buff[CHACE_SIZE];
static uint8_t _lookahead_buff[CHACE_SIZE];
// 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 = CHACE_SIZE, // 块的缓存大小
.lookahead_size = 16, // 现行缓存大小
.block_cycles = 500, // 这里是一个均衡磨损的参数
.read_buffer = _read_buff, // 读缓存
.prog_buffer = _write_buff, // 写缓存
.lookahead_buffer = _lookahead_buff, // 先行分配静态缓冲区
};
在lfs_user_cfg.h添加配置
/*
* @Author: car12
* @Date: 2024-07-01 17:14:23
* @LastEditors:
* @LastEditTime: 2024-07-25 23:38:01
* @Description: 请填写简介
*/
/*
* lfs utility functions
*
* Copyright (c) 2022, The littlefs authors.
* Copyright (c) 2017, Arm Limited. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef LFS_USER_CFG_H
#define LFS_USER_CFG_H
// 断言
#define LFS_NO_ASSERT 1
// 调试宏
#define LFS_NO_DEBUG 1
//线程安全 打开需要定义加锁,解锁函数
// #define LFS_THREADSAFE 1
// 没有动态分配内存
// #define LFS_NO_MALLOC 1
#endif
测试程序1
测试开机次数
void testlfs(void)
{
lfs_t lfs;
lfs_file_t file;
// mount the filesystem
int err = lfs_mount(&lfs, &cfg);
// reformat if we can't mount the filesystem
// this should only happen on the first boot
if (err)
{
lfs_format(&lfs, &cfg);
lfs_mount(&lfs, &cfg);
}
// read current count
uint32_t boot_count = 0;
lfs_file_open(&lfs, &file, "boot_count", LFS_O_RDWR | LFS_O_CREAT);
lfs_file_read(&lfs, &file, &boot_count, sizeof(boot_count));
// update boot count
boot_count += 1;
lfs_file_rewind(&lfs, &file);
lfs_file_write(&lfs, &file, &boot_count, sizeof(boot_count));
// remember the storage is not updated until the file is closed successfully
lfs_file_close(&lfs, &file);
// release any resources we were using
lfs_unmount(&lfs);
// print the boot count
printf("boot_count: %d\n", boot_count);
}
测试函数2
测试文件各种读写和文件夹操作
int test2(void)
{
lfs_t lfs;
lfs_file_t file;
lfs_dir_t dir;
struct lfs_info info;
int err;
err = lfs_mount(&lfs, &cfg); // 第一步要挂载文件系统
if (err < 0)
{
lfs_format(&lfs, &cfg);
lfs_mount(&lfs, &cfg);
}
// 以下操作都为假设操作成功
// 创建一个名为test的文件向文件中写入"1234"4个字节数据
lfs_file_open(&lfs, &file, "test", LFS_O_CREAT | LFS_O_RDWR);
lfs_file_write(&lfs, &file, "1234", 4);
lfs_file_close(&lfs, &file);
// 这时虽然打开文件时也使用了LFS_O_CREAT标志但是并不会创建一个新的文件也不会报错,在加入LFS_O_EXCL标志后才会报错
// LFS_O_RDONLY 标志表示以只读打开文件
// LFS_O_WRONLY 标志表示以只写打开文件
// LFS_O_RDWR 标志表示以可读可写打开文件,等价于 LFS_O_RDONLY | LFS_O_WRONLY
// LFS_O_CREAT 打开文件时如果文件不存在就创建新文件并打开,如果存在将读写指针定位到文件开头打开文件
// LFS_O_EXCL 打开文件时如果文件不存在就创建新文件并打开,如果存在就报错
// LFS_O_TRUNC 打开一个已有文件并将文件大小设置为0
// LFS_O_APPEND 打开一个已有文件并将文件的读写指针设置到文件最后
lfs_file_open(&lfs, &file, "test", LFS_O_CREAT | LFS_O_RDWR);
lfs_file_write(&lfs, &file, "abc", 3);
lfs_file_sync(&lfs, &file); // 这时会见内存中的缓存数据写入到Flash中,这时文件内容为"abc4"
// LFS_SEEK_SET 用绝对位置设置文件的读写指针(用相对用文件开头的位置设置读写指针)
// LFS_SEEK_CUR 用相对于当前的位置位置设置读写指针
// LFS_SEEK_END 用相对用文件末尾的位置设置读写指针
lfs_file_seek(&lfs, &file, 0, LFS_SEEK_SET); // 文件指针返回到文件开头
lfs_file_write(&lfs, &file, "1", 1);
lfs_file_sync(&lfs, &file); // 这时文件内容为"1bc4"
lfs_file_seek(&lfs, &file, 0, LFS_SEEK_END); // 文件指针设置到文件最后
lfs_file_write(&lfs, &file, "5", 1);
lfs_file_sync(&lfs, &file); // 这时文件内容为"1bc45"
// 文件指针设置到相对于当前位置-2,1bc45| --> 1bc|45
lfs_file_seek(&lfs, &file, -2, LFS_SEEK_CUR);
lfs_file_write(&lfs, &file, "d", 1);
lfs_file_sync(&lfs, &file); // 这时文件内容为"1bcd5"
lfs_file_close(&lfs, &file);
// 对test文件设置一个时间和一个日期的自定义属性,在删除文件时也会删除
#define FILE_TIME_TYPE 1
#define FILE_DATE_TYPE 2
lfs_setattr(&lfs, "test", FILE_TIME_TYPE, "12:00:00", 8);
lfs_setattr(&lfs, "test", FILE_DATE_TYPE, "2023-1-1", 8);
// 在根目录下创建了一个名为abc的目录
// 在abc目录下创建了一个名为test的文件,当前有两个test文件一个在根目录一个在abc目录中
lfs_mkdir(&lfs, "abc");
lfs_dir_open(&lfs, &dir, "abc");
lfs_file_open(&lfs, &file, "test",LFS_O_CREAT | LFS_O_RDWR);
lfs_file_close(&lfs, &file);
lfs_dir_close(&lfs, &dir);
// 遍历根目录下的内容,会递归遍历根目录下的目录里的内容
// 同时每个目录都会遍历到一个"."和一个".."的文件夹表示当前文件夹和返回上一个文件夹的路径
lfs_dir_open(&lfs, &dir, ".");
while (1)
{
err = lfs_dir_read(&lfs, &dir, &info);
if (err < 0)
break;
}
lfs_dir_close(&lfs, &dir);
lfs_unmount(&lfs);
}