MicroPython存储与文件系统底层原理:Flash、LittleFS与VFS完全指南
2026/9/11 2:20:39 网站建设 项目流程

先说个真实经历。早年刚玩 ESP8266 的时候,我为了一个配置参数,要把整份脚本重新烧进 Flash,改一个 IP 地址都要重新擦除再上传,那阵子真是改到崩溃。后来我终于摸清楚了 MicroPython 设备上的存储和文件系统机制,才明白问题是出在“它到底把文件放在哪”这件事上。MicroPython 烧进板子之后,你就拥有了一台带文件系统的小电脑:固件占掉一部分 Flash,剩下的一块 Flash 空间会被格式化成文件系统,挂载成“/”目录。你可以像在电脑上一样读写文件、建目录、存日志、存配置,但是这套机制的底层,跟普通电脑上的 NTFS/ext4 完全不是一回事。

很多新手文章会把 open()、write()、close() 讲得跟 PC 编程一样,结果一断电、一擦写、一格式化就原形毕露。这篇文章我要讲的是 MicroPython 存储和文件系统的底层原理,不是让你背概念,而是把这些知识点揉进实际场景里:Flash 是怎么被切分的、文件系统在 Flash 上是怎么存数据的、为什么经常掉电会导致文件损坏、怎么用 os.mount() 把 SD 卡或外部 SPI Flash 接入系统,以及我踩过的一些坑。全程不用太高深的硬件知识,跟着走一遍,你会比大多数只会写 open() 的人都理解得更深。

1. MicroPython 存储体系全景:固件、Flash、RAM 文件系统

1.1 一块 Flash 是怎么被切分的

大部分常见的开发板,比如 ESP32、ESP8266、RP2040、STM32F4 系列,核心存储介质都是 NOR Flash,只是位置不同。ESP32 的 Flash 在芯片外部,通过 SPI 接口连接;RP2040 的 Flash 在芯片外面,QSPI 接口;STM32 则很多把 Flash 做到芯片内部。

MicroPython 固件烧录的时候,并不是把整个 Flash 都拿来当文件系统。以 ESP32 为例,Flash 顶部或底部的某个区域放着 bootloader、分区表、MicroPython 固件本体,剩下的空间才会被划分出来作为“VFS 存储区”,也就是我们平常在 mpremote 或 Thonny 里看到的那几个文件存放的地方。这个区域在不同固件版本里可能叫 “vfs”“spiffs”“littlefs”,命名已经不重要了,你只需要知道它是一块被格式化成文件系统的 Flash 分区。

打个比方,这就像你买了一块 4GB 的手机存储卡,但系统占了 3GB,剩下的 1GB 才是你能用来拍照的“用户空间”。MicroPython 设备也一样,芯片标注的 Flash 大小不等于你能用的存储大小,固件越大、分区越多,可用空间就越小。实测一块标注 4MB Flash 的 ESP32 模块,MicroPython 默认格式化后文件系统空间常见只有 1.5MB 到 2MB。

1.2 RAM 盘和临时文件系统的角色

除了 Flash 分区,部分 MicroPython 移植版还支持 RAM 盘(如 STM32 某些移植版把一部分内存当作块设备),挂载后可以直接在上面读写临时文件。RAM 盘的特点是读写快、掉电即失,适合保存运行时的临时数据、缓存或者需要频繁擦写的中间文件,不适合存放配置和日志。

在电脑上,你不能把“C 盘满了”和“内存不够用”混为一谈,因为两者的寻址方式、读写速度、掉电行为完全不同。MicroPython 设备上也是一样:Flash 上的文件相当于持久化存储,RAM 盘上的文件是临时存储。很多人把大量日志直接写到 Flash 上,一天下来就可能把 2MB 空间写满,时间一长 Flash 的磨损问题也会暴露出来。如果你只是暂存几个变量,没必要动用文件系统,用 ujson 序列化后放进 RAM 还是更合适。

1.3 不同开发板的存储布局有什么差异

下面是我用过的一些常见板子上的存储情况:

