news 2026/9/28 2:03:48

littlefs 在 NOR 与 NAND Flash 上的适配实战与性能调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
littlefs 在 NOR 与 NAND Flash 上的适配实战与性能调优

1. 为什么要在 NOR 与 NAND 上折腾 littlefs

第一次把 littlefs 移植到一块 SPI NOR 上跑通的时候,我以为这事儿就算完了。结果换到一块 SPI NAND 上,同样的代码,挂载直接失败,日志里全是LFS_ERR_CORRUPT。那一刻我才意识到,littlefs 虽然号称"掉电安全、磨损均衡、小体积",但它对底层块设备的假设,在 NOR 和 NAND 这两种介质上完全是两码事。

littlefs 是 ARM 官方开源的一个面向微控制器的嵌入式文件系统,核心卖点有三个:掉电恢复能力、动态磨损均衡、极小的 RAM/ROM 占用。它不像 FAT 那样依赖复杂的 FAT 表,也不像 SPIFFS 那样在大文件场景下性能塌方。它用了一套"元数据对"(metadata pair)加"写时复制"(copy-on-write)的机制,保证任何一次写入被打断,文件系统都能回滚到上一个一致状态。

但问题也恰恰出在这里。littlefs 的这套机制,是建立在对底层存储介质"可擦除块大小""读写粒度""坏块行为"的假设之上的。NOR Flash 和 NAND Flash 在这几个维度上差异巨大:

  • NOR 支持字节级随机读,擦除块通常 4KB,写入前必须是 0xFF,几乎没有坏块;
  • NAND 必须按页读写(常见 2KB 页 + 64B spare),擦除块 128KB 起步,出厂就可能有坏块,而且需要 ECC 校验。

所以"适配"这个词,在 littlefs 语境下不是改改配置那么简单,它涉及到块设备驱动层的抽象、擦除块几何参数的匹配、坏块管理策略、以及 cache 与 lookahead 缓冲的调优。这篇文章我会把我在 NOR 和 NAND 上踩过的坑、调过的参数、写过的适配层代码,尽量完整地摊开讲。适合正在做嵌入式存储选型、或者已经把 littlefs 跑起来但性能不理想的同行参考。

2. littlefs 的存储抽象模型与介质假设

2.1 块设备接口到底要求了什么

littlefs 通过一个lfs_config结构体和底层打交道,核心是四个回调:read、prog、erase、sync。看起来很简单,但每个回调背后都有隐含约束。

read的签名是(block, off, buffer, size),其中block是块号,off是块内偏移。注意,littlefs 内部把整个存储划分成若干个"块",这个"块"的大小由block_size配置决定,它必须等于底层介质的擦除块大小。这是第一个大坑:很多人把block_size设成 512 或者 4096,但底层 NOR 的扇区是 4KB、NAND 的擦除块是 128KB,结果就是擦除操作越界或者对齐错误。

prog回调要求写入的数据区域必须是已经擦除过的(全 0xFF)。littlefs 自己会保证这一点,但前提是你的block_size和block_count配置正确,否则它算出来的块边界和实际介质对不上。

erase回调接收的是块号,要求把整个块擦成 0xFF。对于 NOR,这通常是一条0x20扇区擦除指令;对于 NAND,是一条0xD8块擦除指令,而且擦除后还要检查坏块标记。

sync回调是可选的,用于在元数据提交后强制刷写缓存。在 NOR 上通常不需要,因为 NOR 写入就是直接落到介质;但在 NAND 上,如果你的驱动有写缓存,必须在这里 flush。

2.2 NOR 与 NAND 的几何差异对照

我把两种介质的关键参数整理成一张表,这张表决定了后面所有适配策略的走向:

特性NOR FlashNAND Flash
读取粒度字节级随机读页级读(2KB/4KB)
写入粒度字节/字页级写
擦除块大小4KB / 32KB / 64KB128KB / 256KB
坏块极少出厂即有,需管理
ECC通常不需要必须
擦写寿命约 10 万次约 1 万次(SLC)
典型容量1MB ~ 16MB128MB ~ 4GB
单位成本高低

