news 2026/9/11 6:14:22

Pico MicroPython文件读写实战:打造断电不丢数据的温度记录器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Pico MicroPython文件读写实战:打造断电不丢数据的温度记录器

很多刚拿到树莓派 Pico 的朋友,第一次接触 MicroPython 时的路径几乎一模一样:点亮板载 LED,读一个内置温度值,然后用print()把它打到 Thonny 的终端里,看着数字一行行滚出来,觉得“成了”。

但真要拿它做一个温度数据记录器的时候,问题就来了——USB 一拔,数据全没了。屏幕上那些数字只存在于内存和终端缓冲区里,Pico 一断电,内存清空,你辛苦采集的几百组数据就此蒸发。我见过太多人在这一步卡住:采集逻辑完全没问题,就是不知道该怎么把数据长久留下来。

这篇文章要解决的正是这个核心问题:MicroPython 里的文件读写到底怎么用。我会用“温度数据记录”这个实际项目当载体,把open()write()read()close()这些 API 讲透,同时把断电保护、Flash 寿命、CSV、文件损坏恢复这些真正常遇到的实操问题一并处理掉。适合刚入手 Pico、想在 MicroPython 下做数据记录或日志功能的读者,也适合在职开发想快速了解嵌入式文件系统的朋友。

1. 为什么温度记录必须落到文件系统:串口打印远远不够

1.1 环境准备:固件、Thonny 与开发板连接

这篇文章的实验基于标准的树莓派 Pico 开发板运行 MicroPython 固件。准备起来很简单:

  • 一块 Pico(Pico W 也可以,不涉及网络功能时完全一样)
  • 一根支持数据传输的 Micro USB 线
  • 安装了 Thonny IDE 的电脑,以及对应版本的 MicroPython 固件

按住 Pico 板上的 BOOTSEL 键插上 USB,电脑会出现一个 U 盘,把.uf2固件拖进去,板子会自动重启。Thonny 里配置解释器为 MicroPython (Raspberry Pi Pico),右下角能看到 MicroPython 的版本号,连接就完成了。

提示:如果插上板子后 Thonny 右下角一直显示“未连接”,多半是 USB 线只能充电不能传数据,换线就好。这种情况我遇到不止一次,排查优先级最高。

1.2 串口打印的致命短板:断电即失

先看一个最典型的“新手日志程序”:

from machine import ADC import time sensor = ADC(4) while True: reading = sensor.read_u16() print(reading) time.sleep(1)

这段代码能跑,而且跑得很欢。数据也确实在终端里“记录”下来了——但终点在电脑屏幕上,不在 Pico 里。你拔掉 USB,屏幕上的历史数据还在电脑的终端缓冲区里,Pico 自己也拿不到;如果你换个电脑、换个终端,屏幕清空,历史归零。

更深一层的问题是:这种记录方式完全没有“事后分析”的通道。你想统计一天的温度曲线,总不可能盯着几千行终端输出手工抄数字。真正合理的做法是让 Pico 自己把数据组织成文件,存在板载 Flash 上,之后不管是连续追加、断电重连、导出分析都有据可依。这就是文件系统存在的意义。

1.3 Pico 的文件系统:Flash 上的目录与文件到底放在哪里

MicroPython 对 Pico 的板载 Flash 做了文件系统支持。默认情况下,固件会划出 Flash 的一部分挂载到根目录/,你往里写文件,文件会持久保存在 Flash 上,断电不会消失,重启之后os.listdir('/')依然看得到。类 Unix 的目录结构带来很大便利,/下面可以建子文件夹,也可以直接用/data.csv这种路径访问根目录文件。

单块 Pico 的可用文件系统空间取决于固件版本,从几百 KB 到 1MB 以上都有。对温度记录这种文本日志来说,每行二十几个字节,写个上万行完全不成问题。但是,空间容量不是唯一约束,Flash 的写入寿命才是真正的隐形天花板。后面我会单独讲,先记住一个结论:频繁小段写入对 Flash 不友好,批量写入更能延长寿命。