开发芯片/板卡Flash 来源典型文件系统用户可用空间特点
ESP32外部 SPI NOR FlashLittleFS分区大小由固件分区表决定,4MB 芯片约 1.5MB
ESP8266外部 SPI NOR FlashLittleFS 或 SPIFFS1MB~2MB,老固件是 SPIFFS
RP2040(树莓派 Pico)外部 QSPI NOR Flash标准 MicroPython 文件系统1MB~2MB,取决于固件
STM32F4 系列内部 FlashFAT/内部 Flash 文件系统空间受芯片容量限制
各种开发板 + SD 卡SD 卡FAT32从几十 MB 到几十 GB 都能用

RP2040 和 ESP32 的另一个关键是:它们都支持在运行时把外部存储设备(如 SD 卡、SPI Flash 模块)挂到 VFS 上,这就是我们在后续章节要重点讲的 os.mount()。理解了这个机制,你就不会纠结为什么板子 Flash 看起来很大但实际能用很少,因为你可以用一块 SD 卡把它“扩容”成几百 MB 的文件系统。

2. 文件系统的底层到底在干什么:VFS、块设备和目录树

2.1 VFS 是什么,它解决了什么问题

MicroPython 官方文档里有一个概念叫 VFS(Virtual File System),直译是“虚拟文件系统”。它定义了一套统一的接口,不管是 Flash 上的 LittleFS、SD 卡上的 FAT,还是 RAM 盘,最终都通过同一套 read()、write()、open()、listdir() 来访问。

这有点像是电器上的 USB 接口标准:不管你插的是 U 盘、移动硬盘还是读卡器,电脑只要识别出它是 USB 设备,就能统一读写。VFS 让 MicroPython 也能做到“插上什么文件系统都统一处理”。当你调用 open('/sd/data.txt', 'w') 时,路径最前面的“/sd”会告诉 VFS 去哪个挂载点找对应的块设备。

VFS 的关键概念有:

  • 挂载点(mount point):一个路径,比如 “/”、“/sd”,代表某个文件系统被挂载到目录树的哪个位置。
  • 块设备(block device):提供读、写、擦除、同步等基本操作的低层存储单元。
  • 文件系统驱动(filesystem driver):在块设备之上实现目录、文件、权限管理,比如 LittleFS 驱动、FAT 驱动。

很多教程直接教你怎么用 open(),但对 VFS 这个概念一笔带过。如果不懂 VFS,你在换了一块 SD 卡或者想用外部 Flash 扩展存储的时候,会四处碰壁。因为多了一个设备,你要做的事不是“在代码里写路径”,而是先把设备挂载进 VFS 的目录树里,然后才能通过路径去访问。

2.2 块设备为什么是文件系统的地基

文件系统最终要落在某种存储介质上,而存储介质的访问方式通常被抽象成一块一块的区域,而不是一个一个字节。传统机械硬盘的最小读写单位是扇区(512 字节或 4096 字节),Flash 甚至更特殊,擦除的基本单位是“块”(Block),读取和编程的最小单位可能是“页”(Page)。

MicroPython 里的块设备必须实现 readblocks()、writeblocks() 和 ioctl()。ioctl() 告诉上层文件系统这个块设备有多少块、每块多大,是否支持擦除等。这很像电脑上操作系统通过磁盘驱动去读取硬盘的参数:开始扇区、扇区大小、总扇区数。

我在 ESP32 上用外部 SPI Flash 时,第一步就是写一个块设备驱动类。刚开始我没有真正实现 ioctl() 里查询块大小的逻辑,直接写死成 4096,结果挂载 LittleFS 后只要写入数据就报错,后来才发现是块大小和实际 Flash 参数不一致。这个问题在 PC 开发中几乎不会遇到,因为你不会自己写硬盘驱动,但在嵌入式开发里,块设备就是文件系统的地基,地基参数错了,上面的大楼必塌。

2.3 路径、目录和挂载点是如何关联的

MicroPython 的根目录是“/”,默认挂载的是板载 Flash 上的文件系统。你写 open('config.json') 其实等价于 open('/config.json'),VFS 会在全局挂载表里找到“/”对应的文件系统,然后在这个文件系统里查找 config.json。