这张表里最要命的是擦除块大小和坏块两项。littlefs 的磨损均衡算法是按块来做的,块越大,元数据对占用的空间比例越小,但单次擦除的代价越高。而坏块的存在,意味着 NAND 上不能简单地用"块号 × 块大小"来算物理地址,必须经过一层坏块映射。

2.3 littlefs 的元数据对机制简述

littlefs 把每个目录和文件都表示成一个"元数据对"——两个相邻的块,轮流写入。每次修改,它把新数据追加到当前活跃块,写满后擦除旧块,切换过去。这个设计的好处是:任何时刻至少有一个块是完整的,掉电后可以从另一个块恢复。

但这个机制对block_size非常敏感。如果块太小(比如 4KB),元数据对能容纳的 tag 数量有限,频繁修改大目录会导致频繁的块切换和擦除,寿命消耗快。如果块太大(比如 256KB),每次元数据提交都要擦除 256KB,延迟高,而且小文件场景下空间浪费严重。

所以 NOR 上通常选 4KB 块,NAND 上要根据实际擦除块大小来定,常见的是 128KB。这个选择不是拍脑袋,后面我会讲怎么算。

3. NOR Flash 适配实战:从配置到调优

3.1 块大小与块数量的计算

假设你手头是一块 W25Q128,16MB 容量,扇区 4KB,共 4096 个扇区。littlefs 的block_size就设 4096,block_count设 4096。但实际使用中,我一般会留出前面几个扇区给 bootloader 或者参数区,所以block_count会减掉预留部分。

这里有个细节:littlefs 需要至少 2 个块来做元数据对,所以block_count不能小于 2。另外,lookahead_size这个参数控制磨损均衡的扫描粒度,它必须是 8 的倍数,且block_count不能超过lookahead_size × 8。比如 4096 个块,lookahead_size至少要是 512 字节。

#define NOR_BLOCK_SIZE 4096 #define NOR_BLOCK_COUNT 4096 #define NOR_LOOKAHEAD 512 static uint8_t nor_read_buf[NOR_BLOCK_SIZE]; static uint8_t nor_prog_buf[NOR_BLOCK_SIZE]; static uint8_t nor_lookahead_buf[NOR_LOOKAHEAD]; const struct lfs_config nor_cfg = { .read = nor_read, .prog = nor_prog, .erase = nor_erase, .sync = nor_sync, .read_size = 1, .prog_size = 1, .block_size = NOR_BLOCK_SIZE, .block_count = NOR_BLOCK_COUNT, .cache_size = 256, .lookahead_size = NOR_LOOKAHEAD, .block_cycles = 500, .read_buffer = nor_read_buf, .prog_buffer = nor_prog_buf, .lookahead_buffer = nor_lookahead_buf, };

注意read_size和prog_size在 NOR 上可以设成 1,因为 NOR 支持字节级访问。但如果你用的是 QSPI 模式,驱动层可能要求 4 字节对齐,那就得设成 4。

3.2 擦除与写入的对齐陷阱

NOR 的擦除指令通常要求地址按扇区对齐。比如0x20扇区擦除,地址低 12 位必须是 0。littlefs 传下来的块号乘以block_size就是物理地址,只要block_size等于扇区大小,天然对齐。

但写入就不一定了。有些 NOR 芯片在 QSPI 模式下要求页编程地址按 256 字节对齐,如果你prog_size设成 1,驱动层就得自己做读-改-写。我一般会在驱动里加一层缓冲,把非对齐写入攒成对齐的页写。

static int nor_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { uint32_t addr = block * c->block_size + off; const uint8_t *src = buffer; while (size > 0) { uint32_t page_remain = 256 - (addr & 0xFF); uint32_t chunk = (size < page_remain) ? size : page_remain; nor_page_program(addr, src, chunk); addr += chunk; src += chunk; size -= chunk; } return 0; }

这段代码的逻辑是:每次写入不超过当前页剩余空间,跨页时自动切到下一页。这样即使 littlefs 传下来一个跨页的写请求,驱动层也能正确处理。

3.3 block_cycles 与磨损均衡的取舍

block_cycles这个参数控制元数据块在多少次擦除后强制迁移。设成 -1 表示禁用动态磨损均衡,设成 100 表示每 100 次擦除换一个块。NOR 的擦写寿命是 10 万次,如果你有一个频繁更新的配置文件,不设这个参数,那个块很快就会被擦穿。