1.4 为什么“写文件”这件事比你想的复杂一点

很多人第一次在 PC 上写 Python 时并没有认真琢磨文件读写,因为操作系统和磁盘驱动把一切都处理好了。但在 Pico 上,你面对的是一个资源受限的嵌入式环境:没有大容量内存做缓存,没有成熟的日志轮转机制,也没有闪存磨损均衡的自动保障。MicroPython 把文件 API 设计得和桌面 Python 几乎一模一样,底层却是完全不同的机制。理解了这一点,你就能明白为什么后续要设计“批量写入 + 及时 close + 定期轮转文件”这套策略。

2. open/write/read 三个 API 吃透:先跑通最简单的读写

2.1 打开模式 w/a/r 的选择逻辑

MicroPython 的open()接口和桌面 Python 保持了一致,第一个参数是路径,第二个参数是打开模式。模式决定了你打算对文件做什么,选错模式是新手最常见的错误。

模式含义文件不存在时文件存在时
'r'只读报错从开头读取
'w'只写,清空旧内容创建把原文件内容清空后写入
'a'追加写创建在文件末尾继续追加
'r+'读写,不截断报错从开头读写,可配合 seek 定位
'rb'/'wb'二进制读写按需创建/报错按二进制方式处理
'x'独占创建创建报错,避免覆盖

实际项目里,最常用的就是'w'(新建或重置数据文件)、'a'(持续追加日志)、'r'(回读分析)。'r+'的模式在 MicroPython 上系统支持度视固件而定,能用,但入门阶段可以先把前三种吃透。

关键决策点是“追加 vs 覆盖”。数据记录场景下,你要么每天一个独立文件用'w'新建,要么所有数据写进同一个文件用'a'追加。我强烈建议:每天或每轮跑批创建新文件用'w',同一天内连续采集用'a',这样既不会把昨天数据冲掉,也不会让单个文件无限膨胀。

2.2 最小可运行示例:写入一行温度记录再读回来

纸上谈兵不如直接跑代码。下面这个例子展示了 MicroPython 文件读写的完整闭环:打开文件、写入内容、关闭文件、再打开读取。

import os # 第一步:从路径列表确认根目录可写 print("根目录文件:", os.listdir('/')) # 第二步:写入 with open('/demo.txt', 'w') as f: f.write('hello pico\n') f.write('temperature=26.5\n') # 第三步:读取 with open('/demo.txt', 'r') as f: content = f.read() print("读回的内容:") print(content)

with语句的写法值得养成习惯:它会在代码块结束时自动调用close(),避免你忘关文件。在 MicroPython 里,文件对象占用系统资源,频繁打开却不关闭,最终会导致“打开文件太多”的异常。

读回来后,文件已经留在 Flash 上。你可以在 Thonny 左侧的文件面板里刷新看到/demo.txt,右键还能直接下载到电脑。这就是“数据真正保存下来”的直观证据。

2.3 文件写不进去的经典原因:缓冲区、close 与文件句柄

很多新手跑完上面的代码后改了自己的版本,却遇到“文件存在但内容是空的”这种诡异情况。最常见的原因是:写了数据没 close 就立刻断电或复位

MicroPython 的write()在多数情况下会把字节交给底层文件系统,但文件系统为了性能和 Flash 的写入特性,往往有内存缓冲。数据要真正落实,依赖close()(或显式flush())。如果你在循环里f.write()写了几百行,程序中途异常重启、USB 被拔掉,缓冲区和未完成的元数据更新可能一起丢失,文件就变成残缺的。

来看正确的“显式刷盘”写法:

f = open('/log.txt', 'a') try: for i in range(5): f.write('line {}\n'.format(i)) f.flush() # 把当前缓冲区内容刷到文件系统 finally: f.close()

flush()不是每个循环都必须要调,它的代价是额外写入操作,调太密反而增加 Flash 擦写次数。合理的策略是:批量攒够一定量再一次性write()+flush(),下一节我会给具体方案。

2.4 一个兼容性提醒:a 模式在部分固件上的表现

