ARTICLE DETAIL

资讯详情

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

MicroPython存储与文件系统解析:Flash分区、掉电安全与LittleFS实践

MicroPython存储与文件系统解析:Flash分区、掉电安全与LittleFS实践 1. 先搞清楚MicroPython的存储到底是怎么组织的我接触MicroPython的头两年一直以为它跟Arduino一样代码是“烧”进去的数据是“存”在变量里的。直到有一次做了一个需要掉电保存配置的项目才突然发现事情没那么简单MicroPython压根不是把代码和数据塞进一整块Flash里随便用而是把Flash分成了好几个区域各干各的活。这个分区结构是所有存储和文件系统问题的根本。1.1 固件分区Bootloader、固件区、文件系统区MicroPython的Flash空间在出厂时就被规划好了。以最常见的ESP32为例整个Flash通常被切成三个大区Bootloader区、固件区存放MicroPython解释器、文件系统区默认是FAT或LittleFS格式。Bootloader负责上电引导固件区放的是MicroPython的运行时二进制而文件系统区才是你平时写代码时能直接用到的“硬盘”。我刚开始总觉得“我往板子里写main.py这个文件到底是存在哪里的”其实就是存在这个文件系统分区里。当你用Thonny或者ampy上传代码时工具做的并不是“烧录”动作而是把文件写到文件系统分区然后复位让MicroPython去加载main.py。理解这层逻辑之后很多奇怪的现象就能解释了比如你用esptool全片擦除后之前写的所有文件全部消失但重新烧录固件后板子又能用因为擦除的是整个Flash固件和文件系统都没了。这里要补充一个实际项目里非常关键的点不同开发板的分区默认大小不一样。ESP8266的默认文件系统空间只有1MBESP32通常是1.5MB左右而像RP2040这种板子Flash有2MB甚至更多实际可用空间可能有1.5MB到几十MB视板子设计而定。如果你的项目需要存大量日志或者图片这个默认分区大小很可能会成为瓶颈。1.2 VFS为什么MicroPython能像操作电脑文件一样操作板子MicroPython内置了一套叫VFSVirtual File System虚拟文件系统的抽象层。它的作用说白了就是不管底层存储介质是Flash芯片、SD卡还是SPI RAM对上层代码来说暴露的都是统一的文件操作接口。你调用open()、os.listdir()、os.remove()底层可能是LittleFS在处理Flash扇区也可能是FAT在处理SD卡的簇但你的代码根本不用关心。这个设计让我想起电脑上的“盘符”概念Windows的C盘、D盘对应不同的物理硬盘但你在资源管理器里看到的操作方式是一样的。VFS就是MicroPython的“资源管理器”把不同的存储介质统一成一种操作体验。这也是为什么你可以用几乎一样的代码同时操作内置Flash和外部SD卡只需要挂载不同的路径。2. Flash存储的核心底层机制为什么文件系统不是你想的那样简单很多新手会犯一个认知错误觉得Flash跟机械硬盘一样可以随意在某一个字节上改数据。实际上Flash存储器有一个极其反直觉的特性——写之前必须先擦除而且擦除的最小单位通常是一个扇区sector或块block不是单个字节。2.1 为什么Flash不能直接改写一个字节Flash存储器的工作原理是擦除操作会把一个扇区内的所有位变成1即0xFF而写入操作只能把1变成0。这意味着你想从0x0F改成0xF0不能直接在这个地址上写因为0x0F到0xF0涉及把0变成1的操作这在物理上是不允许的。你必须先把整个扇区擦除变成0xFF然后再重新写入目标数据。放在MicroPython的场景里问题就变成了如果你频繁修改一个只有几百字节的配置文件而文件系统每次都在Flash上做“擦除整个扇区再重写”的操作那么这个扇区会迅速老化。Flash的擦写寿命是有限的常见NOR Flash的额定擦写次数在1万到10万次之间。对于长时间运行的设备这就是一个实实在在的寿命风险。我举一个实际翻车案例有个朋友做了一个温湿度记录仪每分钟往文件里追加一条数据并调用os.sync()强制落盘。结果设备跑了两个多月文件系统开始频繁报错最终无法挂载。原因就是他把Flash当成机械硬盘用高频小数据写入导致某个扇区提前报废。后来改成每10分钟批量写入一次寿命立刻延长了几十倍。2.2 掉电保护与文件损坏一次突然断电的教训之前我在调试一个户外设备时遇到了一个特别头疼的问题设备在写入日志过程中突然断电重新上电后日志文件的内容要么变成一堆0xFF要么整个文件打不开。后来分析才发现这是文件系统写入过程中掉电导致的经典问题。在普通的桌面系统中文件写入会先经过系统缓存再异步写回硬盘所以掉电最多丢一点数据。但在MicroPython这种嵌入式环境中文件系统直接操作Flash写入过程中如果正好在更新目录项或文件分配表时断电就可能导致整个文件系统元数据损坏严重的会让整个分区无法挂载。当时的解决思路有几个一是改用LittleFS作为文件系统它有掉电保护机制使用日志结构和写时复制技术即使突然断电旧的元数据还能保留新写入的数据完整顶多丢当前正在写的那一部分二是自己在应用层做数据保护比如写入时先写到一个临时文件写完后再用os.rename()原子替换这样即使中途断电也永远不会出现半个新文件覆盖旧文件的情况。事实证明LittleFS的坏块管理也值得一提它自带坏块检测和绕过机制比FAT更适应Flash介质。但LittleFS也不是万能如果本身硬件Flash已经达到寿命临界点再好的文件系统也救不回来。2.3 块大小与扇区擦写一个常被忽略的参数很多人在用os.statvfs()查看文件系统信息时只关心“还剩多少空间”但很少有人注意里面的f_bsize和f_frsize字段。这两个字段表示文件系统的块大小直接决定了文件占用的最小物理空间。举个例子如果块大小是4096字节你写一个只有10字节的文件它也要占整整4KB的空间。我在一个SD卡数据记录项目中发现明明写了几百个小文件卡的空间却消耗得飞快就是被这个“最小分配单位”坑了。如果你在存大量小文件建议把多个小数据拼成一个大的JSON或二进制文件减少空间浪费同时也能减少写入次数延长Flash寿命。3. 文件系统选型LittleFS与FAT的对比与取舍MicroPython官方固件在ESP32等主流芯片上默认使用LittleFS但同时也支持FAT格式。很多人用Thonny往板子上传文件时没细想过——为什么是LittleFS而不是我们更熟悉的FAT32这里面的取舍非常值得讲清楚。3.1 LittleFS的优势损耗均衡与掉电安全LittleFS是ARM专门为嵌入式设备设计的文件系统它的核心优势有三个掉电安全、损耗均衡、低内存占用。掉电安全这个词可能有点抽象我用一个日常场景解释假设你在写一篇长文章电脑突然断电。坏的文件系统可能会让你整篇文章都打不开而LittleFS因为采用的是“写时复制”和“日志化”机制断电后最多丢最后几段前面的内容都还在。它通过两阶段提交来保证元数据的一致性先写内容副本再原子地切换指针。如果你的应用涉及频繁写入比如系统日志、参数配置、用户数据选LittleFS比FAT安全得多。损耗均衡是另一个杀手锏。前面说过Flash的寿命跟擦写次数强相关LittleFS通过动态、静态磨损均衡算法尽可能让所有扇区的擦写频率接近。这样就不会出现“某个扇区被写烂了其他扇区还是新的”这种尴尬局面。实测下来一个使用LittleFS的ESP32开发板在每5分钟写一条日志的负载下跑了一年多文件系统依然稳定。3.2 FAT文件系统的适用场景FAT在MicroPython里也没被抛弃尤其是在SD卡场景中。为什么因为SD卡出厂格式就是FAT32直接兼容意味着你可以在电脑上读取MicroPython写入的数据反过来也能在板子上读电脑放进去的文件。如果你做的是一个数据记录仪需要定期拔卡插到电脑上看数据FAT格式显然更方便。FAT的缺点也对得起它的简单没有断电保护机制突然断电可能造成文件分配表和目录项损坏没有磨损均衡频繁写入会对固定区域造成压力最大文件大小受限于FAT32的4GB上限。还有一个实际体验问题FAT在大量小文件情况下簇的利用效率相对较低空间浪费明显。3.3 如何切换或选择文件系统选择哪个文件系统说到底就看你的场景纯板载存储、数据可靠性要求高、需要频繁改写小文件选LittleFS。需要和电脑交换数据、使用SD卡、文件体积大且不频繁修改选FAT。对数据可靠性要求很高的工业级场景还可以考虑外接SPI Flash并叠加外部FRAM或EEPROM做双备份文件系统层面只是基础防线。如果你拿到一块板子想看看当前用的是哪个文件系统可以打开MicroPython REPL运行import os os.mount()这个命令会列出当前已挂载的文件系统类型和挂载点。如果显示的是VfsLittlefs2说明用的是LittleFS如果是VfsFat则是FAT。想重新格式化文件系统可以用os.VfsLittlefs2.mkfs(bdev)或os.VfsFat.mkfs(bdev)但注意这会清空所有数据。4. 实操从查看分区到文件读写的完整记录理论讲再多不如现场跑一遍。我以一块ESP32-S3开发板为例完整演示一次从查看存储分区到实际读写文件的过程顺便把代码里每个关键参数的含义说清楚。4.1 查看文件系统状态与剩余空间第一步先确认系统当前能识别哪些存储设备、文件系统是什么样的。在REPL里执行import os # 查看已挂载的文件系统 os.mount() # 查看根目录下的文件列表 print(os.listdir(/)) # 查看文件系统统计信息总空间、剩余空间、块大小 stat os.statvfs(/) print(Block size:, stat[0]) print(Total blocks:, stat[2]) print(Free blocks:, stat[3]) print(Total space (bytes):, stat[0] * stat[2]) print(Free space (bytes):, stat[0] * stat[3])注意os.statvfs()返回的是一个元组不同MicroPython版本的索引含义可能略有不同但[0]是块大小、[2]是总块数、[3]是可用块数这三个是最常用的。我一般在写存储类代码前都会先打印一下Free space (bytes)因为很多报错其实是空间不足导致的而不是代码逻辑问题。4.2 实际写入文件的完整过程与底层发生了什么下面是一段最普通的文件写入代码但我要把它背后发生的事情一步步拆开with open(/data.txt, w) as f: f.write(hello micropython, this is a test.\n)这一步在底层至少发生了这些事open(/data.txt, w)会让VFS层在目录中查找或创建data.txt的文件条目。如果文件已经存在会先清空长度。写入字符串时VFS把数据从用户空间拷到文件系统缓冲先写文件数据区。如果启用了日志结构LittleFS写入会在一个新块上进行旧数据块先不删除保持掉电安全。文件关闭时LittleFS才把目录项、文件大小、元数据更新到位完成原子切换。所以使用with语句是很重要的它能保证文件对象被正确关闭。如果你用f open(/data.txt, w)写完后忘了f.close()数据很有可能只停留在缓冲区断电后直接丢失。关于os.sync()这个函数会强制把缓冲区的数据刷到Flash上。对可靠性要求高的关键数据写入后调用一次os.sync()很有必要代价是速度会慢不少因为要等待物理擦写完成。一个经验是普通日志数据不必每次都同步可以攒一批再sync但对配置参数、系统状态这类必须掉电不丢的数据务必立即同步。4.3 低层直接操作Flash模块化存储的另一种思路有时候你需要的不是文件而是一块能存一个整数或几个字节变量的空间。MicroPython提供了flashbdev模块让你可以直接操作Flash块设备绕开文件系统。这在做少量配置存取的场景下非常高效。import flashbdev # 访问底层块设备 bdev flashbdev.bdev # 读取第0块的前16个字节 data bdev.readblocks(0, bytearray(16)) print(data)不过说实话直接操作块设备对新手很容易搞坏文件系统我不建议在正式项目里这么做除非你非常清楚自己在干嘛。更推荐的做法是用文件系统的json或pickle模块做结构化配置存储简单、安全、可维护。比如保存一个配置字典import json config {ssid: my_wifi, interval: 30} with open(/config.json, w) as f: json.dump(config, f) with open(/config.json, r) as f: data json.load(f) print(data[ssid])这种方式的优势是数据结构清晰修改方便掉电后重启能原样恢复而且在电脑上也能直接打开看内容。5. 常见文件系统问题排查与避坑技巧这部分是我做MicroPython存储项目时踩过坑的汇总希望能帮你避开同样的问题。5.1 报错OSError: [Errno 28] No space left on device这个错误是典型的磁盘满。但有一个坑有时候你用os.listdir()看文件感觉没多少大文件为什么空间还是不够前面提到的“最小分配单位”就是罪魁祸首之一。大量小文件会让每个文件都占满一个或多个块空间利用率很低。排查步骤先看os.statvfs(/)的剩余空间确认是否真的满了。如果剩余空间很小删掉不用的临时文件。如果有很多日志文件用循环批量删除早期的文件。如果小文件特别多考虑合并存储。注意删除文件后LittleFS的空间不会立即“看起来”变多因为空间是动态管理的但可用块数统计会实时更新。不要纠结于du输出直接看剩余空间数值。5.2 文件系统挂载失败OSError: [Errno 84] EROFS这个报错的含义是“只读文件系统”它通常意味着文件系统损坏或者Flash驱动检测到介质不稳定。在开发阶段最稳妥的办法是重新格式化文件系统但会清空所有文件所以生产环境的设备要设计成可远程清空或标记重要参数做双备份。import os import flashbdev try: os.VfsLittlefs2.mkfs(flashbdev.bdev) os.mount(flashbdev.bdev, /) print(文件系统已重建并重新挂载) except Exception as e: print(重建失败:, e)如果连这个都执行不了考虑用esptool做全片擦除后重新烧录固件一般都能救回来。5.3 掉电后配置丢失不只是sync的问题之前有个用户问我代码里写了os.sync()为什么掉电后配置还是丢了排查后发现问题出在他每次写配置时直接把原来文件覆盖了中途掉电导致新文件和旧文件都不可用。解决办法是“原子写”策略先把新数据写入临时文件/config.tmp。调用os.sync()确保临时文件安全落盘。再执行os.rename(/config.tmp, /config.json)完成替换。再执行一次os.sync()把目录更新也落盘。这样即使掉电最多是临时文件残留主配置文件始终是完整的。这个方法在我的项目里验证过很多次效果稳定。5.4 Flash寿命不够换存储介质是唯一出路如果你已经用了LittleFS、也做了原子写、还是频繁出问题可能真是板载Flash扛不住了。这种时候就该考虑外扩存储。我常用的外扩方案有两种SPI Flash芯片W25Q系列和TF卡。SPI Flash适合对功耗、体积有要求的场景MicroPython有现成的spiram或SPIFlash驱动库TF卡则通过SD卡模块连接用os.mount(sd, /sd)挂载。import os from machine import Pin, SPI import sdcard spi SPI(1, baudrate20000000, polarity0, phase0, sckPin(18), mosiPin(23), misoPin(19)) sd sdcard.SDCard(spi, Pin(5)) os.mount(sd, /sd) print(os.listdir(/sd))挂载成功后你就可以像操作内置Flash一样操作SD卡唯一的区别是前缀路径变成了/sd。注意SD卡用FAT格式拔卡前记得os.sync()不然缓存里的数据很可能丢。6. 数据持久化之外文件系统的一些进阶玩法最后聊几个文件系统的高级用法这些技巧能让你的项目更稳、更专业。6.1 用日志写入策略延长Flash寿命为了延长Flash寿命可以借鉴工业设备里常用的“日志轮转”思路不是把数据永远写到一个文件里而是按天或按小时生成新文件并定期清理最老的文件。这样能让写入压力分散到不同的物理扇区避免局部过热。一个简单的轮转策略import os def append_log(data, max_files10): # 找到当前日志编号 idx 0 while f/log_{idx}.txt in os.listdir(/): idx 1 # 写入新日志 with open(f/log_{idx}.txt, w) as f: f.write(data) # 删除最老的日志保留最多max_files个 files sorted(x for x in os.listdir(/) if x.startswith(log_)) while len(files) max_files: os.remove(/ files.pop(0))这种做法的好处是单文件不会无限膨胀旧数据自动淘汰写入也均匀分布特别适合传感器数据采集、设备状态上报之类的场景。6.2 程序崩溃时自动恢复文件系统在长时间运行的设备上最好在启动时做个文件系统健康检查。一旦发现挂载失败或者关键文件缺失就自动重建文件系统并恢复默认参数。这在远程设备维护时特别有用能省下去现场刷机的麻烦。import os import flashbdev def ensure_fs(): try: os.mount(flashbdev.bdev, /) os.listdir(/) except Exception: os.VfsLittlefs2.mkfs(flashbdev.bdev) os.mount(flashbdev.bdev, /) with open(/config.json, w) as f: f.write({default: true}) print(文件系统损坏已自动重建) ensure_fs()当然这只是一种兜底方案。如果设备硬件有严重问题所有数据都已丢失这份代码能保证至少设备还能启动而不是变砖。6.3 使用RAM盘做临时高速存储有些MicroPython移植版本支持用内存映射块设备创建RAM盘速度极快适合存放临时缓存数据重启即清空。这个用法比较冷门但在一些性能敏感的场景比如图像采集后再压缩处理非常实用。你可以用os.VfsFat.mkfs(ram_block)把一块内存映射成FAT文件系统然后挂载到/tmp。需要注意RAM盘占用的内存会从系统的堆内存里扣掉使用时要控制好大小。从我个人的项目经验来说MicroPython的存储与文件系统恰恰是最值得花时间弄懂上层协议之外的底层知识。很多设备用着用着突然宕机、重启后配置丢失、存储空间莫名其妙少了一块根源都在这里。把Flash的物理特性、文件系统的设计取舍、写入策略的细节搞清楚这些坑基本都能提前避开。如果你正在做一个需要长期稳定运行的MicroPython项目我强烈建议在动手写业务逻辑之前先把存储方案设计好——这个前置投入后面会省下无数个排查问题的深夜。
返回列表