我的经验值是:对于频繁写入的场景,设 100 到 500;对于只读或极少写入的场景,设 -1 或者 1000 以上。设太小会导致元数据频繁迁移,性能下降;设太大则磨损集中。

实测下来,一块 16MB 的 NOR,block_cycles设 500,每天写入 1000 次,理论寿命可以撑到 10 年以上。这个计算很简单:10 万次擦除 × 500 次迁移 = 5000 万次写入,除以每天 1000 次,就是 13 万年。当然这是理想值,实际还要考虑其他块的磨损。

3.4 cache_size 对性能的影响

cache_size是 littlefs 用来缓存元数据块的缓冲区大小。它必须大于等于prog_size,且是prog_size的倍数。在 NOR 上,我一般设 256 或 512。

这个参数直接影响文件创建、目录遍历的速度。设太小,每次读元数据都要多次调用read回调;设太大,RAM 占用高。在 RAM 紧张的 MCU 上,256 是个比较平衡的值。我试过设 64,创建 100 个文件耗时增加了将近 40%。

4. NAND Flash 适配实战:坏块、ECC 与页对齐

4.1 NAND 的物理结构与 littlefs 的冲突

NAND 的读写单位是页,擦除单位是块。一块典型的 SPI NAND(比如 W25N01GV)是 2KB 页 + 64B spare,64 页一块,即 128KB 擦除块。littlefs 的block_size必须设成 128KB,block_count就是总块数。

但这里有个问题:littlefs 的prog_size如果设成 2048(页大小),那它每次写入至少 2KB。对于小文件或者元数据更新,这会造成大量读-改-写。如果设成 1,驱动层就要自己处理页内偏移和跨页。

我的做法是:prog_size设成页大小,read_size也设成页大小,然后在驱动层做缓存。littlefs 内部有prog_buffer和read_buffer,大小等于cache_size,驱动层再维护一个页缓存,把非对齐访问攒成整页操作。

4.2 坏块管理的两种策略

NAND 出厂时可能有坏块,使用过程中也会产生新的坏块。littlefs 本身不处理坏块,它假设底层块设备是可靠的。所以坏块管理必须在驱动层做。

策略一:保留坏块表,逻辑块号到物理块号做映射。启动时扫描所有块,读 spare 区的坏块标记(通常是第 0 页 spare 的第 0 字节,非 0xFF 即为坏块),建立一张映射表。erase和prog时查表转换。

策略二:跳过坏块,用连续逻辑块号覆盖不连续物理块。这种方法实现简单,但需要维护一个"下一个可用物理块"的游标。

我一般用策略一,因为 littlefs 的块号是连续的,映射表可以用一个数组实现,RAM 开销是block_count × 2字节。对于 1Gb NAND(1024 个块),也就 2KB。

static uint16_t badblock_map[NAND_BLOCK_COUNT]; static uint16_t logic_to_phys[NAND_BLOCK_COUNT]; static int nand_erase(const struct lfs_config *c, lfs_block_t block) { uint16_t phys = logic_to_phys[block]; if (phys == 0xFFFF) return LFS_ERR_CORRUPT; return nand_block_erase(phys); }

4.3 ECC 校验的集成位置

NAND 必须做 ECC。SPI NAND 芯片通常内置 ECC 引擎,通过配置寄存器开启,读写时自动校验。但有些裸 NAND 需要软件 ECC,那就得在驱动层实现 BCH 或者 Reed-Solomon。

我的建议是:能用硬件 ECC 就用硬件 ECC。软件 ECC 会占用大量 CPU 时间,而且实现复杂。如果芯片内置 ECC,在初始化时配置好,读写回调里直接调用芯片的页读写指令即可。

需要注意的是,开启 ECC 后,spare 区的部分字节会被 ECC 校验码占用,坏块标记的位置可能变化。一定要查芯片手册确认。

4.4 页缓存与写合并的实现

NAND 的页编程时间典型值是 200~700 微秒,块擦除是 2~10 毫秒。如果每次小写入都触发一次页编程,性能会非常差。所以驱动层必须做写合并。