如果外部 SD 卡被挂在“/sd”,那你访问 SD 卡上的文件就必须完整写清路径,比如 open('/sd/data.csv', 'a')。反过来,如果你把 SD 卡挂到“/”上,就会把板载的文件系统“盖住”,但原来的文件系统并没有被删除,只是暂时无法从这个挂载点访问。这种机制听上去简单,实际操作时很容易搞混:我就干过把 SD 卡挂到“/” 之后,以为自己把固件文件覆盖了,其实是旧的 root 文件系统被隐藏了,换掉挂载点马上又冒出来。

3. 深入底层:LittleFS 是怎么在 Flash 上存文件的

3.1 为什么默认选择 LittleFS,而不是 FAT

MicroPython 的很多移植版(尤其是 ESP32、LuatOS、RP2040)默认文件系统已经逐步转向 LittleFS。LittleFS 是 ARM 设计的一个专门用于嵌入式设备的文件系统,设计目标非常贴合 Flash 的特性:

  • 掉电安全:突然断电不会导致整个文件系统崩溃。
  • 磨损均衡:写入负载会尽量平均分配到 Flash 各块,避免某一块被写穿。
  • 有限的 RAM/ROM 占用:适合单片机上那点可怜的内存。
  • 元数据可靠性:文件的大小、时间戳、名称等信息采用日志式或 COW(写时复制)方式保存。

而 FAT 是 PC 上传统的文件系统,比如 SD 卡出厂常是 FAT32。FAT 的优点是兼容性极强,电脑上能直接识别,但缺点是它设计给磁盘用,不是给 Flash 用的。FAT 写一个文件经常要反复修改 FAT 表(文件分配表)和目录项,每次修改都会造成 Flash 上的同一块区域被反复擦除,长久下去磨损会特别不均匀,很可能把小范围 Flash 提前写坏。

所以 MicroPython 在设计上做了个很聪明的分层:板载 Flash 一般用 LittleFS,SD 卡保留 FAT,各自发挥优势。如果你硬要把 SD 卡格式化成 LittleFS,那性能可能不好,电脑也不认这类卡。

3.2 LittleFS 的“日志式”写入和掉电保护

LittleFS 内部是一种 copy-on-write 的设计,严格来说叫“写时复制 + 日志结构”的组合。你要修改一个文件的内容,它不会直接把原数据覆盖掉,而是在 Flash 上分配新的块,写入新内容,然后再更新元数据指向新的位置,最后回收旧块。这么做的最大好处是:哪怕你在“更新元数据”这步之前突然断电,旧文件的完整内容依然还在,只是新内容没写进去,文件不会变成一半新一半旧的“杂交怪物”。

很多新手容易把“掉电安全”理解成“我随便断电都不会丢数据”,这是不对的。掉电安全指的是文件系统的结构不会崩溃,不会导致目录损坏、整张卡打不开。但如果你写了个文件,只关了文件没调 fsync(MicroPython 里对应 os.sync()),数据可能还在内存缓冲区里,这时断电,这些数据就是真丢了。文件系统保护的是“结构完整性”,不保证“应用层数据不丢”。

我之前测试过一个日志模块,掉电之后总发现最后几条日志不见了。后来我打开一个文件写满一行,然后立刻断电再上电,反复测了几轮,确认这种“少了几条”的丢数据不是文件系统崩溃,而是我根本没有调用 flush() 或 os.sync()。断电前的一瞬间,数据还躺在 RAM 的缓冲区里。

3.3 擦除块、页和写入放大的基本关系

NOR Flash 的物理特性决定了它不能随便覆盖写:已经写了 1 的区域,想重新写 0,必须先擦除整个块,把块内的所有字节变成 0xFF(也就是“1”状态),然后才能编程写入。这个“先擦除再写”的过程,比普通读写慢得多,而且擦除次数是有限的,一般几千到十万次不等。

LittleFS 为了减少擦除次数,会把小文件的写入尽量合并到一个块里,并且在生命周期内动态移动块的位置,这就是磨损均衡。听起来很高端,实际效果是怎样的?你在 MicroPython 里写一个 100 字节的文件,底层可能并不是整个 4KB 的擦除块都被写了一遍,而是 LittleFS 会挑选一个适合的块,把新数据追加到块内未使用的区域,并记录相应的元数据。

