从裸扇区到文件系统

用裸扇区管理 Flash 时,每个功能模块都要自己记地址、记长度、记校验,一旦布局要调整,牵一发动全身。文件系统把这一切抽象成"文件名 + 目录 + 字节流",开发者只需要打开文件、读写字节,底层怎么分配、怎么磨损均衡都由文件系统打理。

嵌入式文件系统跑在 NOR Flash 上,天然要面对两个约束:擦除是块级的(一次擦 4KB 到 64KB),写是页级的(一次写 256 字节),而且任何时刻都可能断电。所以嵌入式文件系统不能照搬 PC 上的 FAT 思路,必须专门设计。

本文核心知识点
  • LittleFS专为 MCU 设计的掉电安全文件系统。
  • 掉电保护写一半断电不丢旧数据。
  • 磨损均衡延长 NOR Flash 擦写寿命。
  • 日志策略顺序追加 + 双缓冲。

选择文件系统的本质,是在"功能完整"和"资源开销"之间做权衡。裸扇区适合数据量小、结构固定的场景;数据一多、结构一变,文件系统的收益就非常明显。

嵌入式文件系统配图:深蓝渐变背景上堆叠的蓝色立方体存储块,上方延伸出发光树状目录节点,右侧文件夹轮廓的扁平科技插画
文件系统把混乱的扇区,整理成井井有条的目录树。

文件系统选型

嵌入式常用的文件系统有 FATFS、LittleFS 和 SPIFFS。FATFS 兼容性好,文件可以拿到 PC 上直接读,但它没有掉电保护和磨损均衡,适合数据不重要或断电极少的场景。

  • LittleFS:掉电安全 + 磨损均衡 + 低 RAM,Matter 设备标准存储方案,适合参数、日志、证书。
  • SPIFFS:实现简单,适合小容量 ESP8266,但写放大和掉电表现一般,社区维护放缓。
  • FATFS:跨平台兼容,适合需要 U 盘导出、上位机直接读取的场景。

选择时还要看 RAM 占用:LittleFS 的运行缓冲与块大小相关,默认配置下只占几 KB 内存,在几十 KB RAM 的 MCU 上也能跑得动。块大小、缓存大小、前瞻窗口这些参数要根据实际 Flash 容量和读写频率一起调。

LittleFS 移植与挂载

LittleFS 的移植工作集中在四类回调:读、写、擦除、同步。上层用这些回调把 Flash 抽象成块设备,配置好块大小和块数量就能挂载。

// littlefs 配置与挂载
lfs_t lfs;
lfs_config cfg = {
  .read  = flash_read_cb,     // 读一块
  .prog  = flash_prog_cb,     // 写一块
  .erase = flash_erase_cb,    // 擦一块
  .sync  = flash_sync_cb,     // 落盘同步
  .read_size      = 256,      // 最小读粒度
  .prog_size      = 256,      // 最小写粒度
  .block_size     = 4096,     // 块大小,须对齐扇区
  .block_count    = 256,      // 块数量
  .cache_size     = 256,
  .lookahead_size = 128,
  .block_cycles   = 500,      // 磨损均衡轮换周期
};
int rc = lfs_mount(&lfs, &cfg);
if (rc != LFS_ERR_OK) {
  lfs_format(&lfs, &cfg);     // 首次使用或损坏时格式化
  lfs_mount(&lfs, &cfg);
}

回调里尤其要注意写和擦除的时序:写入前要确认目标块已擦除,擦除要在芯片不忙时进行,等待 BUSY 位释放后再返回。同步回调负责把缓存刷进 Flash,掉电安全依赖它及时落盘。

目录与文件读写

LittleFS 提供类 POSIX 的接口:open、close、read、write、mkdir、opendir。用它管理配置、日志和资源文件非常直观。

// 写入配置文件的典型流程
lfs_file_t f;
lfs_mkdir(&lfs, "/cfg");                       // 建立配置目录
int fd = lfs_file_open(&lfs, &f, "/cfg/app.json", LFS_O_WRONLY | LFS_O_CREAT);
lfs_file_write(&lfs, &f, json_buf, json_len);  // 写入字节
lfs_file_sync(&lfs, &f);                       // 同步到 Flash
lfs_file_close(&lfs, &f);

// 读取配置
fd = lfs_file_open(&lfs, &f, "/cfg/app.json", LFS_O_RDONLY);
lfs_file_read(&lfs, &f, buf, sizeof(buf));
lfs_file_close(&lfs, &f);

写入前先删旧文件还是直接覆盖,取决于数据重要性。对关键配置推荐"写临时文件 + 原子重命名":先把新内容写到 tmp 文件并 sync,再用 rename 替换正式文件,这样任何时候断电,磁盘上都有一份完整可用的配置。