第4章 文件系统基础:FatFS、LittleFS、SPIFFS的移植与配置
好,咱们进入第四章。前面聊了存储介质和分区,现在该让LVGL真正能从外部存储读图片了——这就要靠文件系统来管理那些图片文件。
说白了,文件系统就是给存储芯片搭个“书架”。没有它,你只能按扇区地址读写数据,跟记电话号码似的,累死人。有了文件系统,你才能用 /images/logo.bmp 这种路径去访问文件。
嵌入式领域常用的文件系统就三个:FatFS、LittleFS、SPIFFS。我这些年项目里都折腾过,各有各的脾气。下面挨个说。
4.1 三大文件系统对比
先给个总览表格,方便你快速选型:
| 特性 | FatFS | LittleFS | SPIFFS |
|---|---|---|---|
| 适用介质 | SD卡、NAND Flash、NOR Flash | NOR Flash、SPI Flash | SPI Flash、NOR Flash |
| 掉电安全 | 一般(需额外处理) | 优秀(内建CRC校验) | 良好(有磨损均衡) |
| RAM占用 | 约2-4KB(可配置) | 约4-8KB | 约1-3KB |
| 最大文件大小 | 4GB(FAT32) | 受Flash容量限制 | 受Flash容量限制 |
| 目录支持 | 完整支持 | 支持(但较简单) | 不支持(扁平结构) |
| PC兼容性 | 可直接插电脑读写 | 需专用工具 | 需专用工具 |
| 典型场景 | SD卡存储、数据记录 | OTA升级、配置文件 | 图片资源、小文件存储 |
我个人习惯是:如果板子上有SD卡槽,优先用FatFS,因为调试方便,拔下来插电脑就能看文件。如果是SPI Flash存图片资源,LittleFS或SPIFFS更合适——掉电不丢数据,这点在嵌入式设备里太重要了。
4.2 FatFS移植与配置
FatFS是ChaN老师写的,开源、稳定、文档全。移植它其实就三步:
4.2.1 底层接口实现
你需要提供6个底层函数,FatFS才能跟你的存储介质通信。以SPI Flash为例:
// 必须实现的底层接口
DSTATUS disk_initialize(BYTE pdrv); // 初始化
DSTATUS disk_status(BYTE pdrv); // 获取状态
DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count); // 读扇区
DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count); // 写扇区
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff); // 控制命令
DWORD get_fattime(void); // 获取当前时间
这里有个坑——disk_ioctl 里的 GET_SECTOR_COUNT 和 GET_SECTOR_SIZE 必须正确实现,否则FatFS不知道你的Flash有多大。我曾经在一个项目里忘了实现 GET_SECTOR_COUNT,结果格式化后只能读写前几个扇区,排查了一下午才发现。
4.2.2 配置裁剪
FatFS的 ffconf.h 里有几十个宏,但别怕,大部分保持默认就行。我一般只改这几个:
// ffconf.h 关键配置
#define FF_USE_MKFS 1 // 允许格式化
#define FF_USE_STRFUNC 1 // 支持长文件名
#define FF_USE_LFN 2 // 启用长文件名(堆分配)
#define FF_VOLUMES 1 // 卷数量
#define FF_MIN_SS 512 // 最小扇区大小
#define FF_MAX_SS 4096 // 最大扇区大小
嗯,这里要注意:如果你的Flash扇区大小不是512字节(比如W25Q64是4KB),一定要把 FF_MAX_SS 设成4096。否则读写会错位,数据全乱套。
4.2.3 挂载与使用
FATFS fs;
FRESULT res;
// 挂载文件系统
res = f_mount(&fs, "0:", 1);
if (res == FR_NO_FILESYSTEM) {
// 首次使用,需要格式化
res = f_mkfs("0:", NULL, 0);
if (res == FR_OK) {
res = f_mount(&fs, "0:", 1);
}
}
// 打开文件
FIL fil;
res = f_open(&fil, "0:/images/logo.bmp", FA_READ);
if (res == FR_OK) {
// 读取文件内容...
f_close(&fil);
}
disk_initialize 里加个打印,确认底层驱动是否正常。我习惯在初始化成功后打印Flash的JEDEC ID,一眼就能看出驱动对不对。
4.3 LittleFS移植与配置
LittleFS是ARM推出的,专门为Flash设计。它的核心优势是掉电安全和磨损均衡。如果你做的是电池供电的设备,或者经常意外断电,LittleFS比FatFS靠谱得多。
4.3.1 底层接口
LittleFS的接口比FatFS少,但更抽象:
// LittleFS 配置结构体
struct lfs_config {
// 底层读写函数
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);
// 硬件参数
lfs_size_t read_size; // 读粒度(通常1字节)
lfs_size_t prog_size; // 写粒度(通常256字节)
lfs_size_t block_size; // 擦除块大小(通常4096字节)
lfs_size_t block_count; // 总块数
lfs_size_t cache_size; // 缓存大小(建议等于prog_size)
lfs_size_t lookahead_size; // 预读大小(建议16或32)
int32_t block_cycles; // 磨损均衡周期(建议500-1000)
};
移植时最容易被忽略的是 read_size 和 prog_size。你想想看,如果你的Flash支持按字节读,但写必须按页(比如256字节),那 prog_size 就必须设成256。设小了,LittleFS会认为可以按更小粒度写,结果底层驱动报错。
4.3.2 配置示例
// W25Q64 的 LittleFS 配置
#define FLASH_BLOCK_SIZE 4096 // 4KB擦除块
#define FLASH_BLOCK_COUNT 2048 // 64MB / 4KB
#define FLASH_PAGE_SIZE 256 // 页大小
struct lfs_config cfg = {
.read = flash_read,
.prog = flash_prog,
.erase = flash_erase,
.sync = flash_sync,
.read_size = 1,
.prog_size = FLASH_PAGE_SIZE,
.block_size = FLASH_BLOCK_SIZE,
.block_count = FLASH_BLOCK_COUNT,
.cache_size = FLASH_PAGE_SIZE,
.lookahead_size = 32,
.block_cycles = 500,
};
4.3.3 挂载与使用
lfs_t lfs;
lfs_file_t file;
// 挂载
int err = lfs_mount(&lfs, &cfg);
if (err) {
// 首次使用,格式化
err = lfs_format(&lfs, &cfg);
if (err == 0) {
err = lfs_mount(&lfs, &cfg);
}
}
// 打开文件
err = lfs_file_open(&lfs, &file, "logo.bmp", LFS_O_RDONLY);
if (err == 0) {
// 读取文件...
lfs_file_close(&lfs, &file);
}
block_cycles 参数直接影响Flash寿命。设得太小(比如50),频繁写操作会很快耗尽擦除寿命;设得太大(比如5000),磨损均衡效果变差。我一般取500-1000,兼顾寿命和性能。
4.4 SPIFFS移植与配置
SPIFFS是最轻量的选择,专为SPI Flash设计。它不支持目录,所有文件都在根目录下。如果你的图片资源不多(几十个文件),SPIFFS完全够用。
4.4.1 底层接口
SPIFFS的接口更简单,只需要三个函数:
// SPIFFS 底层接口
s32_t spiffs_read(u32_t addr, u32_t size, u8_t *dst);
s32_t spiffs_write(u32_t addr, u32_t size, u8_t *src);
s32_t spiffs_erase(u32_t addr, u32_t size);
注意这里的地址是绝对地址,不是块号。SPIFFS内部会自己管理块映射。
4.4.2 配置示例
// SPIFFS 配置
#define SPIFFS_PHYS_ADDR 0x000000 // Flash起始地址
#define SPIFFS_PHYS_SIZE (4 * 1024 * 1024) // 4MB空间
#define SPIFFS_PHYS_PAGE 256 // 页大小
#define SPIFFS_PHYS_BLOCK 4096 // 块大小
spiffs fs;
spiffs_config cfg = {
.phys_size = SPIFFS_PHYS_SIZE,
.phys_addr = SPIFFS_PHYS_ADDR,
.phys_erase_block = SPIFFS_PHYS_BLOCK,
.log_page_size = SPIFFS_PHYS_PAGE,
.log_block_size = SPIFFS_PHYS_BLOCK,
.hal_read_f = spiffs_read,
.hal_write_f = spiffs_write,
.hal_erase_f = spiffs_erase,
};
4.4.3 挂载与使用
// 分配工作缓冲区
u8_t spiffs_work_buf[SPIFFS_PHYS_PAGE * 2];
u8_t spiffs_fds[32 * sizeof(spiffs_fd)];
u8_t spiffs_cache_buf[(SPIFFS_PHYS_PAGE + 32) * 4];
// 挂载
spiffs_mount(&fs, &cfg, spiffs_work_buf, spiffs_fds,
sizeof(spiffs_fds), spiffs_cache_buf,
sizeof(spiffs_cache_buf));
// 打开文件
spiffs_file fd = SPIFFS_open(&fs, "logo.bmp", SPIFFS_RDONLY, 0);
if (fd > 0) {
// 读取文件...
SPIFFS_close(&fs, fd);
}
spiffs_fds 数组要适当增大。我一般设成64,够用又不浪费RAM。
4.5 文件系统选择决策流程
说了这么多,到底该选哪个?我画了个决策图,帮你快速判断:
4.6 与LVGL文件系统接口对接
不管你选哪个文件系统,最后都要跟LVGL的 lv_fs 模块对接。LVGL提供了一个抽象层,你只需要注册几个回调函数:
// LVGL文件系统驱动注册
static void * fs_open(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode);
static lv_fs_res_t fs_close(lv_fs_drv_t * drv, void * file_p);
static lv_fs_res_t fs_read(lv_fs_drv_t * drv, void * file_p, void * buf, uint32_t btr, uint32_t * br);
static lv_fs_res_t fs_seek(lv_fs_drv_t * drv, void * file_p, uint32_t pos, lv_fs_whence_t whence);
static lv_fs_res_t fs_tell(lv_fs_drv_t * drv, void * file_p, uint32_t * pos_p);
void lv_fs_drv_init(void) {
lv_fs_drv_t drv;
lv_fs_drv_init(&drv);
drv.letter = 'F'; // 驱动器字母,LVGL用 "F:/images/logo.bmp"
drv.open_cb = fs_open;
drv.close_cb = fs_close;
drv.read_cb = fs_read;
drv.seek_cb = fs_seek;
drv.tell_cb = fs_tell;
lv_fs_drv_register(&drv);
}
这里有个细节:drv.letter 是驱动器字母,LVGL会用它来解析路径。比如你设成 'F',那LVGL里访问图片就要写成 "F:/images/logo.bmp"。我习惯用 'S' 表示SPI Flash,'C' 表示SD卡,这样一眼就能看出文件存在哪个介质上。
lv_fs_test() 函数验证一下。这个函数会遍历所有注册的驱动器,尝试打开、读写、关闭文件。如果这一步能过,后面LVGL加载图片基本不会出文件系统的问题。
4.7 避坑指南
最后总结几个我踩过的坑,你遇到了可以少走弯路:
- 扇区大小不匹配: FatFS默认扇区512字节,如果你的Flash扇区是4KB,一定要改
FF_MAX_SS。否则格式化后读写全是错的。 - 缓存不足导致死机: LittleFS的
cache_size设得太小,频繁读写时会触发断言失败。我建议至少设成prog_size的两倍。 - SPIFFS文件数限制: SPIFFS默认支持同时打开的文件数很少(通常4-8个)。如果你的LVGL界面复杂,同时加载多个图片,记得增大
SPIFFS_FD_SPACE。 - 路径分隔符: FatFS用
/或\都行,但LittleFS和SPIFFS只认/。统一用/最省心。