但如果文件频繁追加,你会发现写入会变得慢或出现卡顿。因为 LittleFS 一旦发现当前块的空间不够,必须找到或者擦出新的块,这个过程涉及元数据更新、块分配和擦除操作。我在实践里经常看见有人用文件来存传感器历史数据,每秒一次 open()/write()/close(),很快 Flash 空间见底、写入变慢。这种场景的正确做法,其实是先把数据攒在内存列表中,凑够 1KB 或 4KB 再一次写入,或者用多文件轮转写,减少文件系统的分配开销。

4. 实操解析:把存储能力用出来

4.1 基础文件读取:不要小看 flush() 和 sync()

先看 MicroPython 里最经典的一段日志代码:

import os def append_log(path, line): with open(path, "a") as f: f.write(line + "\n") f.flush() os.sync()

很多人写日志时只写 open() 和 close(),忘了 flush() 和 os.sync()。这两个调用有什么区别?

  • flush():把 Python 缓冲区里的数据推送到操作系统/底层文件系统。但如果文件系统本身也有缓存,flush() 不一定保证数据已经落到 Flash。
  • os.sync():同步文件系统,把文件系统缓存中的数据真正写到块设备上。

在 MicroPython 环境里,有些移植版对 flush() 和 os.sync() 的实现并不完全一致,但稳妥的做法是:重要的数据写入后,两者都调用。代价是每次 sync 都会引起 Flash 擦写,写入速度会变慢。所以“重要程度不高”的数据(比如临时缓存)可以不 sync,但在掉电关键场景必须 sync。

我自己的习惯是:配置文件这种改动频率低、但丢失后果严重的,必须 sync;日志文件这种高频追加的,改为在 write 之后只 flush,每隔 N 行或者定时调用 os.sync(),平衡性能和可靠性。

4.2 外部存储挂载:SD 卡的完整接入方式

多数开发板没有自带 SD 卡槽,需要你自己接一个 SPI 模式的 SD 卡模块。在 MicroPython 中接入 SD 卡通常分三步:初始化 SPI 和 SD 卡对象、检查是否已有文件系统、挂载到挂载点。

import machine, os # 以 ESP32 为例,SPI 引脚按你实际接线修改 spi = machine.SPI(2, baudrate=40000000, polarity=0, phase=0, sck=machine.Pin(18), mosi=machine.Pin(23), miso=machine.Pin(19)) cs = machine.Pin(5, machine.Pin.OUT) # 旧版本使用 sd 模块,新版本使用 sdcard 模块 try: import sdcard sd = sdcard.SDCard(spi, cs) except ImportError: import sd sd = sd.SDCard(spi, cs) # 如果 SD 卡是新的,需要先格式化;这是危险操作,确认不要数据再执行 # os.VfsFat.mkfs(sd) os.mount(sd, "/sd") print(os.listdir("/sd"))

这段代码里最关键的一步是 os.mount()。如果你只初始化了 SD 卡,没有挂载,那无论你怎么 open('/sd/a.txt') 都是找不到路径的。我见过不少人把 SD 卡模块接好后,直接写 open('/sd/test.txt', 'w'),结果报 “OSError: [Errno 2] ENOENT”,问题就是少了 mount 这一步。

SD 卡的默认文件系统一般是 FAT32,MicroPython 能原生识别。如果你想在 SPI Flash 上使用 LittleFS,则需要用 os.VfsLfs2.mkfs() 进行格式化,然后再 mount。示例:

import os from machine import SPI, Pin # 假设外部 Flash 块设备实例为 dev # dev = FlashDevice(...) # os.VfsLfs2.mkfs(dev) # 第一次使用才需要 os.mount(dev, "/ext")

格式化是一锤子买卖,会把设备上所有旧数据清掉。新手刚拿到外部 Flash 模块,如果没有老数据,可以直接 mkfs;但如果是买了二手模块或者反复测试过的模块,mkfs 前先确认没有重要数据。

4.3 用 RAM 盘做临时存储

