LittleFS简单移植
简介
LittleFS是ARM mbed使用的文件系统,具有轻量级、掉电安全、磨损平衡等特性,非常适用于Flash这种块存储设备,读取性能优秀,因为其做了多项写入保护措施故写入性能一般
我将其移植到STM32F1系列MCU上,使用W25Q32 SPI Flash芯片作为底层块存储设备。
移植说明
LittleFS的官方文档可以去Github上查看,说是文档,其实只有3篇说明文,包括1篇Readme,2篇关于设计和实现细节的介绍。
对移植有用的也就只有Readme里很少的篇幅还有就是源码里的注释。
讲的不是很清楚,故我在移植过程中阅读了其自带的例程并且参考了CSDN上其他人写的移植文章,其链接附在文末。
移植步骤
1、下载源码
首先需要去Github上下载最新源码,我使用的是2.4版本,littleFS源码使用了C99标准,故使用的编译器需要配置成支持C99,我使用的是MDK5。
源码下载下来后,只需要其中的4个文件:lfs.c、lfs.h、lfs_util.c、lfs_util.h,其他文件都是用于测试的Demo例程,只关心如何移植的话可以忽略。
移植的重点是如何填充 lfs_config
结构体内的成员变量和实现对块设备的读写接口。
我新建了一个文件 lfs_port.h 用来存放移植需要用到的所有代码,即:初始化 lfs_config
结构体函数和块设备读写接口函数。
2、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不支持动态分配堆内存的话,就需要定义这个参数,用于在栈内分配静态变量void *prog_buffer
:可选参数。写入静态缓冲区指针。若目标MCU不支持动态分配堆内存的话,就需要定义这个参数,用于在栈内分配静态变量void *lookahead_buffer
:可选参数。先行分配静态缓冲区指针,需要32bit对齐。若目标MCU不支持动态分配堆内存的话,就需要定义这个参数,用于在栈内分配静态变量
底层块设备读写函数
可以直接查看下面所附的代码注释
在使用 LittleFS 文件系统的地方还需要定义3个变量:
lfs_t lfs; // lfs 文件系统对象
lfs_file_t file; // lfs 文件对象
struct lfs_config cfg; // lfs 文件系统配置结构体
动态分配和静态变量缓冲区
对于目标MCU,有可能不支持动态分配堆内存,LittleFS 提供了一个宏用于配置是否使用动态分配:LFS_NO_MALLOC
对于支持动态分配堆内存的MCU,不需要定义 LFS_NO_MALLOC
宏,相关缓存都是用 malloc
来动态分配的。
对于不支持动态分配堆内存的MCU,需要定义 LFS_NO_MALLOC
宏,相关缓存需要指定静态变量,也即上面指出的 read_buffer
、prog_buffer
、lookahead_buffer
。且每次只能打开一个文件,若要打开多个文件必须使用动态分配。
若动态分配堆内存不是使用c标准库里的 malloc
、free
函数而是自己实现的话(我使用的是MDK自带的 MicroLIB
),则需要自己封装相关接口,见 lfs_util.h 中的相关代码:
// Allocate memory, only used if buffers are not provided to littlefs
// Note, memory must be 64-bit aligned
static inline void *lfs_malloc(size_t size) {
#ifndef LFS_NO_MALLOC
return malloc(size);
#else
(void)size;
return NULL;
#endif
}
// Deallocate memory, only used if buffers are not provided to littlefs
static inline void lfs_free(void *p) {
#ifndef LFS_NO_MALLOC
free(p);
#else
(void)p;
#endif
}
具体代码
lfs_port.h
#ifndef __LFS_PORT_H
#define __LFS_PORT_H
#include "bsp_spi_flash.h"
#include "lfs.h"
#define W25Q32_SECTOR_SIZE 4096
#define W25Q32_SECTOR_NUM 1024 // 总大小: 4096 * 1024 = 4M byte
int lfs_spi_flash_init(struct lfs_config *cfg);
int lfs_spi_flash_read(const struct lfs_config *cfg, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size);
int lfs_spi_flash_prog(const struct lfs_config *cfg, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size);
int lfs_spi_flash_erase(const struct lfs_config *cfg, lfs_block_t block);
int lfs_spi_flash_sync(const struct lfs_config *cfg);
int lfs_spi_flash_init(struct lfs_config *cfg) {
SPI_FLASH_Init();
SPI_FLASH_ModeBitReset();
if (W25Q32_JEDEC_ID == SPI_FLASH_ReadID()) {
cfg->read = lfs_spi_flash_read;
cfg->prog = lfs_spi_flash_prog;
cfg->erase = lfs_spi_flash_erase;
cfg->sync = lfs_spi_flash_sync;
// 最小读取字节数,所有的读取操作字节数必须是它的整数倍
cfg->read_size = 16;
// 最小写入字节数,所有的写入操作字节数必须是它的整数倍
cfg->prog_size = 16;
// 擦除块操作的字节数,该选项不影响 RAM 消耗,可以比物理擦除尺寸大
// 但是每个文件至少占用一个块,必须是读取和写入操作字节数的整数倍
cfg->block_size = W25Q32_SECTOR_SIZE;
// 设备上可擦除块的数量,即容量
cfg->block_count = W25Q32_SECTOR_NUM;
// littlefs 系统删除元数据日志并将元数据移动到另一个块之前的擦除周期数。
// 建议取值范围为 100 ~ 1000,较大数值有较好的性能但是会导致磨损分布不一致
// 取值 -1 的话,即为禁用块级磨损均衡
cfg->block_cycles = 500;
// 块缓存大小,每个缓存都会在 RAM 中缓冲一部分块数据,
// littlefs 系统需要一个读取缓存、一个写入缓存,每个文件还需要一个额外的缓存。
// 更大的缓存可以通过存储更多的数据并降低磁盘访问数量等手段来提高性能
cfg->cache_size = 16;
// 先行缓冲大小,更大的先行缓冲可以提高分配操作中可被发现的块数量
// 即分配块时每次步进多少个块,16就表示每次分配16个块
// 先行缓冲以紧凑的bit位形式来存储,故 RAM 中的一个字节可以对应8个块
// 该值必须是8的整数倍
cfg->lookahead_size = 16;
return LFS_ERR_OK;
} else {
return LFS_ERR_IO;
}
}
/*
* @brief 从指定块内的某区域读数据
* @param [in] lfs_config格式参数
* @param [in] block 逻辑块索引号,从0开始
* @param [in] off 块内偏移,该值需能被read_size整除
* @param [out] 读出数据的输出缓冲区
* @param [in] size 要读取的字节数,该值需能被read_size整除,lfs在读取时会确保不会跨块;
* @retval 0 成功, < 0 错误码
*/
int lfs_spi_flash_read(const struct lfs_config *cfg, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) {
// check if read is valid
LFS_ASSERT(off % cfg->read_size == 0);
LFS_ASSERT(size % cfg->read_size == 0);
LFS_ASSERT(block < cfg->block_count);
SPI_FLASH_ReadData(block * cfg->block_size + off, (uint8_t *)buffer, size);
return LFS_ERR_OK;
}
/*
* @brief 将数据写入指定块内的某区域。该区域必须已经先被擦除过,可以返回 LFS_ERR_CORRUPT 表示该块已损坏
* @param [in] lfs_config格式参数
* @param [in] block 逻辑块索引号,从0开始
* @param [in] off 块内偏移,该值需能被rprog_size整除
* @param [in] 写入数据的缓冲区
* @param [in] size 要写入的字节数,该值需能被read_size整除,lfs在读取时会确保不会跨块;
* @retval 0 成功, < 0 错误码
*/
int lfs_spi_flash_prog(const struct lfs_config *cfg, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) {
// check if write is valid
LFS_ASSERT(off % cfg->prog_size == 0);
LFS_ASSERT(size % cfg->prog_size == 0);
LFS_ASSERT(block < cfg->block_count);
SPI_FLASH_WriteEnable();
SPI_FLASH_Program(block * cfg->block_size + off, (uint8_t *)buffer, size);
return LFS_ERR_OK;
}
/*
* @brief 擦除指定块。块在写入之前必须先被擦除过,被擦除块的状态是未定义
* @param [in] lfs_config格式参数
* @param [in] block 要擦除的逻辑块索引号,从0开始
* @retval 0 成功, < 0 错误码
*/
int lfs_spi_flash_erase(const struct lfs_config *cfg, lfs_block_t block) {
// check if erase is valid
LFS_ASSERT(block < cfg->block_count);
SPI_FLASH_WriteEnable();
SPI_FLASH_SectorErase(block * cfg->block_size);
return LFS_ERR_OK;
}
/*
* @brief 对底层块设备做同步操作。若底层块设备不没有同步这项操作可以直接返回
* @param [in] lfs_config格式参数;
* @retval 0 成功, < 0 错误码
*/
int lfs_spi_flash_sync(const struct lfs_config *cfg) {
return LFS_ERR_OK;
}
#endif // __LFS_PORT_H
main.c
#include <stdio.h>
#include "stm32f10x.h"
#include "bsp_spi_flash.h"
#include "bsp_systick.h"
#include "bsp_uart.h"
#include "lfs.h"
#include "lfs_port.h"
void TestLFS(const struct lfs_config *cfg, 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);
}
int main(void) {
uint8_t bCanTest = 0;
lfs_t lfs; // lfs 文件系统对象
lfs_file_t file; // lfs 文件对象
struct lfs_config cfg; // lfs 文件系统配置结构体
USART_Config();
printf("\nUSART parameter: %d 8-N-1\n", DEBUG_USART_BAUDRATE);
int err = lfs_spi_flash_init(&cfg);
if (err) {
printf("LFS init failed!");
} else {
bCanTest = 1;
}
while (1) {
if (bCanTest) {
TestLFS(&cfg, &lfs, &file);
}
SysTick_Delay_ms(2000);
}
}