尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

LittleFS 嵌入式文件系统机制拆解与某 RISC-V MCU 实测配置

LittleFS 嵌入式文件系统机制拆解与某 RISC-V MCU 实测配置 做嵌入式的同行大概都遇到过这种返修客户反馈设备用着用着配置丢了或者 OTA 升级中途断电后板子再也起不来。查下来根因十有八九落在 Flash 文件系统上——要么掉电写坏了 FAT 表要么某个扇区被写穿。FAT 不抗掉电裸写 Flash 不耐磨这俩坑在量产面前迟早要还。这篇文章拆解 LittleFS 怎么从根上解决这俩问题并拿某款 RISC-V MCU 的 SDK 实测配置做案例讲清楚每个配置项为什么这么填。一、为什么嵌入式需要专门的文件系统SPI NOR Flash 有三条铁律按页读写、按块擦除、写前必擦。页一般是 256 字节擦除块是 4096 字节而且擦写有寿命一块 Flash 擦写十万次左右就开始出坏块。FAT 在 PC 上没问题搬到嵌入式有三个硬伤。最致命的是掉电就损坏FAT 表和目录项是原地改写的写一半断电FAT 表和目录对不上整个分区就废了。再就是没有磨损均衡同一个目录反复改永远写那几个扇区很快就写穿。还有 RAM 占用随文件数增长小 MCU 扛不住。LittleFS 就是冲着这三点设计的掉电安全、动态磨损均衡、RAM/ROM 有界。它的状态严格受控RAM 消耗不随文件系统变大而涨也没有无界递归。二、LittleFS 的四个核心机制2.1 CTZ 压缩目录大目录不会写穿一个块CTZ 是 count-leading-zeros 的缩写听起来玄乎本质是一种可变长跳转的链表编码。LittleFS 的目录不是像 FAT 那样占一段连续扇区的目录表而是把每个目录条目组织成链表每个条目里存一个skip 值指向后面第几个块。关键在于这个 skip 值用 CTZ 编码后是变长的前面几个条目 skip 小、占的位少越往后 skip 越大、占的位多。结果是目录条目天然分散到多个块上不会全挤在一个块里。为什么要这样设计因为目录是写最频繁的元数据。FAT 的目录表集中在几个连续扇区你往一个目录里反复增删文件那几个扇区很快就被写穿了。CTZ 把目录摊开到多个块配合下面的磨损均衡目录块的磨损被均摊开。2.2 磨损均衡block_cycles 让元数据块轮换着用LittleFS 的写都是 copy-on-write要改一个块不是原地改而是写一个新块再把指针指过去。这本身就是磨损均衡的基础写不固定落在某个块。但光靠 copy-on-write 还不够。超级块和目录块是热点每次挂载要读超级块每次改目录要写目录块它们的擦写频率远高于数据块。如果任由它们待在原地迟早被写穿。所以 LittleFS 加了block_cycles机制一个元数据块每被擦写 N 次后就把它整个搬到另一个新块上旧块释放。本工程配的是 500意思是元数据块擦写 500 次就换一个家。官方对block_cycles的说法很直白建议 100-1000值大性能好但磨损分布不均匀值小磨损均匀但搬动开销大。500 是个偏中庸的选择。设成 -1 则关闭块级磨损均衡除非你确信自己的负载不会产生热点元数据块否则别关。延迟擦除是另一个细节旧块被新块取代后不立刻擦先标记失效等空闲了再擦避免擦除阻塞关键路径。2.3 掉电保护三步 copy-on-write 保证任意时刻可回退这是 LittleFS 最值钱的设计。一次写操作拆成三步写新块把新数据写到空闲块更新指针原子地把元数据指针从旧块指到新块释放旧块旧块标记为空闲任意时刻掉电文件系统都能回到上一个一致状态 - 掉在第 1 步新块写了一半指针还指着旧块旧数据完好新块那半截下次 GC 回收 - 掉在第 2 步指针要么没更新回退到旧块要么更新了用新块二选一不会乱 - 掉在第 3 步指针已经指新块了旧块没释放只是多了个垃圾块不影响正确性对比 FATFAT 表是原地改写的掉电时 FAT 表和目录可能各改了一半对不上就损坏。这是 FAT 在嵌入式不抗掉电的根因。源文档里有个实测很能说明问题往文件写两段数据但不 close直接拔电重新上电后读到的还是上一次成功 close 的内容不会读到半截损坏数据。LittleFS 保证回到上一个一致状态。2.4 元数据双副本单块损坏不影响挂载关键元数据挂载点、根目录写两份交替更新。这次写副本 A下次写副本 B再下次又写 A。两份里只要有一份是好的就能挂载。为什么需要双副本元数据是单点超级块坏了整个文件系统都认不出来。双副本把单点故障概率降一个数量级配合 CRC 校验一个副本损坏能被检测到并切到另一个副本。三、lfs_config 每个字段都在说什么这是新手最容易踩坑的地方。lfs_config不是随便填的每个字段都要和底层 SPI Flash 的物理参数对齐填错轻则性能差重则挂载失败、数据损坏。某 RISC-V MCU SDK 的实测配置适配层ak_lfs化名里长这样static struct lfs_config cfg { .read block_device_read, // 读XIP memcpy .prog block_device_prog, // 写spi_flash_write .erase block_device_erase, // 擦spi_flash_sector_erase .read_size 256, // Flash 页大小 .prog_size 256, // Flash 页大小 .block_size 4096, // Flash 扇区大小 .block_count 25, // 挂载时按分区大小重算 .cache_size 256, // RAM 缓存 .lookahead_size 16, // 块分配位图前瞻 .block_cycles 500, // 元数据磨损均衡周期 };逐个说。read_size / prog_size 256读和写的最小粒度必须等于 Flash 的页大小。SPI NOR Flash 一页 256 字节跨页读要发多条命令所以填页大小最经济。填小于页没意义底层还是按页读填大于页会要求底层做超出物理能力的对齐。block_size 4096擦除块大小等于 Flash 的扇区大小。官方注释强调这个值不影响 RAM 消耗但非内联文件至少占一整个块所以 block_size 越大小文件越浪费空间。它必须是 read_size 和 prog_size 的倍数。block_count文件系统的块数。这里填 25 只是占位挂载时适配层会按分区实际大小重算block_count 分区字节数 / 4096。填 0 也行LittleFS 会读磁盘上存的 block_count。cache_size 256RAM 里的块缓存大小。LittleFS 需要一个读缓存、一个写缓存外加每个打开的文件一个缓存。必须是 read_size/prog_size 的倍数且是 block_size 的因数。本工程填 256一页省 RAM想提速可以填大比如 4096一整块缓存命中率高但多吃 4KB RAM。lookahead_size 16块分配的前瞻缓冲。它是个紧凑位图1 字节能跟踪 8 个块。16 字节就能前瞻 128 个块。分配新块时一次扫一批比一块一块找快。值大找块快但多吃 RAM。block_cycles 500前面讲过的元数据块轮换周期。100-1000 之间取舍。记住一条这三个参数4096 / 256 / 256必须和生成文件系统镜像时的参数完全一致否则挂载失败。源文档里生成空镜像用的命令是mklfs -b 4096 -p 256 -r 256和配置里的 block_size/prog_size/read_size 一一对应。四、分区挂载LittleFS 不懂分区适配层来填这是个容易混淆的点LittleFS 本身没有分区概念。它只认一个从第 0 块到第 N 块的块设备不知道也不关心这些块在 Flash 上的物理位置。但实际产品里一块 SPI Flash 会被切成多个分区ROMFS 放只读资源、LittleFS 放可读写数据、还有 OTA 分区。LittleFS 只该管自己那段。把分区名映射到 Flash 物理位置是适配层的活。某 SDK 的适配层ak_lfs化名干的就是这件事挂载流程是ak_lfs_mount(UDATA) → partition_get_bin_info(binInfo) // 按UDATA查 flash 头部分区表 → lfs_info.start_page binInfo.file_start_page // 分区起始页 → cfg.block_count binInfo.file_len / 4096 // 按分区大小算块数 → lfs_mount(lfs, cfg) // 挂载分区表存在 Flash 头部第 0 页附近由烧录工具在烧录时写入。适配层按分区名字符串匹配返回该分区的起始页、长度、XIP 映射地址。读写路径还分两条读走 XIPeXecute In Place把 Flash 物理地址映射到 CPU 能直接 memcpy 的地址读不经过 SPI 命令开销写和擦走 spi_flash_write / spi_flash_sector_erase按页写、按扇区擦。读快写慢这跟 Flash 物理特性一致。为什么必须要有这层适配因为 LittleFS 的块设备接口是抽象的它说读第 K 块但第 K 块在 Flash 上的真实地址是分区起始页 K*4096这个换算只有适配层知道。没有适配层LittleFS 会从 Flash 第 0 块开始用直接踩到分区表和 ROMFS 上去。五、和 EasyFlash 比什么时候选谁先说清楚一个前提本 SDK 并没有集成 EasyFlash全仓搜不到任何 easyflash 源码。下面的对比是基于 EasyFlash 公开机制armink/EasyFlash v4.x的理论推演用来理解选型逻辑不是实测对比。两者定位完全不同。LittleFS 是文件系统有目录、文件、路径数据模型是path → bytesEasyFlash 是 KV 存储库核心是ef_env的key → value外加日志和 IAP数据模型是扁平的键值对。场景LittleFSEasyFlash存大量结构化数据文件、音频、OTA 包强弱KV 不适合大块二进制存少量配置键值对几十个参数能用但重强ef_env_set/get一行搞定需要目录/路径组织强无代码体积较大编译后约 20KB较小核心 env 几 KB本工程为什么选 LittleFS因为 OTA、音频、指纹这些要存的是文件用文件 APIopen/read/write/close语义统一上层代码能复用。而且锁类设备掉电频繁LittleFS 的 copy-on-write 保护任意文件比 EasyFlash 的 KV 备份更通用。本工程的配置参数走自有存储没用到 EasyFlash 的 env 模型引入 EasyFlash 反而多一层抽象。说到底就一条要存文件选 LittleFS要存几十个配置参数且追求极小体积选 EasyFlash 的 ef_env。六、实测一条 CLI 命令跑通 LittleFS源文档里在ez_ble_test.c化名新增了一条ez_lfsCLI 测试命令设计很简洁单命令形式为ez_lfs加操作符和参数用 static 状态机管理当前文件句柄和挂载标志。参考了 SDK 里已有的多命令测试但改成单命令形式省去注册一堆命令。基本读写测试跑下来是这样msh ez_lfs mount lfs mount success msh ez_lfs open test.txt msh ez_lfs write hello_lfs_123 lfs write success, len:13, pos:13 msh ez_lfs seek 0 0 msh ez_lfs read 32 lfs read success, len:13, data:hello_lfs_123 msh ez_lfs close掉电保护测试更有意思mount 后 open 一个文件写两段数据但不 close直接拔板子电源模拟写中断。重新上电 mount 后读读到的是上一次成功 close 的内容或空不会读到半截损坏数据。这正是 2.3 节讲的三步 copy-on-write 在起作用。实测踩了三个坑值得说。第一个是 UDATA 分区要单独烧默认烧录配置不含它mount 会因找不到分区失败得先用另一套烧录配置把 UDATA 分区和 lfs.bin 烧进去。第二个是写后必须 closelittlefs 的写在 close 或 sync 前不落盘写完直接断电数据不保证持久化掉电测试观察的正是这点。第三个是单 fd 设计命令只维护一个当前句柄open 新文件会先关旧文件多文件并发要自己扩展成句柄表。构建验证也印证了机制未加ez_lfs命令前因为开了--gc-sections且无人引用littlefs 库整个被链接器丢弃elf 里 0 个 lfs 符号加了命令把符号拉回后87 个 littlefs v2.50 符号全部链接进来bin 大了约 20KB即 lfs 库体积。写在最后选文件系统这事本质是在掉电安全、磨损均衡、资源占用之间取舍。LittleFS 用 copy-on-write 双副本 CTZ 目录把前两个做到了嵌入式能用的程度代价是 20KB 左右的代码体积和一点 RAM 缓存。对要存文件、会掉电的产品这笔账划算。你的产品上用什么文件系统掉电保护实测过吗还是只在实验室跑通就上了评论区聊聊有用的话点个在看让更多踩过 Flash 损坏坑的同行看到。嵌入式 #文件系统 #LittleFS #Flash存储 #单片机
返回列表