有些移植版支持 RAM 盘,比如 ESP32 上你可以用 machine.RTC().memory() 保存一小块数据,或者用 bytearray 自己实现一个内存块设备。不过最简单的实际用法是先把需要高频读写的临时配置或者缓冲结果放在内存里,不碰文件系统,等最后确认要持久化时才一次性写入 Flash。

比如你要保存一组 WiFi 扫描结果:

import ujson, time scan_data = [{"ssid": "A", "rssi": -40}, {"ssid": "B", "rssi": -55}] json_str = ujson.dumps(scan_data) # 先放内存里,等扫码完成或定期再写 buffer = json_str ... # 确认写入时才调用 with open("/wifi_scan.json", "w") as f: f.write(buffer) f.flush() os.sync()

注意,MicroPython 的字符串拼接和 list 操作都在 RAM 中进行,如果数据量太大,比如几十 KB,ESP32 的 RAM 可能扛不住。这时候还是要直接流式写入文件,每攒一小段 buffer 就写入一次,但不要每次 write 都 sync。

4.4 配置文件的读写与默认值处理

配置文件是嵌入式设备最常用的存储场景。启动时读配置,没有文件则写默认值。常见做法是:

DEFAULT_CONFIG = { "ssid": "mywifi", "password": "12345678", "interval": 30, } def load_config(path="/config.json"): try: with open(path, "r") as f: data = ujson.load(f) except (OSError, ValueError): # 文件不存在或格式损坏,回退到默认配置 data = {} cfg = dict(DEFAULT_CONFIG) cfg.update(data) return cfg def save_config(cfg, path="/config.json"): with open(path, "w") as f: ujson.dump(cfg, f) f.flush() os.sync()

这里有两个容易出错的地方。第一,如果断电导致配置文件只写了一半,ujson.load() 会抛 ValueError,这时程序必须能回退到默认配置,否则就会启动失败。第二,不要把密码之类敏感信息明文存储,这只是个开发板,最好加密或至少混淆,不过我主要从功能完整性角度提醒一下。

5. 常见问题与排查实录

5.1 文件写不完,断电后整个文件消失或打不开

这个现象我几乎每周都能在论坛上看到。出现这种问题,首先检查你是不是只调用 close(),没有 flush() 或 os.sync()。close() 会关闭文件句柄,但不保证把文件系统缓存刷到 Flash。其次,检查代码逻辑:如果写入过程中抛出异常,文件是否没有正常 close?MicroPython 的 with 语句可以保证退出时 close,但 close 不等于 sync,这是两码事。

注意:重要数据写入后,先 f.flush() 再 os.sync()。如果程序崩溃或断电,最坏情况是丢失最近几次未同步的数据,但不会导致文件系统整个挂掉。

5.2 删除文件后,空间没有立刻变多

你可能遇到过这样的情况:删掉一个大文件后,调用 os.statvfs("/") 或者查看剩余空间,发现空间并没有立刻完全恢复,或者隔一段时间才恢复。这跟文件系统的垃圾回收和 COW 设计有关。删除一个文件,实际上是把文件的数据块标记为“可回收”,而不是立刻擦除。LittleFS 会在后续写入时按需回收和擦除块。所以你删除后马上看到空间不变是很正常的,等下一次写入或者文件系统执行后台填充时,可用空间会恢复过来。

这在电脑上也很常见,Windows 删除文件后,磁盘空间经常不会立刻 1:1 恢复,因为文件系统可能有延迟回收、卷影副本或者预分配机制。嵌入式文件系统也一样,只是延迟时间可能更长。针对这一点,做空间管理时不要用“删掉一个文件就重新计算空间剩余”的逻辑,最好在写入前检查剩余空间,而不是依赖删除后的即时恢复。

5.3 大量小文件写入越来越慢

如果你的应用频繁地在根目录直接写入很多小文件,比如每秒往 /data 目录下新建一个 100 字节的文件,文件系统会越来越慢。LittleFS 处理小文件的方式本来是为了节省空间的,但如果文件数量太多,元数据的查找、目录树的更新都会变慢。

我的解决办法是设计一个“分区目录策略”:把最高频的文件放在一个固定的轮转文件里,而不是每个周期新建一个文件。例如日志文件只保留一个 data.log,写满到一定大小后重命名成 data.old,再新建 data.log。这样整个过程中目录项的数量基本不变,写入负载也集中一些。