我的实现是维护一个 2KB 的页缓存和一个脏标记。当 littlefs 调用prog时,如果写入区域在当前缓存页内,就更新缓存并置脏;如果跨页,先把脏页刷下去,再处理新页。sync回调里强制刷脏页。

static uint8_t page_cache[2048]; static uint32_t cache_page_addr = 0xFFFFFFFF; static bool cache_dirty = false; static int nand_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { uint32_t addr = block * c->block_size + off; uint32_t page_addr = addr & ~(2048 - 1); uint32_t page_off = addr & (2048 - 1); if (page_addr != cache_page_addr) { if (cache_dirty) nand_page_program(cache_page_addr, page_cache); nand_page_read(page_addr, page_cache); cache_page_addr = page_addr; cache_dirty = false; } memcpy(page_cache + page_off, buffer, size); cache_dirty = true; return 0; }

这段代码的关键点是:切换页之前先把脏页刷下去,再读新页。读新页是因为 NAND 写入前必须是 0xFF,而我们要做部分更新,必须先读出原内容。

5. 两种介质上的性能对比与参数调优

5.1 实测数据对比

我在同一块开发板上,分别接 W25Q128(NOR)和 W25N01GV(NAND),跑了相同的测试用例:创建 100 个 1KB 文件,然后读取、删除。结果如下:

操作NOR (4KB块)NAND (128KB块)
格式化1.2s8.5s
创建100文件3.4s6.8s
读取100文件0.8s1.5s
删除100文件2.1s4.2s
RAM占用1.5KB4KB

NAND 慢的主要原因是擦除块大,格式化要擦 1024 个块,每个块 2ms,就是 2 秒,加上坏块扫描,8.5 秒很正常。创建文件慢是因为元数据提交要擦除 128KB 块。

5.2 针对 NAND 的优化手段

手段一:减小 block_size。有些 SPI NAND 支持 64KB 擦除块(通过配置寄存器),这样block_size可以设 64KB,擦除时间减半。但不是所有芯片都支持。

手段二:增大 cache_size。NAND 上我把cache_size设成 2048,和页大小一致,减少读-改-写次数。RAM 占用增加,但性能提升明显。

手段三:调整 block_cycles。NAND 擦写寿命只有 1 万次,block_cycles要设小一点,比如 100,让磨损更均匀。

手段四:预擦除。在系统空闲时,后台预擦除一些块,减少写入时的等待。这个需要自己实现一个后台任务。

5.3 NOR 上的性能优化

NOR 的瓶颈通常在写入和擦除。优化手段包括:

  • 开启 QSPI 四线模式,读取速度提升 4 倍;
  • 使用页编程而不是字节编程,减少指令开销;
  • 把频繁写入的数据放到 RAM 里缓存,定期刷写;
  • block_cycles适当调大,减少元数据迁移。

我实测过,QSPI 模式下 NOR 的读取速度从 20MB/s 提升到 80MB/s,littlefs 的目录遍历速度提升约 3 倍。

6. 常见问题与排查技巧实录

6.1 挂载失败问题速查表

现象可能原因排查方法
LFS_ERR_CORRUPTblock_size 不匹配确认 block_size 等于擦除块大小
LFS_ERR_IO驱动读写失败用逻辑分析仪抓 SPI 波形
LFS_ERR_NOSPCblock_count 太小检查是否预留了坏块空间
挂载后文件丢失掉电时元数据未提交检查 sync 回调是否实现
写入后读回数据错ECC 未开启或配置错读芯片状态寄存器

6.2 掉电测试怎么做才靠谱

掉电安全是 littlefs 的核心卖点,但很多人的测试方法不对。我见过有人直接拔电源,结果文件系统挂了,就怪 littlefs 不行。其实问题在于:拔电源的时机是随机的,可能正好在擦除过程中,这时候任何文件系统都救不了。

正确的测试方法是:在prog和erase回调里加一个计数器,每 N 次操作后触发一次复位。这样能精确控制掉电时机,覆盖各种边界情况。我一般会跑 1000 次随机复位,确认每次挂载后文件系统都一致。

6.3 NAND 坏块导致的诡异问题

有一次客户反馈,设备运行几天后文件系统就挂了。我查了半天,发现是 NAND 产生了新的坏块,而驱动层没有动态更新坏块表。解决方法是在erase失败时,把该块标记为坏块,更新映射表,并返回LFS_ERR_CORRUPT让 littlefs 处理。