微控制器上的 MicroPython 固件百花齐放,不同版本对小众文件模式的实现完整度并不一致。实测下来,'w''r'是最稳的组合,'a'在绝大多数官方和主流第三方固件上表现正常,但如果你用的是比较老的定制固件,要留意追加是否真正落在文件末尾。

万一真碰到追加异常,降级方案是用读写模式手动定位到末尾:

with open('/log.txt', 'r+') as f: f.seek(0, 2) # 2 表示从文件末尾开始定位 f.write('append line\n')

这个技巧也可以帮你理解seek()的作用:它可以精确控制读写位置。不过入门阶段,绝大多数官方固件直接用'a'就够了。

3. 实战记录器改造:一分钟一批,断电不丢数据

3.1 从内置 ADC 读温度:公式与误差

Pico 内置了一个温度传感器,挂在 ADC 输入的第 4 通道上。读取方式很简单:

from machine import ADC import time sensor = ADC(4) def read_temperature(): reading = sensor.read_u16() voltage = reading * 3.3 / 65535 temperature = 27 - (voltage - 0.706) / 0.001721 return temperature while True: print(read_temperature()) time.sleep(1)

这个公式来自 RP2040 数据手册:0.706V 对应 27°C,温度每上升 1°C,电压下降 1.721mV。理解公式对排查异常读数很有用——如果你看到voltage超出了 0.3V 到 0.9V 的范围,说明哪里算错了。

需要特别提醒:内置传感器测的是SoC 芯片内部的温度,不是环境温度。芯片运行负载高、持续读写 Flash 时,内部温度会比环境高出好几度。所以它适合做“相对温度趋势”记录,不适合做高精度环境测温。真要测环境温度,可以外接 DS18B20 这类数字传感器,但文件读写的逻辑是通用的,本文后续步骤完全不用改。

3.2 批量累积 + 定期落盘:一次写入五条数据

回到文件写入策略。直接每秒 open 一次文件写入一条再 close,虽然安全,但是对 Flash 擦写次数不友好;反过来一直不 close,又增加断电丢数据的时间窗口。

我的做法是“积攒到固定数量再一次性写盘”。下面这个例子,每 10 秒采集一次,攒满 6 条(即一分钟)写一次文件:

from machine import ADC import time sensor = ADC(4) def read_temperature(): reading = sensor.read_u16() voltage = reading * 3.3 / 65535 return 27 - (voltage - 0.706) / 0.001721 batch = [] LOG_FILE = '/temperature.csv' def flush_batch(): global batch if not batch: return with open(LOG_FILE, 'a') as f: for row in batch: f.write(row) batch = [] # 首次创建文件时写表头 try: with open(LOG_FILE, 'r') as f: pass except OSError: with open(LOG_FILE, 'w') as f: f.write('elapsed_sec,temperature_c\n') start = time.ticks_ms() while True: # 这里可以改成任意真实采集逻辑 temp = read_temperature() line = '{},{:.2f}\n'.format(time.ticks_diff(time.ticks_ms(), start) / 1000, temp) batch.append(line) if len(batch) >= 6: flush_batch() time.sleep(10)

这段代码有四个值得记住的细节:

第一,表头只在文件不存在时写。程序重启后再次运行,如果直接'w'就会把旧数据全删掉,做成“异常重开不覆盖历史”状态,对长时间数据记录来说非常重要。

第二,每 6 条数据组成一个 batch,一次性写入。这样平均 1 分钟才真正对 Flash 执行一次写操作,写入次数瞬间降了一个数量级。

第三,追加模式下不重复打开同一文件的钩子with open(..., 'a')每次只占用文件系统很短的时间,写完立刻关闭,掉电丢失窗口被压到毫秒级。

第四,数据行本身是纯文本字符串。如果你在采集插槽里读到的数据是浮点数,转换成字符串再写入,能避免二进制格式在文本分析工具里的兼容性问题。

3.3 文件名组织:序号命名比时间戳命名更稳

