ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

MicroPython存储与文件系统:Flash、VFS与littlefs实战指南

MicroPython存储与文件系统:Flash、VFS与littlefs实战指南 很多刚接触 MicroPython 的朋友都会问一个问题为什么板子上的变量重启之后就没了但用open()写进文件的内容断电再开居然还在这个问题的答案就藏在 MicroPython 的存储和文件系统里。我最初也以为文件系统只是 SD 卡才有的功能直到把 ESP32 上的 Flash 空间用满、把文件写坏、甚至刷过新固件后挂载不上才真正意识到在这类小设备上认真做数据记录、配置保存和日志管理底层原理是绕不开的。这篇东西我尽量用大白话讲覆盖 Flash 存储特性、VFS 挂载机制、FAT 和 littlefs 的取舍以及我实际踩过的坑争取让新手看完能少走半年弯路。1. 先搞懂 MicroPython 的存储架构程序、数据、文件分别住在哪1.1 从 Flash 到 RAM为什么掉电不丢数据的是 FlashMicroPython 运行在一块 MCU 上最常见的芯片有 ESP32、ESP8266、RP2040、STM32 系列。无论哪一颗芯片内部都有两种关键存储器RAM 和 Flash。RAM 的读写速度非常快去掉供电数据立刻清空。你定义的变量、列表、字典、类对象运行时全部活在 RAM 里。MicroPython 的解释器执行字节码、调用函数、计算表达式主要也是靠 RAM 来回倒腾数据。Flash 则是非易失存储器掉电后数据依然保留。MicroPython 固件本身就被烧录在 Flash 里你写的main.py和boot.py也会保存到 Flash 的某个区域。通过open(data.txt, w)写入的文本或二进制内容最终也是写到 Flash 里。这个布局和 PC 非常相似RAM 相当于电脑内存Flash 相当于固态硬盘。电脑断电后内存清空但硬盘上的文件还在。MicroPython 设备也是一样的逻辑只不过 Flash 容量小得多从几百 KB 到几十 MB 不等。很多人以为open()写文件是“MicroPython 特有的魔法”其实它背后就是在操作一个微型块设备。脚本调用文件接口MicroPython 的虚拟文件系统层会负责把这个请求翻译成对 Flash 的读写命令再经由驱动操作硬件。1.2 VFS 挂载点/、/flash、/sd 是怎么来的MicroPython 引入了 VFSVirtual File System虚拟文件系统抽象层目的是让上层代码不用关心底层存储设备到底是什么。你会在电脑上看到 C 盘、D 盘、U 盘等盘符映射到不同目录。MicroPython 也做了类似的事只不过没有盘符概念而是用路径来区分不同存储设备。在大多数板子上根路径/默认挂载内部 Flash 文件系统。于是你可能看到/main.py、/boot.py、/lib这样的路径。如果你插了一张 SD 卡并执行os.mount(sd, /sd)那之后访问/sd/xxx.txt就是在读写 SD 卡。多个存储设备可以同时挂载路径各自独立互不干扰。这个机制非常有价值你写一个record.py放在根目录它可以同时往/data.json写入配置往/sd/log.csv写入日志完全由路径决定数据落到哪个设备。所以当有人抱怨“为什么我的文件消失了”第一步不是检查文件内容而是检查当前路径到底挂载在哪个设备上。我见过很多新手把 SD 卡挂载到/sd却忘记插卡就执行写入结果写到了内部 Flash 却不自知。2. Flash 底层原理为什么 Flash 不是像内存一样随便写2.1 从 NOR 到 NAND开发板上的 Flash 是哪一种Flash 存储器分成两大流派NOR Flash 和 NAND Flash。NOR Flash 容量偏小但读取速度快支持从 Flash 直接执行代码XIP所以非常适合存放固件。MCU 内部 Flash、外部 SPI NOR Flash绝大多数就是 NOR 类型。ESP32、ESP8266、RP2040 板载的 Flash基本都是 SPI NOR Flash。NAND Flash 容量大按页读写、按块擦除成本低适合做 U 盘和 SSD。但 NAND 的接口和控制更复杂出厂还可能有坏块MCU 直接管理起来很麻烦所以在 MicroPython 生态里用户文件系统很少直接建立在裸 NAND 上。我们平时说的“Flash 不能覆盖写”主要针对 NOR 和 NAND 共同的一类特性写操作只能把某些位从 1 变成 0想要把 0 变回 1就必须执行擦除操作而擦除以较大的块为单位。我打一个比方Flash 就像一块有格子的黑板你用粉笔在格子里写字写入。但每次只能把格子涂黑写 0要想把黑板重新变白恢复成 1必须用黑板擦把整个区域擦一遍而且黑板擦的最小面积是固定的不能只擦一个格子。这个“最小擦除面积”就是 Flash 擦除块。2.2 块、扇区、页擦除、编程、覆盖为什么不行在 Flash 的世界里经常听到三个词扇区Sector、块Block、页Page。不同芯片的命名和大小不完全一致但通常可以理解为扇区是擦除操作的最小单位比如 4KB。页是编程写入的最小单位常见大小是 256B 或 4KB。块可能是多个扇区的组合不同文件系统对块的定义不同。假设文件系统要在某个扇区里更新一个 16 字节的配置项。如果只是把这 16 字节直接写进去由于原先的数据可能包含 0 位无法把 0 改回 1结果就会产生错误。所以文件系统必须先把整个扇区内容读入内存修改那 16 字节然后把整个扇区擦除再把修改后的完整数据写回去。这就是为什么 Flash 文件系统不能像 PC 内存那样逐字节覆盖。文件系统必须管理“哪些块是干净的”“哪些块脏了需要回收”“什么时候执行擦除”。如果没有这层管理用户程序直接读写 Flash很快就会遇到数据异常甚至把固件区域破坏。MicroPython 的 VFS 层底下就是一类块设备对象块设备接口只需提供读块、写块、ioctl 控制几个方法就可以被文件系统使用。文件系统负责把对文件的逻辑读写转换成对块设备的物理块读写并处理擦除、分配、回收等一系列问题。2.3 磨损均衡与掉电保护文件系统为什么要管这些Flash 的每个扇区都有擦写寿命限制。NOR Flash 常见的标称寿命是 10 万次擦除NAND 可能更低或更高取决于具体型号。10 万次听起来很多但如果你有个日志功能每 10 秒写一次文件每天都固定写在同一个逻辑位置那么几天后这个区域就可能磨损。磨损均衡Wear Leveling就是把写入操作尽量均匀地分布到整个存储空间避免某个扇区被反复擦除其他扇区却一直闲置。littlefs 内置了基本的磨损均衡能够在文件频繁更新的情况下延长 Flash 寿命。掉电保护也很关键。微控制器设备经常被直接断电如果恰好在文件写入过程中断电目录项可能只更新了一半FAT 表可能出现不一致最终表现为文件损坏、容量突然变小、甚至设备无法挂载。日志结构文件系统通过“写新数据再更新指针”的方式尽量让掉电时只丢失最近一次未完成写入而不破坏整个文件系统结构。3. MicroPython 的 VFS 与文件系统实现FAT vs littlefs3.1 VFS 层做了什么mount、listdir、open 背后的调度MicroPython 的os模块提供了文件系统操作但真正的调度权在 VFS 层。每次调用os.listdir(/)VFS 会解析路径找到/路径上挂载的块设备再调用对应的文件系统实现。如果是 SD 卡底层可能是 FAT如果是内部 Flash底层可能是 littlefs。VFS 只负责路由和抽象不关心具体文件系统逻辑。你还可以自己实现块设备类然后挂载上去。块设备对象至少要实现class MyBlockDev: def readblocks(self, block_num, buf): pass def writeblocks(self, block_num, buf): pass def ioctl(self, op, arg): passioctl用于查询块大小、设备容量、同步等。只要这几个方法正确就可以传给os.VfsLfs2或os.VfsFat构造文件系统后挂载。这个能力让高级玩家可以挂载外部 SPI Flash、SD 卡甚至自定义存储芯片。在实际开发中你不需要天天写块设备驱动但理解这个分层可以帮你快速定位问题文件操作报错要么是上层文件系统损坏要么是底层块设备驱动异常。先确认挂载是否成功再排查驱动会比瞎猜高效得多。3.2 FAT 文件系统的特点与适用场景FATFile Allocation Table文件系统是最古老也最通用的文件系统之一。Windows 电脑可以直接读写 FAT16/FAT32 的 SD 卡数码相机、车载设备、各类廉价播放器也普遍支持 FAT。FAT 的优势是简单、兼容性好。如果你想通过 USB 读卡器把 MicroPython 记录的日志放到电脑上直接查看SD 卡用 FAT 格式最方便。但 FAT 也有明显缺点元数据更新不是原子的写文件与更新 FAT 表、目录项分多步完成掉电容易损坏。FAT 表本身需要频繁更新同一个扇区会被反复擦写在内部 Flash 上做磨损均衡比较吃力。对长文件名支持依赖附加目录项复杂存储场景下开销不小。文件大小和分区大小有限制FAT16 不能支持超大分区。因此MicroPython 官方内部文件系统并不推荐使用 FAT而是默认使用 littlefs。FAT 更适合 SD 卡、U 盘等需要与电脑频繁交换数据的场景。3.3 littlefs v1/v2专为微控制器设计的日志结构文件系统littlefs 是 ARM 主导设计的嵌入式文件系统专门考虑了小资源设备的限制RAM 占用低、代码量小、掉电安全、磨损均衡良好。它的核心思路是日志结构log-structured和写时复制Copy-on-Write。写入新数据时不会直接覆盖旧数据而是先写到新的块更新元数据后再让指针对准新位置。这样就算断电旧数据也可能还在文件系统结构不会被破坏。littlefs 有两个主要版本littlefs v1 和 littlefs v2。v2 优化了性能特别是目录遍历和元数据读取方面有明显提升。新版本 MicroPython 固件大多默认使用 littlefs v2如果旧项目格式化时指定 v1升级固件后也可能继续兼容但最好确认固件支持情况。与 FAT 相比littlefs 的兼容性差一些PC 直接读不了但它更适合嵌入式系统尤其是内部 Flash。MicroPython 官方文档也建议内部文件系统用 littlefsSD 卡等外部移动存储才用 FAT。3.4 分区布局固件和文件系统如何共存MCU 的 Flash 空间不能全用来存文件。固件、启动引导、MicroPython 解释器本身都要占用空间所以必须做分区规划。以 ESP32 为例Flash 里有一个分区表把存储空间划分成factory分区存放 MicroPython 固件。vfs分区存放用户文件系统。其他区域可能还有 bootloader、分区表、NVS 等。esp32.Partition(esp32.Partition.DATA)可以拿到用户数据分区对象内部文件系统就建立在这个分区上。如果你更新固件后文件还在说明固件烧录时没有动vfs分区如果全盘擦除烧录文件自然就没了。RP2040 的玩法略有不同它常用外部 QSPI Flash固件和文件系统都在这颗 Flash 上。烧录时如果选择擦除整个 Flash文件系统也会被清空。所以不要以为升级固件一定保留文件具体取决于烧录工具的擦除范围和分区配置。理解分区布局还有个实际用途当文件系统损坏导致无法开机时你可以通过重新格式化文件系统分区而不必重烧固件。4. 在开发板上把存储用起来从查看容量到自定义挂载4.1 用 os.statvfs 查看真实剩余空间在 MicroPython 里最直接的容量查询 API 是os.statvfs(/)。import os info os.statvfs(/) print(info)返回值是一个元组常见字段含义如下下标含义0文件系统块大小1总块数2空闲块数4可用块数9最大文件名长度计算总容量和剩余容量的代码一般是这样import os info os.statvfs(/) block_size info[0] total_size info[1] * block_size free_size info[2] * block_size print(总容量: %.2f MB % (total_size / 1024 / 1024)) print(剩余容量: %.2f MB % (free_size / 1024 / 1024))我在 ESP32 开发板上跑这段代码显示总容量为 1.4 MB 左右这是正常现象。别指望 4MB Flash 就能装下 4MB 文件固件和引导程序已经占用大部分空间。此外os.statvfs(/sd)可以查看 SD 卡挂载点。如果你发现剩余容量和电脑上看到的不一致先确认是否自己的文件写到了别的路径。4.2 重格式化内部文件系统何时做怎么做以下情况可能需要重新格式化内部文件系统文件系统结构损坏开机报OSError: [Errno 22] EINVAL或OSError: [Errno 84] EROFS。你想把内部 Flash 从 littlefs v1 切换到 littlefs v2。你想完全清空所有文件又不想重刷固件。在 ESP32 上典型的重格式化流程如下import os import esp32 bdev esp32.Partition(esp32.Partition.DATA) os.umount(/) # 如果 / 已被挂载先卸载 os.VfsLfs2.mkfs(bdev) # 格式化 vfs os.VfsLfs2(bdev) os.mount(vfs, /)注意格式化会清空文件系统里的全部数据。操作前一定要把main.py、boot.py和重要数据备份到电脑或 SD 卡否则哭都来不及。不同开发板的块设备获取方式不一样pyboard 可能是pyb.Flash(start0, len1024*1024)RP2040 上可能是flashbdev.bdev。具体要参考对应固件的文档。如果不知道当前块设备对象怎么拿可以执行import os print(os.listdir(/))然后在启动脚本或源码里搜索VfsLfs2或VfsFat的挂载方式很多固件会在boot.py或main.py里暴露。4.3 挂载外部 SD 卡从接线到 os.mountSD 卡是最常用的外部存储扩展方案。大部分 MicroPython 板子通过 SPI 接口与 SD 卡通信。接线一般是SD 卡VCC接 3.3V不能接 5VSD 卡GND接 GNDSD 卡SCK接 SPI SCKSD 卡MOSI接 SPI MOSISD 卡MISO接 SPI MISOSD 卡CS接任意一个空闲 GPIO片选然后使用 MicroPython 官方sdcard驱动import os from machine import SPI, Pin import sdcard spi SPI(1, baudrate40000000, polarity0, phase0, sckPin(10), mosiPin(11), misoPin(12)) cs Pin(13, Pin.OUT) sd sdcard.SDCard(spi, cs) os.mount(sd, /sd)挂载成功后你就可以正常读写/sd路径下的文件。几个实用建议SD 卡格式最好先用电脑格式化为 FAT32。首次使用可以在代码里检查/sd in os.listdir(/)或直接 try 一下挂载。拔卡之前先os.umount(/sd)避免数据缓存未写盘。有些 SD 卡电压或时序不标准如果os.mount报OSError先把 SPI 时钟降低到 1-10 MHz 试试。4.4 数据写盘与 syncflush 和 sync 到底谁更可靠很多新手写文件时写完立刻断电结果打开文件内容为空或残缺。这不是文件系统坏了而是数据还在缓存里没真正写到 Flash。看这段代码f open(data.txt, w) f.write(hello) f.close()执行close()后数据不一定马上落到 Flash。MicroPython 为了性能可能让底层驱动把数据先暂存在 RAM 缓冲区。如果这之后立刻断电缓冲区里的数据就会丢失。flush()的作用是把文件对象内部缓冲的数据交给 VFS 层但并不保证已经提交到硬件。os.sync()才会请求底层块设备把所有脏数据写回 Flash。所以关键数据写入后建议这样处理f open(data.txt, w) f.write(hello) f.flush() os.sync() f.close()如果要频繁记录日志也不必每次写完都sync那样性能和寿命都会受影响。可以采取“每隔 N 条记录执行一次os.sync()”的策略。5. 文件系统常见故障排查与避坑实录5.1 文件写一半断电损坏、容量异常、挂载失败这是嵌入式开发最经典的“翻车现场”。表现通常有几种开机后os.listdir(/)能看到目录但打开某个文件报错。设备反复重启报OSError。可用空间莫名变少删掉文件也释放不了。挂载时提示OSError: [Errno 19] ENODEV或[Errno 22] EINVAL。如果文件系统损坏不严重可以尝试重启恢复。如果无法恢复只能重新格式化。为了防止问题扩大我在实际项目里会做三层保护重要配置用独立文件保存不要和日志混在一起。日志写满后自动轮转避免单个文件持续被写。定期把内部 Flash 数据备份到 SD 卡或电脑。我用过一个数据采集设备外接电源不稳定经常被人直接拔电。一开始用 FAT 格式内部 Flash平均两周就挂一次。后来改用 littlefs 并加上os.sync()策略连续运行几个月都没再出现文件系统损坏。5.2 文件名、中文、大小限制这些“软坑”文件系统报错不一定都是硬件问题很多是文件名和路径的“软坑”。MicroPython 的open()不会自动创建不存在的目录。比如你要写/log/2025/data.txt但/log或/log/2025目录不存在就会报OSError: [Errno 2] ENOENT。必须先os.mkdir(/log)再os.mkdir(/log/2025)。FAT 文件系统对文件名有历史包袱。虽然 MicroPython 通常支持长文件名但中文命名在部分固件上编码不稳定。我的建议是嵌入式设备上的文件一律用 ASCII 字符命名比如data_20250101.csv不要用中文或特殊符号。littlefs 对文件名支持比 FAT 好很多但也不要写超长的路径。os.statvfs返回的f_namemax可以查看最大文件名长度编写代码时得留足余量。5.3 存储空间被吃满日志循环和临时文件的处理IoT 设备长期跑日志最怕存储空间被慢慢吃满。一个每秒写 20 字节的日志一天就是 1.7MB几天后内部 Flash 就满了。关键不是“满了会自动清理”而是“满了之后open(..., w)可能直接失败”。为了解决这个问题我一般会在日志模块里加一个容量检查import os def get_free_mb(path/): info os.statvfs(path) return info[0] * info[2] / 1024 / 1024写入前检查剩余空间如果低于设定阈值就先删除最旧的日志文件或者把当前日志文件关闭然后新建一个。对于 DB 存储、传感器采集这类高频写入场景强烈建议用 SD 卡或者外置 Flash内部 Flash 只保存配置和少量关键状态。5.4 几个我踩过的坑和总结的检查顺序这些年我在 MicroPython 存储上踩过不少坑挑几个典型分享。第一次踩坑是“文件写进去了读出来却只有一半”。排查了很久发现是 SD 卡供电不稳SPI 信号在高速下出错数据写到错误地址。后来把 SPI 频率从 40MHz 降到 10MHz 就正常了。别盲信 SD 卡标称速度在真实接线上要留够余量。第二次踩坑是“内部 Flash 只剩几十 KB但文件删不掉”。原因是某个脚本以追加模式open(log.txt, a)没关文件导致句柄一直占用删除逻辑失败。嵌入式环境里文件句柄是稀缺资源用完必须close()尤其是异常处理分支里也要加上finally。第三次踩坑是升级固件后文件系统全空了。因为我烧录时选择了“擦除整个 Flash”覆盖了vfs分区。从那以后我要么只烧应用固件不擦除全片要么在升级前先导出重要文件。如果遇到文件系统异常我建议按这个顺序排查排查步骤命令或操作作用1. 确认设备挂载正常os.mount状态os.listdir(/)是否能列出判断文件系统是否可访问2. 查看容量os.statvfs(/)确认空间有没有满3. 检查单个文件open(file, r)定位是否只有特定文件损坏4. 查看文件系统类型启动日志或help(modules)找 Vfs 类确认用的是 FAT 还是 littlefs5. 最后考虑格式化备份后mkfs修复严重损坏最后再分享一个我养成的习惯每次在文件系统相关代码上做改动之前一定会先把板子上的main.py和关键配置同步到电脑。很多设备看起来是“逻辑问题”实际上只需要一个最简单的备份就能让你的开发效率提升一倍。如果你准备在 MicroPython 设备上长期做数据记录请记住存储和文件系统的底层原理不复杂但每一个细节都可能关系到你的数据安全问题。
RELATED READING

延伸阅读

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