5.4 Flash 生命周期耗尽:还能救吗

NOR Flash 的擦写次数有限,典型寿命在 100k 次左右。如果你每秒钟都写一次文件并 sync,寿命短得吓人。一些工业级模块的 Flash 可以撑更久,但也不是无限的。

在 MicroPython 中做高频数据记录时,务必考虑把数据先缓存在 RAM,再批量化写入;或者使用专门的外部 SD 卡 / 外部 SPI Flash 记录数据,让主控芯片自带 Flash 只保留固件和配置。SD 卡的磨损均衡通常也由卡内主控负责,承载大数据写入更合适。如果必须频繁写板载 Flash,建议加一个上传机制,把本地 Flash 仅仅当作缓冲,数据上传到服务器后就删除,这样能显著延长 Flash 寿命。

5.5 路径大小写和文件命名

MicroPython 的 LittleFS 是大小写敏感的,这一点和 Windows FAT 习惯不同。你写入一个叫 Data.txt 的文件,另一个文件叫 data.txt,这两个可以同时存在。新手最容易踩的坑是:代码里写 open("/sd/config.txt"),但实际生成的是 CONFIG.TXT,结果怎么都打不开。遇到奇怪的文件找不到问题,优先用 os.listdir() 打印一下目录里的真实文件名。

5.6 固件更新、main.py 和文件系统被重置

很多时候你改了 main.py,却发现上电后运行的是旧代码。常见原因有两个:一是 Thonny 之类的工具实际保存到了别的路径或者没有真正写进去;二是固件更新后文件系统被重新格式化,旧文件丢失。MicroPython 在烧写固件时,不一定每次都会保留 VFS 分区。如果你 update 固件后发现文件全没了,优先检查固件烧录工具有没有勾选“擦除 Flash”选项。

为了避免 main.py 在开发中途崩溃导致设备变砖(严格说是反复重启),我会先删除 main.py,只保留 boot.py,等所有功能在脚本里稳定运行后,再重新创建 main.py 设为主程序。不要在 main.py 中做危险的无限循环也不做任何异常捕获,否则你连重新接入 REPL 都可能变得很麻烦。

6. 一些特殊的存储玩法:从源码到 schema 的扩展思路

6.1 把文件系统当配置数据库用

有些项目需要保存多个传感器校准参数,或者一组设备配置。最简单的办法是用 JSON 文件,但如果你有几十上百条记录,建议按 ID 拆分文件,比如 /cfg/device_001.json。这种做法的好处是写入一个文件不会影响其他文件,修改某一条配置时不用把整个大文件读出来再写回去。

不过,文件数量太多也有目录项开销的问题。折中方案是“分桶”:把 ID 换算成目录层级,比如 /cfg/00/device_001.json,每个目录只放少量文件,避免根目录堆积大量 Term 项。

6.2 启动时检查并修复文件系统

MicroPython 本身一般不会像桌面系统那样提供 fsck 工具,但你可以写一个启动自检逻辑:

import os, machine def check_fs(): try: f = open("/health_check", "w") f.write("ok") f.close() os.remove("/health_check") print("filesystem ok") except OSError as e: print("filesystem error:", e) # 根据具体错误决定是否格式化或使用备用方案

这个“健康检查”文件的方法,能快速发现文件系统是否只读或者已损坏。如果输出 OSError 且无法解决,最后的手段是重新格式化整个 Flash 分区。格式化之前确认固件本身还是好的,因为格式化会清空你所有脚本,包括 boot.py 和 main.py,如果格式化后没法写脚本进去,设备就只能重新烧固件。

注意:不要轻易格式化。格式化前先把 boot.py/main.py 备份到电脑,否则设备重新上电后可能连 REPL 都进不去(通常是能进,但应用功能全部消失)。

6.3 与 PC 端同步文件的工具链

开发时经常需要在电脑和板子之间同步代码。常用的 mpremote 命令:

mpremote cp main.py : mpremote cp /sd/data.csv : mpremote ls