温度记录器有时候会长时间连续运行。运行三五天之后,单个 CSV 文件可能膨胀到几万行。文件系统里放单个巨大文件虽然也能工作,但有两个隐患:一是文件读写过程中一旦损坏,全部历史可能一起遭殃;二是 Flash 文件系统对超大文件的支持不够优雅。

更稳妥的组织方式是按“批次”或“天数”滚动文件名:

import os def next_log_filename(prefix='temperature'): index = 1 while True: name = '/{}_{}.csv'.format(prefix, index) try: os.stat(name) index += 1 except OSError: return name

这样产生的文件是temperature_1.csvtemperature_2.csv……程序启动时自动找一个不存在的编号创建新文件。相比用时间戳命名,序号命名完全依赖板上 Flash 已有文件,不依赖 RTC 时钟是否准确,也不依赖网络对时,可靠性更高。Pico 本身没有纽扣电池,RTC 每次复位都是系统默认时间,直接拿时间命名很容易出现奇怪日期。

3.4 为什么这套策略能断电不丢数据

把“批量写入 + 每次写完 close + 滚动文件名”这套组合拆开看,它分别解决的是:

  • 批量写入降低 Flash 擦写频率,延长存储介质寿命
  • 写完就 close 缩短“数据在缓冲区里”的时长,断电时最多丢这一批
  • 滚动文件把损坏风险限制在一个文件内,不至于全军覆没

即便真遇到极端掉电导致最后一批丢了,损失也就一分钟的 6 条数据,整个项目还能继续跑。追求极端可靠的做法还可以在内存里保留未落盘 batch 的副本,启动时检测到上次异常,把副本重新写入新文件。不过这是更高阶的话题,入门阶段做到“一分钟最多丢 6 条”已经足够。

4. 数据怎么读回来:CSV 格式、回放与临时分析

4.1 写 CSV 而不是纯文本:为后续分析铺路

实测下来,CSV 是嵌入式日志最实用的文本格式,没有之一。它只有一行一个记录,字段用逗号分隔,第一行写表头,既能在 MicroPython 里轻松逐行解析,又能导出到电脑上用 Excel、pandas 直接分析。

前面例子里的表头是elapsed_sec,temperature_c,数据行是123.45,26.83。如果你还想记录采集次数,字段顺序按需扩展,比如index,elapsed_sec,temperature_c。有几个约定俗成的规范建议遵守:

  • 表头字段名不要带空格
  • 浮点数用点号分隔小数,不要用千分位
  • 每行以\n结尾
  • 如果某个字段值本身包含逗号,必须加引号转义,不过温度记录场景基本用不上

遵循这些规范,后续把 CSV 丢到电脑上分析的时候会非常省心。

4.2 用 seek 和 readline 读回刚才的记录

文件写进去了,不代表软件写完了。一个数据记录器通常还需要“回放”能力:程序重启后,能读回历史数据,显示十几分钟内的温度曲线,或者把数据重新发到串口。

读取历史 CSV 最符合直觉的方式是逐行处理:

def read_temperature_log(path='/temperature.csv'): temps = [] with open(path, 'r') as f: next(f) # 跳过表头 for line in f: line = line.strip() if not line: continue parts = line.split(',') elapsed = float(parts[0]) temp = float(parts[1]) temps.append((elapsed, temp)) return temps

next(f)跳过了第一行表头;split(',')把每一行拆成字段列表;float()把字符串转成可计算的数值。这就是“回放”的全部核心逻辑,非常简单。

如果你不想把全部数据加载进内存,只是需要看最后几条,配合seek()可以定位读取:

with open('/temperature.csv', 'r') as f: f.seek(0, 2) # 跳到文件末尾 size = f.tell() start_pos = max(0, size - 200) # 读最后 200 字节左右 f.seek(start_pos, 0) tail = f.read() print(tail)

注意seek(0, 2)里的第二个参数 2 表示“相对文件末尾偏移”,这在文件末尾回溯时很常用。

4.3 数据量超过内存时,逐行处理而不是一次性 read

