从裸扇区到文件系统
用裸扇区管理 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 替换正式文件,这样任何时候断电,磁盘上都有一份完整可用的配置。
掉电保护与日志策略
掉电保护的核心理念是"任何时刻断电,系统都能回到一个一致的状态"。LittleFS 靠 copy-on-write 和原子提交实现这一点,但应用层依然要注意配合。
- 关键配置用双文件交替写:A 版和 B 版轮流更新,启动时校验魔数和 CRC,损坏就回退另一版。
- 日志用顺序追加:一个日志文件写满再开下一个,天然低磨损且恢复简单。
- 写流程三步走:先写数据再写元数据,避免出现"元数据指向半截数据"的中间态。
- 上电恢复:挂载失败或校验失败时,格式化或从备份重建,不让坏状态留在盘上。
日志场景还要防"日志风暴":设备异常时频繁写日志会加速磨损并拖慢系统。可以加日志节流和环形策略,保留最近 N 条,老日志自动覆盖。
磨损均衡与寿命管理
NOR Flash 擦写寿命通常只有十万次左右,高频写入同一个文件会很快把它写穿。磨损均衡就是把擦写均匀分摊到所有块上。
// 运行时监控块擦写次数(示例思路)
uint32_t block_erasures[256]; // 每块擦除计数
void on_erase(uint32_t block) {
block_erasures[block]++;
if (block_erasures[block] > 90000) {
report_wear(block); // 接近寿命阈值,上报告警
}
}
LittleFS 的 block_cycles 参数控制轮换频率:每次分配文件块时,已擦写较少的块会被优先选用。把高频小文件放在专用目录、日志采用循环覆盖、参数集中到少数文件,都能显著延长 Flash 的整体寿命。
常见问题排查
// 1. 挂载失败:块大小与 Flash 扇区不匹配,检查 block_size
// 2. 写入返回错误:块未擦除或擦除超时,检查 BUSY 等待逻辑
// 3. 掉电丢数据:文件打开后没有 sync,数据还在缓存里
// 4. 重启后文件损坏:没有用临时文件加原子重命名
// 5. 写速度慢:prog_size 太大或跨块写,按页对齐写入
// 6. 文件系统占 RAM 多:调小 cache_size 和 lookahead_size
掉电安全的本质不是"不会坏",而是"坏了也能恢复"。
把 LittleFS 和双备份、日志策略组合起来,Flash 里的数据就能扛住各种意外,产品的现场可靠性也就稳了。