但这里有个坑:littlefs 收到LFS_ERR_CORRUPT后,会尝试从元数据对的另一个块恢复。如果两个块都坏了,文件系统就真的挂了。所以坏块管理要尽量提前,不要等到擦除失败才发现。

6.4 性能不达预期的排查思路

如果你觉得 littlefs 慢,按这个顺序查:

  1. 确认block_size和block_count配置正确;
  2. 检查cache_size是否太小;
  3. 看read/prog回调里有没有不必要的延时;
  4. 用示波器测 SPI 时钟频率是否达到芯片上限;
  5. 确认没有频繁的元数据迁移(调大block_cycles)。

我遇到过一个案例,客户把cache_size设成 16,结果创建 1000 个文件花了 30 秒。改成 256 后,降到 4 秒。

6.5 几个容易忽略的细节

  • lookahead_size必须是 8 的倍数,且block_count ≤ lookahead_size × 8;
  • read_buffer和prog_buffer如果不用静态分配,可以用malloc,但要注意对齐;
  • NAND 的 spare 区读写需要单独的指令,不要和主区混在一起;
  • littlefs 的format会擦除所有块,NAND 上耗时很长,建议在工厂预格式化。

7. 从适配到优化:我的经验总结

7.1 选型建议:什么时候用 NOR,什么时候用 NAND

如果你的数据量小于 16MB,写入频率不高,选 NOR。它的字节级访问、无坏块、简单驱动,能省掉大量适配工作。littlefs 在 NOR 上的表现非常稳定,我做过连续 72 小时的压力测试,没有出现一次挂载失败。

如果数据量大于 128MB,或者需要存储音频、图像等大文件,选 NAND。但要做好心理准备:坏块管理、ECC、页缓存,每一项都要花时间调试。littlefs 在 NAND 上的性能不如在 NOR 上,但胜在容量和成本。

7.2 参数配置的通用原则

我把 littlefs 的参数配置总结成几条原则:

  • block_size必须等于物理擦除块大小,没有例外;
  • block_count要减去预留块和坏块余量,NAND 上建议留 2%;
  • cache_size至少 256,NAND 上建议等于页大小;
  • lookahead_size按block_count / 8向上取整到 8 的倍数;
  • block_cycles根据擦写寿命和写入频率算,NOR 设 500,NAND 设 100。

7.3 后续可以扩展的方向

如果你已经把 littlefs 跑通了,可以考虑这几个方向继续优化:

  • 实现一个块设备抽象层,让同一套代码同时支持 NOR 和 NAND,通过配置切换;
  • 加入后台垃圾回收任务,在系统空闲时预擦除块;
  • 对 NAND 实现动态坏块管理,运行时更新坏块表;
  • 结合 RTOS,把 littlefs 的读写操作放到独立线程,避免阻塞主循环。

我个人在实际操作中的体会是:littlefs 的适配工作,80% 的时间花在驱动层,20% 花在参数调优。驱动层写好了,参数调优就是水到渠成的事。反过来,如果驱动层有隐患,再怎么调参数都是白搭。所以我的建议是,先把read/prog/erase三个回调写扎实,用单元测试覆盖各种边界情况,再上 littlefs 跑业务逻辑。这样能省掉后面大量的排查时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 2:02:52

IP5356与SC8815快充芯片量产选型实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:02:17

TSMaster高效处理BLF报文回放与离线分析实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:00:55

Python空气质量数据挖掘与机器学习预测模型实战

简介&#xff1a;这份资源面向环境科学、数据挖掘与机器学习方向的学习者和研究者&#xff0c;提供一套基于Python的空气质量数据可视化分析系统源码及配套数据。项目采用BS架构&#xff0c;前端整合HTML、CSS、JavaScript与D3、ECharts、Mapbox等可视化库&#xff0c;后端基于…

作者头像 李华
网站建设 2026/9/28 2:00:33

机器人嵌入式工程师四城对比:深圳上海北京杭州怎么选

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:00:15

杰发AC7801x芯片J-Link调试烧录全栈适配指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:59:56

YOLOv8车道偏离预警系统:训练、部署与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华