MicroPython 在 Pico 上运行时,可用内存算是很紧张的,普通 Pico 只有 264KB 内存,程序、Python 堆、文件缓冲都在里面。如果你的 CSV 文件已经写了几万行,直接f.read()一次性读出来,极可能触发MemoryError

正确的处理思想是“流式处理”:读一行,处理一行,不保留整份文件。统计温度最大最小值就是一个典型场景:

def summarize(path='/temperature.csv'): min_temp = None max_temp = None count = 0 with open(path, 'r') as f: next(f) for line in f: parts = line.strip().split(',') if len(parts) < 2: continue t = float(parts[1]) if min_temp is None or t < min_temp: min_temp = t if max_temp is None or t > max_temp: max_temp = t count += 1 return count, min_temp, max_temp

这段代码对内存占用恒定,不管 CSV 是几百行还是几十万行都能平稳运行。凡是需要在板子上分析大量数据的场景,都应该用流式处理,而不是把文件read()成一个大字符串。

4.4 文件体积控制:os.stat 触发滚动

长时间记录时,文件会越来越大。可以在程序里定期检查文件大小,超过阈值就滚动到下一个文件。Pico 上用os.stat()获得文件元数据,其中下标为 6 的元素是文件大小(字节):

import os def current_log_size(path='/temperature.csv'): try: info = os.stat(path) return info[6] except OSError: return 0 # 在 flush_batch 里判断,超过 100KB 就滚动 if current_log_size() > 100 * 1024: LOG_FILE = next_log_filename() with open(LOG_FILE, 'w') as f: f.write('elapsed_sec,temperature_c\n')

这样单个文件被限制在合理大小,后续归档、传输、损坏恢复都更容易处理。滚动频率取决于采集频率和数据格式,100KB 对这种短行 CSV 大约是几千行,对多数场景足够了。

5. 实测中踩过的坑与恢复方案

5.1 直接拔电导致“最后一批数据丢失”的台前幕后

这个问题我在 3.2 节设计批量写盘时已经做了预防,但很多人实际运行时会遇到一个变种:程序里明明写了 close,最后一条数据还是丢了

排查该问题有一个经验规律:最后一批数据是在内存 batch 里,还是已经调用过flush_batch()?如果你把采集频率设置为 1 秒,每 60 条才 flush 一次,那么在两次 flush 之间断电,丢掉的最高纪录就是 60 条数据。很多人觉得“我明明写了”,其实写的是内存里的 Python 列表,底层文件系统并不知道。

解决方式有两个层面:

  • 调低批量大小:每 10 条 flush 一次,最多丢 10 条
  • 更可靠的是用machine.reset_cause()检测是否为异常掉电启动,如果有上次未完成的恢复任务,在启动时补写
> 注意:`flush()` 不是免费的。它对 Flash 执行真实写入,flush 越频繁,Flash 擦写越快。追求“完全不丢数据”需要接受 Flash 寿命折损,二者要取舍。

5.2 文件系统进入只读状态:别慌,先备份再修复

有一个真实情况可能让初学者崩溃:程序在某次掉电或强杀后,再次运行,open('/xx.csv', 'a')突然报OSError: [Errno 30] Read-only file system。这是底层文件系统检测到异常,自动挂载为只读保护数据。

解决步骤按顺序来:

  1. 先把还能读出来的文件复制到电脑上,这一步最关键
  2. Thonny 里给 Pico 做一次软复位,或拔插 USB 让系统干净重启
  3. 如果可以,格式化文件系统释放异常状态

格式化不是随便做的,会在 Thonny 的终端里执行:

import os os.format()

格式化会清空板上的文件,所以严格执行“先备份”的顺序。Pico 上的 MicroPython 文件系统异常概率不高,但长期无人值守运行偶尔会遇到,了解这个修复链路能救回很多数据。格式化以后记得重新创建表头和日志文件。

5.3 Thonny 的软复位与会话干扰:长期录制的坑

用 Thonny 调试时,程序运行方式是“能跑通就行”,尤其是记录器这种需要长时间驻留的程序,问题更大:Thonny 的停止按钮会触发软复位,让程序从头开始;它和 Pico 的交互也会占用 USB 串口资源,导致日志输出和程序运行互相干扰。