mpremote 支持直接操作文件和目录,比 Thonny 命令行方式更灵活。这里有一个小技巧:别用 os.rename() 去覆盖正在运行的 main.py,如果你在脚本运行期间想更新 main.py,最好先写到 main.new.py,再用 mpremote 之类的工具在复位后替换。

6.4 文件系统事件的利用

MicroPython 没有完整的事件通知机制,但你可以利用文件是否存在作为状态机标志。比如设备开机时检测 /flag_boot 是否存在,存在说明上次可能没有正常关机,从而触发自检或日志检查。这是一种非常朴素的“崩溃标记”做法。

import os FLAG_PATH = "/flag_boot" if os.path.exists(FLAG_PATH): print("previous boot was not clean") # 进入安全模式或错误处理 else: with open(FLAG_PATH, "w") as f: f.write("booted")

这种方案简单有效,但要注意:Flag 文件的写入时机最好放在 boot.py 或者 main.py 最前面,而且要在主要逻辑执行前完成。如果主要逻辑在写 Flag 前就崩溃了,那下一次仍会认为上一次是干净启动,导致漏报。

7. 几个值得记住的数值和选择建议

我把日常调参过程中比较有用的几个经验数值整理成表格,方便大家参考:

参数/场景推荐值/做法理由
日志写入频率每秒最多 1 次,推荐 10 秒以上一次减少擦写,延长 Flash 寿命
单次写入数据量尽量凑到 1KB~4KB匹配 Flash 页大小,减少写放大
文件数量级单目录下不超过 100 个文件避免目录查找和元数据开销过大
配置文件格式JSON 或简单键值文本易解析,不易像二进制那样出错
外部存储SD 卡 FAT32,SPI Flash LittleFS各自发挥兼容性/掉电安全优势
掉电保护诉求高每次写完调用 flush + os.sync尽可能降低数据丢失窗口

这些数值不是绝对标准,具体还要看你的 Flash 芯片型号和 MicroPython 移植版本。但有一个原则可以通用:离 Flash 物理擦除边界越远,应用越不容易踩坑。

8. 关于“存储还是不够用”的扩展思路

如果你的项目对存储容量需求很大,比如要保存几十 MB 的传感数据或图片,板载 Flash 显然不够。我的次选方案是直接用 SD 卡,SD 卡容量大、FAT32 兼容性好,但要注意 SPI 模式下的写入速度通常比较慢,实测大约几十 KB/s 到几百 KB/s,取决于卡本身和接线质量。如果对速度要求高,可以考虑 SDIO 模式或者更高速的 SPI 接口,但代码复杂度和硬件连线难度都会上升。

还有一个思路是把数据通过 Wi-Fi/BLE 上传到服务器或 NAS,本地 Flash 只做缓冲。这类场景下,Flash 上的文件系统更像是“临时缓冲区”,而不是最终数据仓库。这样即使 Flash 空间被写满,也不会造成不可恢复的损失。我在做一个环境监测项目时,就是把数据先写到本地的 small buffer 文件,每 5 分钟 FTP 上传一次,上传成功后立刻删除本地文件,板载 Flash 空间一直很稳定。

9. 我在实际项目中的一点体会

做了几年 MicroPython 开发,我最大的体会是:文件系统不是“用一下就行”的黑盒子,它跟底层 Flash 的配合关系,决定了你项目的长期稳定性。早期我只知道 open() 和 write(),只要偶尔出错就怀疑是固件问题,后来搞懂 VFS、块设备、LittleFS 的原理之后,再遇到奇怪的问题,至少知道该往哪个方向排查:是挂载问题、同步问题、还是 Flash 磨损问题。

如果你也是新手,我建议不要一上来就去读 LittleFS 的源码,先把这把“存储钥匙”装进脑子里:文件系统是构建在块设备之上的,块设备负责物理读、写、擦除,文件系统负责目录、文件和安全。MicroPython 帮你把这两层封装成了 os.mount() 和 open(),但当你遇到“文件打不开”“空间不释放”“写入变慢”这类问题时,一定要回到块设备、挂载、同步和磨损这几个维度去想。这个思路一旦建立起来,后面不管是玩 ESP32、RP2040 还是 STM32,你都会非常从容。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询