ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FatFS与LittleFS对比:嵌入式文件系统选型与掉电安全设计

FatFS与LittleFS对比:嵌入式文件系统选型与掉电安全设计 1. 两个文件系统摆在一起比什么搞嵌入式的只要做过SD卡存储、SPI Flash日志这类活基本绕不开FatFS和LittleFS这两个小型文件系统。很多朋友选型时习惯把两者放在一块儿比但说实话它俩虽然都叫“文件系统”出身、设计目标、适合干的活完全不是一路。把它俩硬拉到同一个擂台上问“哪个更好”本身就是个伪命题。真正该问的是我的项目里存储介质是什么断电可靠性要求多高数据要不要拿下来给PC直接读这几个问题回答完了选谁基本就定了。FatFS是ChaN老师写的老牌开源文件系统主打FAT12/16/32/exFAT跑在裸机或者RTOS上跟SD卡、U盘这类块设备是天作之合。它的最大底气是“兼容”你往SD卡里写个文件拔下来插电脑上Windows和Linux直接认。就冲这一点很多数据采集、音视频记录、车载黑匣子项目到现在还是认准它。LittleFS则是ARM牵头搞出来的嵌入式文件系统最初伴随Mbed OS推出来目标非常明确给NOR Flash这类“先擦后写、按块擦除”的介质用。它自带掉电保护、磨损均衡、坏块处理不是靠外部驱动兜底而是把可靠性做进了文件系统内部。用一句话概括FatFS更像是“移植到MCU上的PC文件系统规范”LittleFS则是“从Flash物理特性出发长出来的专用方案”。这篇文章我会把两者从设计机制、资源占用、移植实操、选型逻辑到掉电测试踩坑一条龙讲透适合正在做存储方案选型、或者已经在某一条路上踩过坑想换路线的朋友。看完你至少能回答一个问题手头这个项目拔电后丢数据能不能忍文件要不要拿出去给别的设备读这两个条件一确定答案就出来一大半了。2. FAT表 vs 写时复制两种截然不同的底层哲学2.1 FatFS靠“账本”记账快但怕撕账本FatFS底层对应的是FAT文件系统核心结构是FAT表我习惯把它叫“账本”。文件数据按簇分配FAT表记录每个簇的编号链谁连着谁链子断没断全看这张表。目录项则是另一个“目录账本”记录文件名、起始簇号、文件大小这些属性。写一个文件时FatFS至少要干三件事先写数据到空闲簇再去更新FAT表里对应的簇链最后更新目录项。问题在于这三步不是原子的。如果写数据写到一半断电数据簇可能已经占了但FAT表和目录项还没更新完轻则产生孤立簇重则整个文件链断掉目录项指向一个不存在的起始簇。这就是典型的“账本和仓库对不上”。在很多存储介质上这个问题不算致命因为坏账可以靠扫描、修复工具处理。但在嵌入式裸Flash上FAT表还有另一个更隐蔽的坑表的位置是固定的每写一个文件都要去刷FAT区域相当于你有一块橡皮天天就逮着同一个角使劲擦擦个几千次那个区域就先报废了。SD卡内部有FTL层Flash Translation Layer把逻辑扇区映射到物理块同时做了坏块管理所以FatFS在SD卡上能跑得很稳但这其实是SD卡替FatFS扛住了闪存管理的脏活累活。如果你直接把FatFS挪到一片裸SPI NOR上跑磨损集中和掉电损坏的风险就全暴露出来了。2.2 LittleFS不覆盖旧数据掉电最多“白写一次”LittleFS内部走的是日志结构log-structured路线核心操作是写时复制Copy-on-WriteCOW。说人话它从来不直接在旧数据的位置上原地修改而是把新内容写到一个空闲块里等写入完全成功、校验没问题再把文件系统的“指针”一次性切换过去。这就好比你在笔记本上改文章不拿橡皮擦掉旧字而是在下一页写一个新版本写完后把目录里的索引撕下来换成新页码。断电最坏的结果是新版本没写完索引还是指向旧版本文件系统整体还是上一版的完整状态。不会出现“新数据写了一半旧数据已经被覆盖没了”这种两头空的情况。为了进一步防元数据损坏LittleFS还搞了metadata pair元数据对。关键元数据不是存一份而是同时在两个块里各写一份并且带序列号和CRC校验。挂载时比较两份选有效且序号更新的那份。即使掉电正好落在提交元数据的刹那间也能靠另一份恢复不会读到半个目录项。同时对上层是透明的你不感觉它的存在但它确实把“掉电安全”这件事从“靠运气”变成了“靠设计”。2.3 磨损均衡和坏块管理一个靠文件系统一个靠底层驱动磨损均衡是我在帮人评估方案时必问的一条。NOR Flash的擦写寿命一般也就1万到10万次如果一个区域反复被写很快会先于其它区域报废。FatFS本身完全没有均衡概念它只把块设备当成一个可以随机读写的“磁盘”因为目标介质是SD卡/硬盘这类自带FTL的设备坏块和磨损是设备自己处理的。一旦换到裸Flash底层没有FTL磨损就集中在FAT表和数据频繁更新的固定区域。LittleFS则把动态磨损均衡做进了分配算法里在为新数据找块时会倾向于选择擦写次数较少的块把写入压力尽量摊到全盘。虽然它不是严格意义上的静止磨损均衡但用于MCU掉电存储这类场景已经能把寿命表现拉高一个量级。坏块处理也一样FatFS遇到擦除或写入失败基本只能报错返回而LittleFS会把这个块标记成bad block重新找一块继续写。这就是为什么很多产品把“参数存储、日志存储”这类重度写操作交给LittleFS而不是自己用FatFS硬扛。3. 资源占用与性能实测小文件系统的硬指标3.1 ROM、RAM占用对比选型时最常被问到的是“费不费资源”。FatFS的代码量受配置项影响非常大如果只开最核心的FAT16/32、不开长文件名、不做格式化ROM占用大概在10KB上下如果把exFAT、长文件名、多卷、各种高级选项全开二三十KB也很正常。RAM方面FatFS通常需要一个扇区大小的缓冲512B到4KB不等文件对象本身也有几十字节的开销多开几个文件句柄RAM就蹭蹭往上涨。LittleFS在这方面走的是“预算RAM封顶可控”的路线。官网给过数据代码区可以压到几KB运行RAM也常常在几百字节到几KB这个区间具体取决于cache_size和lookahead_size配置。关键是LittleFS的RAM开销是可以在配置阶段算死的不会因为文件数量增多、路径变深而动态上涨。对于RAM按字节省的MCU项目这个特性非常值钱。3.2 实测表现不同场景跑出来的速度完全不一样我在STM32F103平台上分别搭了FatFSSD卡SPI模式和LittleFSW25Q64做对比下面是我自己条件下的结果供参考不是绝对指标。项目FatFS SD卡SPI模式LittleFS W25Q64SPI模式顺序写大文件较快能跑到几百KB/s级别取决于Flash擦写和文件系统开销通常几十KB/s但可优化小块频繁改写受FAT表更新拖累且磨损集中主要开销是块迁移和元数据提交写放大存在但可靠掉电保护依赖上层日志/双备份方案文件系统内建断电后挂载不坏未完成写操作会被回滚挂载/格式化时间SD卡FAT挂载快格式化也快需要扫描元数据对容量越大挂载稍慢但通常可接受兼容性拔出即是一个标准U盘需要专用工具或FUSE驱动才能被PC读取如果你是“往SD卡里写摄像头视频、采集大块传感器数据”这类应用FatFS的顺序写吞吐确实占优因为它就是照着块设备的逻辑设计的PC标准协议栈对它没有任何额外负担。如果你的需求是“设备随时掉电、频繁改几个小配置文件、每5秒追加一条日志”LittleFS的可靠性优势远大于那点额外开销。很多项目真正需要的是“写参数别丢、别让Flash死太快”这时候顺序写速度反而没那么关键。3.3 碎片和空间碎屑两种文件系统都得面对碎片问题经常被忽略但实际用久了都会遇到。FatFS的FAT链结构天然允许文件离散存储簇号不连续也没关系但读大文件时要不停跳转性能会降。更麻烦的是频繁删改文件后FAT表里会产生大量零散的“空洞”空间明明够却很难找到一整段连续区域。LittleFS因为是COW机制旧版本的数据块被新版本替代后原来的块就变成垃圾块需要靠后台/下一次写入时回收。这个过程就是元数据压缩和块回收内部会自动做但如果你把空间用到99%文件系统会在写入时频繁触发回收速度急剧下降。所以不管用哪个预留10%到20%的空闲空间都是我在项目里强制的规矩这条比选哪个文件系统更能保住产品寿命。4. 移植与配置实操STM32SD卡和STM32SPI Flash4.1 FatFS在STM32SD卡上的标准流程FatFS这些年已经被移植烂了基本套路是固定的。你从官网下的包里有一层“diskio”抽象层要自己实现disk_initialize、disk_status、disk_read、disk_write、disk_ioctl这5个函数外加一个获取当前时间的get_fattime。SD卡驱动用SPI还是SDIO都可以只要保证底层能正确读写扇区、能获取卡容量和扇区大小即可。第一次使用建议按这个顺序走先单独测SD卡驱动确保能读扇区0得到MBR数据再调disk_ioctl的GET_SECTOR_COUNT、GET_SECTOR_SIZE、GET_BLOCK_SIZEFatFS要依赖这几个参数做容量判断和格式化最后再挂载卷f_mount成功后用f_getfree确认容量。很多新手一上来就f_open失败也不知道卡在驱动还是文件系统层排查起来很痛苦。ffconf.h里几个关键配置你要先想清楚FF_USE_LFN不开长文件名中文文件名基本就别想了开了会增加RAM开销。FF_MAX_SS建议直接设4096兼容512B和4KB扇区的卡但缓冲区会变大。FF_USE_MKFS需要格式化SD卡时打开量产工具里通常要自带的。FF_FS_RPATH开相对路径会方便些但RAM和代码量都会涨。如果追求兼容性还要记得一点4GB以上的SD卡默认出厂是FAT32PC上会显示为一个标准可移动磁盘但如果你的板子跑的是老版本FAT可能不识别exFAT大卡请确认驱动支持sd卡类型。4.2 LittleFS在W25Q64上的移植要点LittleFS只有4个核心接口read、prog、erase、sync比FatFS还简洁。W25Q64这类SPI NOR Flashblock_size一般按4096字节设prog_size按256字节设因为它是页编程模式一页最多写256字节写之前还得先擦除整个4KB扇区。下面是一个精简的设备层示例框架static int w25q_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { // 底层SPI读从 block * c-block_size off 地址读size字节 w25q_read_data(block * c-block_size off, buffer, size); return LFS_ERR_OK; } static int w25q_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { // 页编程注意W25Q一页256字节size不能超过页边界 w25q_page_program(block * c-block_size off, buffer, size); return LFS_ERR_OK; } static int w25q_erase(const struct lfs_config *c, lfs_block_t block) { // 扇区擦除W25Q64按4KB擦除 w25q_erase_sector(block * c-block_size); return LFS_ERR_OK; } static int w25q_sync(const struct lfs_config *c) { // 对NOR Flash而言写完了自然落盘没有cache同步问题返回OK即可 return LFS_ERR_OK; }LFS配置结构体也要仔细填。以下是我在W25Q64上的典型配置struct lfs_config cfg { .read_size 1, .prog_size 256, .block_size 4096, .block_count 2048, // 8MB / 4KB .cache_size 256, .lookahead_size 256, .read w25q_read, .prog w25q_prog, .erase w25q_erase, .sync w25q_sync, };block_count 8MB / 4KB 2048这一个值别填错填错容量直接不对。lookahead_size影响块分配算法扫描空闲块的范围太小会导致分配效率下降256足够了。挂载逻辑也要有一个标准姿势先试lfs_mount如果返回错误说明Flash上还没有有效文件系统这时要调用lfs_format然后再lfs_mount。很多项目第一次上电会卡在这里因为出厂Flash全是0xFF不格式化就挂载肯定失败。量产时如果不想让首启做格式化可以用镜像工具直接把文件系统镜像烧录进Flash但前提是文件系统版本匹配。4.3 真正影响可靠性的隐藏参数和注意事项LittleFS不是拿过来随便改个block_size就能跑的有几个隐藏细节很容易踩prog_size如果小于Flash的页写入长度比如W25Q是256B写小块数据时会很慢甚至底层驱动不支持部分页编程建议设成与Flash页大小一致。cache_size建议是prog_size的整数倍否则文件系统内部缓存读写会频繁边界对齐失败性能脑溢血。block_size必须与Flash的擦除块大小严格对应W25Q系列常用4KB也有16KB、64KB的变体一定要查对应型号的datasheet。掉电安全不代表“写一条日志就永远不会丢”如果你在文件里攒了一堆数据还没lfs_file_sync断电压根没到Flash上。需要每条都保证落地就每次写完调sync本质上是牺牲性能换可靠性。FatFS侧也有对应的“坑”掉电后文件大小可能停在旧值因为FAT表和目录项没刷盘。ff_sync不能省它对标的就是LittleFS的lfs_file_sync不调sync就拔电丢数据是大概率事件。很多“写完就拔电文件怎么是0KB”的反馈都是没在写完数据后做同步。5. 选型决策指南什么项目用什么基本不纠结5.1 按存储介质选SD卡闭眼FatFS裸Flash认真考虑LittleFS先说最直接的判断标准介质是什么。如果你用的是SD卡、TF卡、U盘这种自带控制器的块设备优先选FatFS几乎不用犹豫。原因很简单SD卡内部已经有FTL负责坏块管理和磨损均衡FatFS只负责记录文件结构两边分工明确。而且这种卡最大的价值是“可移动”文件要拿到电脑上处理那FAT兼容性就是你无论如何都不能丢的核心需求。如果你用的是板载SPI NOR Flash比如W25Q系列、内部Flash余量、或者NAND Flash情况就不一样了。这些裸Flash没有FTL擦写寿命有限坏块只能靠你自己管。FatFS在这里属于“能跑但不推荐长期跑”磨损和掉电风险都高LittleFS针对Flash特性设计自动均衡、坏块重映射、掉电回滚都是你需要的所以裸Flash场景我建议优先考虑LittleFS。5.2 按可靠性需求选能不能容忍“数据稀里糊涂没了”当设备用在工业、车载、电表、医疗这些场景断电是不可控事件而且往往断电时机最刁钻偏赶上写入那一刻。FatFS在这种情况下很难保证“文件系统不坏、文件不丢”你需要自己设计日志式写入、双备份区、上电自检修复这些机制工作量非常大。LittleFS至少把“文件系统层面不会坏”给你兜住了剩下的业务数据一致性只要应用层做简单的双缓存CRC校验就能达到相当高的可靠性。反过来如果掉电场景很少或者设备有稳定供电又或者你接受“断电丢最后1条数据”以换取更快写速FatFS完全可以胜任。很多数据记录仪确实就是这么干的定期sync一次即使掉电损失范围也就是上次sync之后那点数据业务可接受。5.3 一个项目里“两个文件系统共存”一点不冲突不少人会把FatFS和LittleFS看成二选一的对立选项实际上在一个MCU工程里同时编译两个文件系统很常见。比如一个产品有SD卡也有板载FlashSD卡上放音频、图片、录像走FatFS方便用户拷出来板载Flash放设备配置、运行日志、升级临时文件走LittleFS保证频繁写和意外掉电时不出问题。应用层可以自己做一层轻量封装类似一个微型VFSint fs_open(const char *path, int mode) { if (path和SD卡目录匹配) return fatfs_open_handler(path, mode); else return lfs_open_handler(path, mode); }在Linux里这叫虚拟文件系统层嵌入式裸机上当然没这么高大上但思路是一样的把设备类型和文件系统类型解耦。上层业务只调统一的open/read/write/sync底层根据路径前缀分发到FatFS或LittleFS。这么做的好处是后续换存储介质、换文件系统应用层代码几乎不用动调底层分发逻辑就行。很多RTOS SDK实际上是这么干的只是你没留意。5.4 常见选型误区根据我看到的咨询和踩坑案例大多数人选错不是技术不行而是被惯性思维带偏。三维误区反复出现第一个误区是“FatFS成熟所以更可靠”。FatFS确实稳定但那是在它适用的介质上稳定跑到裸NOR上频繁小文件写入会让FAT表区域先死为敬。成熟解决的是协议逻辑Bug不解决Flash物理磨损和掉电原子性。第二个误区是“LittleFS是给低端MCU用的性能不行”。LittleFS的目标是可靠性和资源可控不代表它撑不起吞吐需求。合理调大cache_size、用DMA、做页对齐写入之后顺序读写在SPI Flash上也能跑出不错的成绩。而且性能瓶颈往往在SPI时钟频率和擦除时间上不在文件系统本身。第三个误区是“LittleFS可以在PC上自由读取”。它默认只能被LittleFS自己的工具链读取PC上要么用FUSE驱动挂载镜像要么用官方提供的Python工具不像FAT那样拔下来插上就能读。如果这个需求是硬性的那就老老实实回FatFS。6. 踩坑实录掉电测试、坏块处理、性能排查6.1 掉电测试怎么做最容易踩什么坑很多人测试掉电就是“写文件然后按电源键”这种测试测不出问题因为正常断电时序里Flash基本已经写完。真正有价值的掉电测试要随机、要压着写入窗口、要反复循环。我这边实际做过的测试方法很简单MCU在循环里不断写一条带序号的结构化日志每次写完随机延时0到20ms外部用一个继电器或MOS管控制板卡电源在MCU工作的任意相位随机断电上电后读取日志检查文件系统能不能正常挂载已写入的记录有没有出现“尾记录损坏”有没有中间缺号循环做2000次以上才能说方案扛住了掉电。这个过程中最容易暴露的问题不是文件系统本身的Bug而是“你以为写完其实还在缓冲区里面”这种问题。所以测试脚本里要刻意加入“写完立即断电”的高危场景很多没有做sync的FatFS方案到这里就原形毕露了。对于LittleFS因为掉电安全内建通常挂载没问题但业务数据一致性仍然要靠应用层协议比如每条记录尾部放CRC读取时校验。把这个想清楚你就不会指望文件系统帮你解决一切。6.2 Flash坏块与坏道文件系统层面能屏蔽多少话题一聊到“屏蔽坏道”很多人会想成机械硬盘的坏道扫描。实际上在嵌入式Flash上这个问题分情况SD卡内部的FTL已经把坏块处理掉了表现为“透明修复”你在上层根本感知不到文件系统不会也不该看到坏块但对裸Flash如果没有坏块管理写入失败就会暴露成“文件写入返回错误”或文件系统异常。LittleFS对待坏块的策略是擦除或写入某个块失败时把它标记为bad block重新选择一个块继续操作。这样坏块从逻辑上被“屏蔽”了不需要你在应用层感知。但你别指望它处理所有问题比如Flash出现大量坏块、寿命到头、或者因为设计不当导致同区域被写到“上班”文件系统能做的只是尽量隔离救不回寿命。NOR Flash出厂通常没有大量坏块但是擦写次数逼近极限时坏块会增多。项目里最好在固件里做一个Flash健康状态统计记录每个块的擦写次数分布发现异常提前报警。LittleFS不直接提供这种接口需要你在底层prog/erase函数里做计数成本不高但对售后预判帮助非常大。6.3 常见问题速查表现象可能原因排查方向SD卡挂载失败SPI时序不稳、卡供电不足、卡类型不兼容先读扇区0确认MBR能读到换卡交叉测试写SD卡后拔电文件为0KB写完没有ff_syncFAT表和目录项未落盘写完文件务必调用ff_sync再断电LittleFS第一次上电挂载失败Flash上没有格式化过的文件系统先lfs_format再lfs_mount或出厂烧写镜像LittleFS写入速度越来越慢空间利用率过高、触发频繁块回收预留10%-20%空闲空间检查lookahead_sizeFlash某区域先报废没有磨损均衡写入集中在固定区域换LittleFS或底层自己做地址映射和均衡空间满了但f_getfree显示异常FAT碎片、孤立簇未回收格式化重建文件系统应用层做定期维护掉电后LittleFS挂载成功但文件内容异常文件系统没坏但业务数据不完整应用层加CRC、双份记录、事务标志看到表格里这些场景你可能会发现真正把项目搞崩的往往不是“文件系统选错了”而是“没有认真做掉电设计、没有留够空间、没有做同步”。文件系统只能保证它职责范围内的东西职责之外的还是得靠你的整体软件架构来兜底。最后再分享一个我自己的习惯不管用FatFS还是LittleFS在项目定型前我都会先做一个压力测试板跑一遍掉电测试、高低温写入测试、空间写满再删再写循环测试。这听起来费时间但存储类问题大多是概率性的测试样本不够问题就永远躺在产线上等着爆发。文件系统选型只是第一步把测试流程固化下来才是真正让产品“稳”的关键。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进