更隐蔽的坑是——你让记录器跑了半小时,把 Thonny 窗口关掉,或者电脑休眠,USB 通信中断,Pico 的程序可能停住。这不是 MicroPython 的 bug,而是 USB CDC 会话断开后,部分固件版本对控制台输出还在尝试写入串口导致的阻塞。

所以,长时间数据记录的正确运行姿势是:

  • 把最终程序保存为 Pico 根目录的main.py
  • 拔掉 USB 线,用外部 5V 电源或充电宝供电
  • 让 Pico 上电后自动执行main.py,数据静静写盘
  • 需要取数据时,再插回电脑,用 Thonny 打开文件或直接读取
> 提示:把程序保存为 main.py 之后,Pico 上电会优先运行它。调试时想暂停自动运行,可以在 Thonny 里按住板上的 BOOTSEL 键再插 USB,进入固件更新模式,或者把 main.py 临时改名。

5.4 追加模式常见困惑:文件不存在、换行符与行尾

最后一个容易被忽略的小坑:open(path, 'a')在文件不存在时会自动创建文件,但如果你期望它“永远从第 0 字节开始”,它可能不会——因为如果脚本前面有一段以'w'模式打开文件的代码,就把它覆盖了。检查路径是否被其他打开方式抢占,是排查文件内容异常的第一件事。

另外,MicroPython 里写入'\n'就是 LF 换行。如果你把 CSV 文件拉到 Windows 上用记事本打开,可能看到所有行连在一起。不要慌,文件没坏,只是记事本不认 LF。用 VSCode、Thonny 自带的编辑器或者 Excel 打开都是正常的。如果你确实需要 CRLF 换行,可以在write时显式写成'\r\n',但温度数据分析基本不需要。

最后再分享一个非常实用的习惯

我自己做了几年嵌入式日志类项目后,深深体会到一个原则:日志文件的意义不在于“写”,而在于“读”。你把数据写进 Flash 只是第一步,真正体现程序价值的是之后能顺利地把数据取回来、看懂、分析。

所以我现在的 Pico 温度记录器脚本里,总会保留两个“隐藏功能”:一个是在启动时打印出当前记录文件的总行数和最后一条数据,方便检查上一周期是否正常;另一个是把文件滚动阈值设得偏小,宁可多生成几个文件,也不让单个文件承担全部风险。这两条看似简单,却帮我少跑了很多趟修数据的弯路。

如果你照着这篇文章的思路做完自己的温度记录器,建议试一下这个扩展:把采集频率从 10 秒改到 1 秒,把批量大小改成 30,跑一个晚上,第二天起来检查文件的连续性。文件里每一秒都有一条记录、没有断裂的时候,你才算真正吃透了 MicroPython 文件读写和数据记录这套组合拳。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 6:13:51

SSM框架毕业设计实战:从零搭建留学资讯网站

简介&#xff1a;本资源是一套完整的Java毕业设计项目——基于SSM框架的“萨丁”留学资讯网站&#xff0c;面向计算机专业本科生及Java初学者&#xff0c;解决课程设计、期末大作业与毕业设计选题难、部署调试复杂等实际问题。压缩包共1394个文件&#xff0c;涵盖176个Java后端…

作者头像 李华
网站建设 2026/9/11 6:13:01

让 HeyGem.ai 在 WSL 中用上 GPU:Docker 数字人服务部署与排障

让 HeyGem.ai 在 WSL 中用上 GPU&#xff1a;Docker 数字人服务部署与排障 【免费下载链接】Duix-Avatar &#x1f680; Truly open-source AI avatar(digital human) toolkit for offline video generation and digital human cloning. 项目地址: https://gitcode.com/GitHu…

作者头像 李华
网站建设 2026/9/11 6:11:57

国产FPGA一次点亮全流程:从RDP架构到产线落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 6:10:15

MicroPython轻量级日志模块设计:级别、轮转与过滤实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华