ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MicroPython文件系统与Flash存储:掉电保护、FAT/littlefs选型及运维实践

MicroPython文件系统与Flash存储:掉电保护、FAT/littlefs选型及运维实践 把main.py写进MicroPython板子断电再上电代码照常跑——这是很多人第一次接触MicroPython存储与文件系统底层原理的起点。RAM断电即失忆为什么脚本还在是谁把Flash划分成了“程序区”和“文件区”的为什么有些板子文件系统叫FAT有些叫littlefs删除一个大文件后剩余空间为什么没有立刻变大这些问题的答案全藏在存储层级和文件系统的设计细节里。这篇文章不打算写那种“照着敲一遍就完事”的用法教程而是把整条链路拆开从Flash芯片的物理特性到MicroPython的VFS抽象层再到FAT和littlefs的取舍逻辑最后给出格式化、SD卡挂载、同步落盘这些真实项目里每天都在用的操作。全文尽量说人话新手可以当入门地图老手可以当一次系统性复盘。1. 掉电不丢数据的秘密Flash 芯片的物理脾气1.1 RAM和Flash的分工先从一个最简单的区分开始板子上的RAM和Flash一个负责“正在发生的事情”一个负责“被记住的事情”。RAM速度快可以随机读写但一断电里面就全部清零Flash虽然慢一些数据却能保持很多年断电也不丢。MicroPython的main.py并不存在RAM里而是存在Flash的文件系统里。开机时MicroPython解释器会去文件系统里找boot.py和main.py再把它们读进RAM执行。所以断电丢不丢代码取决于你是不是真的把文件写进了Flash。很多新手查内存占用看到gc.mem_free()觉得存储空间变小了其实这个函数查的是RAM和文件系统能放多少文件完全是两码事。RAM相当于办公桌Flash才相当于抽屉。知道这个区别之后很多“为什么我内存明明很多却写不了大文件”的疑问就迎刃而解了。1.2 先擦后写与寿命约束Flash有一个反直觉的物理特性它不能像RAM那样按字节把0改成1或把1改成0。写入Flash之前必须先对一大块区域做“擦除”擦除会把整块区域恢复成全1之后才能把需要的地方写成0。这个擦除操作的最小单元通常叫扇区sector或块block大小从4KB到几十KB不等。这带来两个直接影响。第一如果文件系统只修改一个文件末尾的几十个字节底层可能要把整个扇区读出来在内存里改好再整块擦除并写回去。这也是为什么Flash不适合高频小量写入。第二Flash的擦写次数有上限寿命通常在几千到几万次左右。如果每次写日志都落在同一个扇区那个位置很快会报废。文件系统里的“磨损均衡”就是尽量让所有扇区被擦写的次数均匀从而延长整体寿命这也是文件系统存在的意义之一。MicroPython本身不直接管理这些物理扇区而是通过文件系统去管理。文件系统把一堆扇区组合成逻辑上的“文件”和“目录”负责分配空间、记录哪些块空闲、哪些块被占用。文件系统选得好不好直接决定了你的数据在断电和长期写入下能不能活下来。1.3 Flash不一定就是一颗独立芯片还要澄清一个常见误解很多板子的Flash并不是一颗外置小芯片。ESP32和STM32把Flash集成在主控内部或封装在模块里树莓派Pico则是外挂QSPI Flash芯片。SD卡本质上也是Flash只是多了一层控制器和磨损均衡逻辑。对MicroPython来说不管是内置Flash还是外挂SD卡最终都会抽象成“块设备”一个能按固定大小读写数据块的设备。块设备之上再套文件系统。这个层次关系是后面所有内容的地基。2. 固件、文件系统、用户代码Flash 里的“三块地盘”2.1 先有固件才有文件系统你烧录MicroPython固件时实际上是把一整套解释器代码写进了Flash的固定区域这部分叫固件区。固件区前面通常还有Bootloader和分区表。分区表就像一张地图告诉主控Flash的哪一段是Bootloader、哪一段是固件、哪一段留给文件系统。以常见的ESP32为例烧录后的Flash默认会划分出nvs保存校准数据和密钥、otadata、OTA应用区以及vfs文件系统区。文件系统区专门用来存放boot.py、main.py和你后面自己创建的任何文件。不同板子、不同固件版本的分区大小差别很大这也是为什么同样写着“8MB Flash”的两块板子实际能存文件的容量可能完全不一样。有些固件的分区表把文件系统分区命名为spiffs这是历史遗留的叫法实际文件系统类型由MicroPython源码里的初始化代码决定现在主流已经迁移到littlefs。2.2 boot.py 和 main.py开机代码的寻找路线MicroPython上电后固件里的解释器会做两件事先尝试挂载根文件系统再依次自动执行根目录下的boot.py和main.py。boot.py通常用来做硬件初始化、挂载SD卡、设置网络main.py则放你的业务主逻辑。这样拆开的好处是即使main.py写崩了你还能在REPL里手动修复因为boot.py不一定受影响。如果main.py一启动就崩溃MicroPython大多会停在REPL把问题暴露出来。我自己习惯在改代码之前先把“最后一次正常运行”的版本备份到PC上不然改崩了想回滚只能重新敲这个教训很深刻。2.3 用os模块查看真实存储布局想知道板子现在文件系统多大、还剩多少空间不要靠猜直接在REPL里跑import os info os.statvfs(/) # f_bsize 是块大小f_blocks 是总块数 total info[0] * info[2] free info[0] * info[3] print(total bytes:, total) print(free bytes :, free)os.statvfs(/)返回的是一个元组不同位置的数字含义在官方文档里有说明最常见的用法就是拿它算容量和剩余空间。要查整颗Flash总大小ESP32可以用import esp; esp.flash_size()STM32则用pyb.flash_size()。有些固件还提供flashbdev模块里面带bdev对象能直接告诉你在哪里、有多大。这些命令组合起来你就能知道自己手里这块板子的“硬盘”到底是什么状态。2.4 为什么删除文件后剩余空间“没回来”我见过很多新手在论坛问删了一个大文件为什么statvfs看到的剩余空间还是没变如果你在删除后马上同步文件系统已经标记空间为可用但块设备层的缓存或littlefs的垃圾回收还没及时体现出来统计就会滞后。最直接的验证办法是调用os.sync()或者把文件系统重新挂载一下再看。这里其实和电脑上“删除文件后存储没释放”的现象是同一个逻辑数据不是立刻被物理抹掉文件系统只是把空间标记成可复用。延后释放很正常但如果长时间不释放那就要查是不是有另一个打开的文件句柄还占着空间或者日志策略里存在大量碎片文件。3. VFS让“文件”这个抽象概念统一一切设备3.1 没有VFS的时候有多痛苦假设没有抽象层你要操作内置Flash就得用Flash的读写函数要操作SD卡又得用SD卡的读写函数。每换一种存储介质所有代码都得改。MicroPython用VFSVirtual File System解决了这个问题VFS把“文件”这个东西抽象成统一接口不管底层是Flash、SD卡还是你自己实现的一块内存用户看到的都是open()、read()、write()、os.listdir()。这不是MicroPython自己的发明操作系统几十年一直这么干。但MicroPython的VFS非常轻量它只负责两件事管理挂载点以及把路径请求分发到对应的文件系统实现。3.2 挂载点、文件系统驱动、块设备三层关系VFS模型可以拆成三层最底层是块设备Block Device负责和物理介质打交道中间层是文件系统驱动如FAT、littlefs负责把块设备上的数据组织成文件和目录最上层是VFS挂载点负责把某个文件系统挂到路径上。你在MicroPython里执行os.mount(sd, /sd)意思就是把SD卡的块设备交给文件系统驱动并且让/sd这个路径指向它。之后你写open(/sd/data.txt, w)VFS会先解析出/sd的挂载点再调用对应文件系统驱动去处理。而/根目录下面的main.py走的也是同一套路径只不过它挂载的是内置Flash的文件系统。3.3 自己做一个块设备直观理解文件系统从零诞生想真正理解这层抽象最直接的办法是自己实现一个块设备。这里的块设备可以是一块RAM区域也就是传说中的“内存盘”。import os class BlockDev: def __init__(self, n): self.data bytearray(n * 512) def readblocks(self, block_num, buf): for i in range(len(buf)): buf[i] self.data[block_num * 512 i] def writeblocks(self, block_num, buf): for i in range(len(buf)): self.data[block_num * 512 i] buf[i] def ioctl(self, op, arg): if op 3: # 块数量 return len(self.data) // 512 if op 4: # 块大小 return 512 bdev BlockDev(128) # 128个512字节的“扇区” os.VfsFat.mkfs(bdev) # 在上面创建FAT文件系统 os.mount(bdev, /ramdisk) # 挂载这段代码的意思是先把一块64KB的内存初始化为块设备然后让FAT文件系统把它格式化成一张“磁盘”最后挂到/ramdisk目录。执行完你会发现可以往里面读写文件但一掉电数据就没了因为内存不保电。这个练习虽然不能持久化却把块设备和文件系统两个概念拆得干干净净块设备提供扇区读写能力文件系统提供名字、目录、空间分配能力。4. FAT 还是 littlefs两个主流文件系统的选型博弈4.1 FAT为了和PC兼容而存在的老将FAT源自上世纪个人电脑的软盘时代结构简单Windows、Linux、Mac都能直接识别。MicroPython之所以还在不少板子上采用FAT一个重要原因是方便你把SD卡从板子上拔下来插到电脑上文件直接就能读不需要任何额外工具。不过FAT的缺陷也很明显没有掉电保护。写入过程中断电FAT表可能和实际数据不一致轻则出现损坏文件重则整个目录结构乱掉。FAT的元数据集中分布反复修改同一个目录会导致这些区域的Flash磨损特别快也没有设计磨损均衡。再加上FAT里多字节字段按小端字节序组织这在和PC交互时是优点但在嵌入式世界里只是历史包袱。4.2 littlefs为“随时可能断电”而生的现代方案littlefs是ARM为嵌入式设备设计的开源文件系统目标就是解决FAT在Flash上的两大痛点掉电安全与磨损均衡。它的设计思路可以粗略理解为写入新数据时不原地覆盖而是先写入一个新块等数据完整确认后再把元数据指针切换过去元数据本身也是成对保存提交时带校验任一瞬间掉电下次挂载都能回滚到最近一个完整状态。这套机制换来的是更强的可靠性代价是运行时的CPU和内存开销比FAT大PC也没法直接识别。所以littlefs非常适合作为板载Flash的文件系统系统永远知道哪些文件是完整的断电不会把main.py写烂。MicroPython里它分VfsLfs1和VfsLfs2两代目前绝大多数官方固件推荐使用VfsLfs2。4.3 场景化选型不是越新越好而是匹配使用场景我整理了一份简单的选型参考场景推荐文件系统原因板载Flash存main.py和配置littlefs掉电安全、磨损均衡优先保护系统关键文件SD卡存日志或数据需要PC读取FATPC直接兼容处理起来方便大量高频写入日志看情况优先用SD卡FAT并批量写入板载Flash应减少频繁写临时缓存掉电无所谓FAT/littlefs均可用内存盘甚至不用文件系统实际项目里我见过最难受的配置是把日志写到板载Flash的FAT文件系统上每几秒就open/write/close一次。这样既容易把Flash磨损死又容易在断电时损坏FAT。后来改成“日志存SD卡FAT关键配置存Flashlittlefs”之后问题一下子少了非常多。5. 实操指南格式化、SD卡挂载和落盘同步5.1 格式化板载文件系统两种可行路线格式化板载文件系统属于“高危动作”因为它会把里面的boot.py、main.py全清掉。但有时候你又必须做比如文件系统逻辑损坏、无法挂载或者想从FAT换成littlefs。一个可靠思路是使用官方工具mpremote连接板子在REPL里执行格式化。以常见的ESP32等固件为例mpremote connect /dev/ttyUSB0 exec import os, flashbdev; os.VfsLfs2.mkfs(flashbdev.bdev)这个命令会调用flashbdev模块里的块设备对象在上面创建littlefs文件系统。执行完重启板子会重新生成空的文件系统。如果你手里的固件没有flashbdev这个模块或命令报错那就换一条更暴力的路线直接用esptool.py erase_flash擦除整片Flash再重新烧录MicroPython固件。这样所有分区都会重置文件系统也一定会被重建。这里必须再强调一遍格式化之前先把板子里所有要留的文件备份出来否则一路清空想后悔都没机会。5.2 给板子挂SD卡SD卡的价值在于容量大、可拆卸、可以快速插到PC上分析数据。硬件接法一般用SPI模式或SDMMC模式不同主控接线也不一样。ESP32用SDMMC时可以这么挂import os, machine sd machine.SDCard(slot2, width4) os.mount(sd, /sd) print(os.listdir(/sd))SPI模式的SD模块更常见接好MOSI、MISO、SCK、CS之后配合sdcard驱动库from machine import SPI, Pin import sdcard, os spi SPI(1, baudrate4000000, polarity0, phase0) sd sdcard.SDCard(spi, Pin(5)) # Pin(5) 是CS os.mount(sd, /sd)这块有个细节值得注意SD卡一般格式化成FAT不是为了什么“高级特性”而是为了你能把卡拔下来插电脑直接看数据。挂载SD卡后/sd下读写文件的方式和根目录一模一样因为VFS把差异全部抹平了。如果你希望上电自动挂载把挂载逻辑放进boot.py并做好异常处理避免SD卡没插时开机直接报错卡住。5.3 sync什么时候调、为什么调、副作用是什么MicroPython的open()和write()不是每次调用都直接把数据写进Flash。为了性能文件系统可能会有缓存区真正落盘要等close、flush或sync。close能保证你这一次写入的文件结构完整但如果你要保持文件长期打开、边采集边写那就得手动调os.sync()把缓存刷下去。你可能会问那我每写一行就sync一次行不行能行但代价很大。Flash先擦后写高频sync会让文件系统频繁操作底层块设备磨损加速写入速度也明显变慢。正确的姿势应该是按业务节奏批量写入比如采集10条数据、或每隔1秒累积成一小块再write一次在需要保证数据不丢的关键节点比如刚写完配置、刚记录完异常才调用sync。别看这个习惯小它直接决定设备是“偶尔丢几秒数据”还是“经常坏文件系统”。如果写的是日志类数据我更推荐用SD卡FAT并且按天生成文件名比如log_20250617.csv。这样单文件不会无限膨胀查找方便删除旧文件也不会影响当前日志。6. 我踩过的那些“存储玄学”坑和排查链路6.1 “删除文件后空间没回来”的完整排查过程这个问题我在开发日志存储功能时真真切切遇到过。现象是设备上删除了一批旧日志os.statvfs(/)显示可用空间还是老样子。第一次遇到时我甚至怀疑是文件系统坏了。排查链路是这样的先执行os.sync()再读statvfs看空间是否恢复。如果没恢复检查是不是有任务还持有被删文件的句柄。MicroPython里如果你open了一个文件没有close删除后文件系统可能认为它还被占用空间不会完全释放。再考虑是不是文件系统碎片或littlefs的垃圾回收还没做。可以os.umount(/)再重新挂载一般会触发清理和重新统计。如果依然没恢复用工具把文件系统整个导出来检查是否大量空间被坏块或隐藏的临时文件占掉。最后发现我的问题出在“删除后没有sync”上加上统计模块读的是缓存的容量信息刷新一下就好了。这个排查思路对PC上的“删除后存储还在”也有参考价值——先看是不是回收站再看有没有进程占用再看文件系统是否需要整理。6.2 “写了一半文件重启后文件丢失”的根因另一个高频现象是程序运行中突然断电再上电发现日志文件变成0字节或者压根不存在。很多人第一反应是Flash坏了其实大概率是写入流程没走完。如果你的代码是f.write()之后马上断电而数据还在缓存层那确实会丢。解决方式就是前面说的在关键点os.sync()或者把写入改成原子式先写临时文件等完整写完后用os.rename()替换旧文件。这样即使中途断电旧文件依然是上一次的完整版本。这种“临时文件rename”的思路在配置类数据上特别管用。我自己做设备配置存储时向来都是config.tmp写完后os.rename(config.tmp, config.json)再配合sync。文件系统只要不是彻底损坏总能保证至少有一个可用的配置文件。6.3 格式化后“找不到设备”或“REPL都不见了”格式化板载文件系统时最怕的是写到一半断电或者用错了块设备对象。出现这种情况板子可能表现为上电后卡在启动阶段或者无法挂载根文件系统。不要慌这通常不是硬件损坏而是文件系统元数据写烂了。处理路径很明确如果能进REPL尝试用mpremote执行格式化如果完全连不上就用esptool整片擦除后重刷固件。重新刷完固件之后文件系统会回到初始状态但boot.py和main.py也没了相当于一台“全新设备”。所以再次强调任何涉及板载文件系统的操作之前先把文件全部备份到PC。6.4 为什么日志写久了“性能越来越差”最后说一个不那么致命但很磨人的问题日志写了几百个小时后明显感觉文件读写变慢了。FAT文件系统下文件长期append容易导致数据链跨多个分散的区域形成碎片littlefs则可能在垃圾回收时做大量块搬移同样拖慢性能。我的处理经验是能写SD卡就不要写板载Flash日志按时间或大小滚动定期清理旧日志如果必须要长时间写板载Flash尽量把数据批量累积成较大的块再写减小碎片和磨损。把这些策略加进去之后我手上的设备连续运行几个月的稳定性比之前强了一个量级。最后再送你一个我自己的习惯任何板载文件系统的大动作之前先mpremote cp -r :/ ./backup把整盘拉到电脑上。备份成本只有几十秒但能省掉无数次想砸板的冲动。存储和文件系统不像写代码那样有即时反馈它的坑都埋在时间后面。你在这上面多花的十分钟思考设备会替你还回来的。
返回列表