树莓派 Pico 这个小板子,是我玩单片机这几年里觉得最“亲民”的一颗 MCU。今天要聊的项目名字挺朴素——MicroPython 文件读写入门教程:树莓派 Pico 温度数据记录实战——但它把嵌入式开发里最常用、也最容易出暗坑的两件事串在了一起:从传感器读数据,再把数据可靠地写进文件系统。
这个项目不需要额外接一堆传感器,Pico 板载的温度传感器就能采集温度,我们用 MicroPython 把它读出来,再存到板载 flash 上的文件里,实现一个真正的“数据记录器”。整个过程不依赖电脑串口持续在线,Pico 自己跑、自己存,后面把文件导出来就能分析。对刚入门 MicroPython 的朋友来说,这是一个非常合适的练手项目:硬件成本极低、代码量不大,但涉及的知识点很完整——模拟量采集、文件系统、文本格式化、异常处理,全都覆盖了。
我接下来会把设计思路、文件系统原理、完整代码、实测过程、踩坑经验都摊开讲,尽量让第一次接触 Pico 的人也能照着做出来。
1. 项目怎么来的:为什么选 Pico 做温度记录器
1.1 板载温度传感器省掉了外接硬件的麻烦
做温度记录器,通常想到的方案是拿一个 DS18B20 或者 DHT11 接在单片机上去读。但 Pico 有个别的开发板少有的特点:芯片内部自带一个温度传感器,连接到 ADC 通道 4,不需要外加任何元器件。
这个设计本来是帮开发者监控芯片工作温度的,但拿来做环境温度参考也够用。精度不算高,误差一般在 ±2℃ 左右,但用来做趋势记录、温度变化监测完全足够。如果你想测的是室温变化、设备散热情况,或者只是学习数据记录的工作流程,这个内置传感器就是零成本起点。
我这里也提一句:Pico 内置温度传感器测的是RP2040 芯片内部的温度,不是直接测量空气温度。芯片运行时自身会发热,读数会比环境温度偏高几度。如果要做精确环境测量,建议外接 DS18B20 这类数字传感器,但项目逻辑是完全一样的——换一下读取函数就行。这篇教程先用内置传感器把流程跑通。
1.2 为什么用文件读写而不是一直往串口打印
很多新手做完采集之后,习惯性就是print()往串口输出,电脑上开着串口助手看着数据滚动。这种方式的问题在于:数据没有落地。一旦拔掉 USB 线,或者 Pico 意外重启,前面采集的数据全丢。而且串口打印速度再快,也没法在离线场景下工作。
文件读写的意义就是“持久化”。把温度值格式化成一行的文本,追加写到文件里,数据就永久保存在 Pico 板载的 flash 闪存中。设备可以脱离电脑独立运行,之后用一根 USB 线把 Pico 重新连上,直接通过 Thonny 打开文件就能看到历史数据。
选择 MicroPython 来做,还有一个非常实际的原因:它对文件操作足够友好。open()、write()、read()这些函数用法和电脑上的 Python 几乎一样,完全不需要像 C 语言那样管理文件描述符、处理缓冲区。在嵌入式开发里,MicroPython 把“读写文件”这个门槛降得非常低,这正是它适合入门的原因。
1.3 硬件和软件清单
做这个项目需要的东西非常少:
- 树莓派 Pico 或 Pico W 开发板 1 块
- Micro-USB 数据线 1 根(注意要选能传数据的线,有的线只能充电)
- 电脑一台,且能上网
- Thonny IDE 或 mpremote 命令行工具
预算基本就是一块 Pico 的钱,二三十块搞定。如果你手头有 Pico W,也没问题,同样的代码可以跑,只是 WiFi 模块不参与这个项目。
软件方面重点准备两样:MicroPython 固件和Thonny。Pico 出厂默认不带 MicroPython 固件,需要先烧录。这个流程不复杂,但有几个细节容易踩坑,我在后面“实操记录”部分会专门说。
2. 先搞懂文件系统,再动手写代码
2.1 MicroPython 的虚拟文件系统到底是什么
MicroPython 里操作文件,很多教程直接让你抄代码,但我觉得至少要搞明白文件系统这层概念,否则报错都不知道去哪查。
MicroPython 底层有一套**虚拟文件系统(VFS)**机制。它把 flash 存储、SD 卡、甚至外部 SPI Flash 都抽象成统一的文件系统。在 Pico 上,默认用的是LittleFS。LittleFS 是一个专门为嵌入式设备设计的小型文件系统,特点是省空间、防断电损坏、支持磨损均衡。这和电脑上常见的 FAT32、NTFS 不完全一样,但在 MicroPython 里,你用open()读写文件时,不需要关心底层是小文件系统还是别的,接口是一样的。
可以把它理解成一个微型 U 盘:Pico 的 flash 就是 U 盘的存储颗粒,LittleFS 就是 U 盘的文件分配表,而你用 MicroPython 写的文件名,就是 U 盘里的文件。
在 REPL 环境里可以用这些命令直接查看文件系统:
import os # 列出当前目录所有文件 print(os.listdir("/")) # 查看某个文件的详细信息 print(os.stat("/data.txt"))如果你用 Thonny 把 Pico 连接好,左侧文件浏览器里可以直接看到板子上的文件列表,手动删除、重命名都可以。非常直观。
2.2 文件打开模式:r、w、a 的区别
MicroPython 文件操作的核心就是open()函数。第一个参数是路径,第二个参数是打开模式。很多入门教程只是让你照抄"w"或"r",但没有讲清楚三个模式的区别,导致数据被覆盖的问题非常常见。
我用一张表说明:
| 模式 | 含义 | 文件不存在时 | 文件存在时 | 文件指针位置 |
|---|---|---|---|---|
"r" | 只读 | 报错 OSError | 正常打开 | 文件开头 |
"w" | 只写,覆盖 | 自动创建 | 清空原有内容 | 文件开头 |
"a" | 只写,追加 | 自动创建 | 保留原有内容 | 文件末尾 |
"r+" | 读写 | 报错 | 正常打开 | 文件开头 |
"w+" | 读写,覆盖 | 自动创建 | 清空原有内容 | 文件开头 |
"a+" | 读写,追加 | 自动创建 | 保留原有内容 | 文件末尾 |
做数据记录时最常用的就是"a"模式。每次打开文件写入新数据,内容会追加到文件末尾,之前的数据不会丢。如果你误用了"w",第二次运行程序时文件会被清空,前面的温度记录全部消失——这是最容易踩的坑,而且是静默发生的,没有任何提示。
我的建议是:凡是做日志、记录、追加采集数据,一律用"a"模式。
还顺便说一个问题:MicroPython 并不要求你用with语句,但强烈建议用。下面这两种写法的区别在于文件是否正确关闭:
# 推荐写法 with open("data.txt", "a") as f: f.write("26.5\n")# 不推荐,容易忘记 close f = open("data.txt", "a") f.write("26.5\n") f.close()在 MicroPython 里,如果程序异常中途退出,忘记关闭的文件可能导致写入内容没有真正刷到 flash 上。虽然 MicroPython 的write()通常会直接写入,但用with语句可以把资源管理交给解释器,少操一份心。
2.3 写入之后为什么需要 flush 和 close
这一点值得单独拎出来说。很多人在 REPL 里测试时发现:明明执行了write(),但用文件编辑器打开看到的内容是空的,或者不完整。原因在于写入缓存。
MicroPython 在底层会做数据缓冲,write()执行后数据可能还在内存缓冲区里,并没有立刻物理写入 flash。只有执行flush()或close()时,数据才会真正落盘。
所以重要的数据写入,推荐这种写法:
f.write("26.5\n") f.flush()如果只是每隔几分钟写一条数据,可以不频繁 flush,但如果要确保任何时刻断电都不丢数据,就要每写一条就 flush 一次。不过 flash 写入次数有限,频繁写入会加速磨损,这里要按项目需求做个平衡。我这个温度记录器和大多数日志类项目一样,几秒或几十秒写一条,用 flush 没任何问题。
3. 动手写温度采集与文件存储
3.1 内置温度传感器的读取原理
Pico 内置温度传感器的读取流程,比外接传感器简单很多。它连接到 ADC 模拟数字转换器的第 4 通道。ADC(Analog-to-Digital Converter)就是把模拟电压信号转换成数字数值的模块。
读取代码一般是这样的:
import machine sensor_temp = machine.ADC(4) # 通道4接温度传感器 raw = sensor_temp.read_u16() # 读取16位原始值read_u16()返回的是 0~65535 的值,代表传感器输出的电压经过 ADC 转换后的数字值。这个数字本身不是温度,需要换算。
RP2040 数据手册提供了一个换算公式,在 MicroPython 社区里也形成了统一写法:
voltage = raw * 3.3 / 65535 temperature = 27 - (voltage - 0.706) / 0.001721这里的参数含义是:传感器在 27℃ 时输出电压 0.706V,温度每上升 1℃,电压下降约 1.721mV。
写成完整函数就是这样:
import machine import time def read_temperature(): sensor_temp = machine.ADC(4) raw = sensor_temp.read_u16() voltage = raw * 3.3 / 65535 temp = 27 - (voltage - 0.706) / 0.001721 return temp注意一点:machine.ADC(4)这个对象不需要重复创建,可以在模块初始化时就创建好,提升效率。上面的写法是为了展示完整流程,实际项目里应该在文件开头初始化一次。
3.2 代码怎么组织:先跑通一步,再写循环
我不建议一上来就写一个“完整”的采集程序,因为一旦出错,你很难判断是采集模块的问题还是文件写入的问题。我自己写项目的时候,习惯分三步推进。
第一步,先确认能读到温度值:
import machine import time sensor_temp = machine.ADC(4) def read_temp(): raw = sensor_temp.read_u16() voltage = raw * 3.3 / 65535 return 27 - (voltage - 0.706) / 0.001721 while True: print("温度:", read_temp()) time.sleep(2)这一步跑通之后,说明 ADC 采集没问题。
第二步,测试文件写入:
with open("/test.txt", "a") as f: f.write("hello pico\n")在 Thonny 里运行一次,然后在文件浏览器里打开 test.txt,看看有没有内容。这一步验证文件系统工作正常。
第三步,把两者结合起来,做完整的记录程序。
3.3 完整代码:采集温度并写入文件
下面是我推荐的一个标准示例代码,真正做记录时可以直接参考:
import machine import time import os # 初始化 ADC 通道4,读取内置温度传感器 sensor_temp = machine.ADC(4) # 数据文件路径 DATA_FILE = "/temperature.csv" # 采集间隔,单位秒 INTERVAL = 10 def read_temperature(): """读取 Pico 内置温度传感器温度值,返回摄氏度""" raw = sensor_temp.read_u16() voltage = raw * 3.3 / 65535 temp = 27 - (voltage - 0.706) / 0.001721 return temp def init_file(): """初次运行时创建文件,并写入表头""" try: # 尝试以只读方式打开,文件存在则跳过 with open(DATA_FILE, "r"): pass except OSError: # 文件不存在,创建并写入表头 with open(DATA_FILE, "w") as f: f.write("timestamp,temperature_c\n") def append_temperature(): """读取温度并追加写入文件""" temp = read_temperature() # 当前时间戳,用系统运行时间模拟,也可以接 RTC 模块 timestamp = time.ticks_ms() line = "{},{:.2f}\n".format(timestamp, temp) with open(DATA_FILE, "a") as f: f.write(line) # 打印到控制台,方便现场观察 print(line.strip()) # 初始化文件 init_file() # 主循环 while True: append_temperature() time.sleep(INTERVAL)这个代码有几个设计点说明一下。
数据格式用的是 CSV。温度和时间用逗号分隔,后缀名用.csv。这样导到电脑上之后,Excel 或 pandas 可以直接读取分析,不用额外转换。
init_file()函数先检查文件是否存在。如果文件不存在,就创建并写入表头timestamp,temperature_c。如果文件已存在,就跳过这一步。这样程序重启后不会覆盖旧数据,也不会重复写入表头。
时间戳先用了time.ticks_ms()。这是 Pico 上电后的毫秒数,不是真实时间。如果只记录相对变化趋势,这个够用。如果需要真实时间,需要接一个 RTC 模块或者从电脑同步,后面我会在扩展部分专门讲。
3.4 为什么用 try-except 判断文件不存在
这里有个细节:判断文件是否存在,MicroPython 里没有 Python 的os.path.exists(),标准做法是尝试打开文件,捕获OSError异常。
try: with open(DATA_FILE, "r"): pass except OSError: # 文件不存在 ...有的朋友可能见过用os.listdir()判断,这方法也行,但要遍历目录,代码反而啰嗦:
if DATA_FILE not in os.listdir("/"): ...两种写法效果差不多,我更推荐 try-except,因为代码量少,还能顺便处理文件损坏、路径错误等异常情况。
4. 完整运行实录与调试记录
4.1 从烧录固件到第一行数据
这里我把实际操作过程完整记录下来,给第一次用 Pico 的人参考。
第一步:烧录 MicroPython 固件
从树莓派官网或者 MicroPython 官网下载.uf2固件文件。然后按住 Pico 板子上的 BOOTSEL 按键,同时用 USB 线连接电脑。此时电脑上会出现一个名为RPI-RP2的 U 盘。把下载好的.uf2文件拖进这个 U 盘,拷贝完成后 Pico 会自动重启,固件就烧录好了。
这个步骤里最常见的坑是:连接后电脑没有出现 U 盘图标。原因通常是 USB 线质量差或者不支持数据传输——只能充电的线是不会出现 U 盘的。此时换一根数据线试试,大多数问题都能解决。
第二步:打开 Thonny,选择正确的解释器
Thonny 安装好之后,打开菜单“运行”,选择“选择解释器”,在列表里找到MicroPython (Raspberry Pi Pico)。如果列表里没有,需要先安装 MicroPython 插件,或者更新 Thonny 到最新版本。
选对解释器之后,Thonny 右下角会显示MicroPython (Raspberry Pi Pico) @ USB 串行设备。此时就可以在 Shell 窗口直接输入 Python 命令测试了。
第三步:先手动测试一下文件写入
在 Shell 窗口输入:
with open("test.txt", "w") as f: f.write("temperature test\n")然后到 Thonny 左侧的文件浏览器里,双击test.txt,确认内容已经写入。这步能确认文件系统正常工作,同时也能确认你对设备的读写权限没问题。
第四步:运行完整项目
把上面的完整代码复制到 Thonny 编辑器里,点击“运行当前脚本”。Shell 窗口每隔 10 秒就会打印一条数据。同时左侧文件浏览器里会生成temperature.csv文件。
可以趁运行的时候在板上找到 ADC 附近的位置摸一下芯片——温度读数会明显上升,然后过一会儿回落,这是个很有趣的实时反馈。
4.2 循环采集的实测效果
我这里跑了一个小时,数据文件大概是这样的:
timestamp,temperature_c 4698,28.31 14698,28.17 24698,28.44 ...因为我设置的采集间隔是 10 秒,一个小时大概产生 360 条数据,文件大小在 15KB 左右。相对于 Pico 板载 flash 存储空间(Pico 有 2MB,MicroPython 文件系统可用大概 1.5MB),这个容量可以连续记录很久。
如果你希望记录得更密,可以把INTERVAL设为 1,也就是每秒一条,一小时就是 3600 条,文件也不大。但要注意:越密集的写入,越考验 flash 的寿命。后面“避坑经验”部分我专门说这个。
4.3 数据导出到电脑
采集完成后,把数据从 Pico 导出的方法很简单。在 Thonny 左侧文件浏览器里,找到temperature.csv,右键选择“下载到…”,保存到电脑本地即可。
如果不用 Thonny,也可以自己写个 mpremote 命令:
mpremote connect /dev/ttyACM0 cp :temperature.csv ./temperature.csvWindows 下端口名一般是COM3、COM4之类的,根据实际情况调整。
文件导出到电脑后,用 Excel 打开,或者用 pandas 读取:
import pandas as pd df = pd.read_csv("temperature.csv") print(df.head()) print(df["temperature_c"].mean())一次数据记录闭环就完成了:采集、存储、导出、分析。
5. 常见问题速查与避坑经验
5.1 最常见的问题排查表
我在教学和实际项目中遇到过很多次类似问题,整理成一张表,方便对照排查:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
OSError: [Errno 28] No space left on device | flash 空间被写满 | 删除无用文件,或定期将数据转存到电脑 |
| 文件内容为空 | 文件没有 close,或没执行 flush | 使用with语句,或写入后调用f.flush() |
| 数据每次都覆盖旧的 | 用了"w"模式 | 改成"a"追加模式 |
ValueError: invalid ADC pin | ADC 通道号写错 | 内置温度传感器必须是 ADC(4) |
| 温度读数异常偏高(五六十度) | 可能 ADC 浮动电压问题 | 检查是否有外接负载影响供电,或改用外接传感器对比 |
| 程序跑一会后卡死 | 写入频繁、flash 写入耗时 | 加大INTERVAL,或减少写入频率 |
| Thonny 连接不上 Pico | 驱动缺失或线材问题 | 换数据线、重新插拔、重装驱动 |
5.2 flash 寿命与磨损均衡
Pico 板载 flash 用的是W25Q16 等 SPI NOR Flash 芯片,理论上擦写次数在 10 万次左右。可能有人一看到 10 万次就慌了——每秒写一次,三天就完蛋了?
这里要解释一下:MicroPython 使用的是 LittleFS 文件系统,它自带磨损均衡(wear leveling)机制,会自动把写入分散到不同的物理块上。也就是说,逻辑上你一直往同一个文件追加数据,物理上数据其实分布在不同区块,不会总擦写同一个位置。所以实际寿命比“10 万次除以每秒一次”要长得多。
但我依然建议,不要过于频繁地写入。如果是高频率数据采集,最佳实践是:先在内存里积累一批数据,每攒到一定量或一定时间,再批量写入一次。
举个例子:
buffer = [] def collect_data(): global buffer temp = read_temperature() buffer.append("{}\n".format(temp)) if len(buffer) >= 10: with open(DATA_FILE, "a") as f: f.writelines(buffer) buffer.clear()这样把十次采集的数据攒到内存,一次性写入,既减少了写入次数,也降低了 flash 磨损。
5.3 掉电丢失数据的风险与对策
这个可能是很多人在实际项目中遇到过的问题:运行时一切正常,数据也能写入,但只要一拔电或者重启,发现之前写入的一部分数据没了。
原因还是 LittleFS 的缓存机制。某些情况下,最后写入的数据还停留在内存中,没有真正刷到 flash 上,突然断电就会导致这部分数据丢失。所以如果项目要求任何时刻断电都不丢数据,就要在每次写入后主动调用flush():
with open(DATA_FILE, "a") as f: f.write(line) f.flush()flush()执行后,数据会尽力物理写入 flash。当然,即使是flush()也不能保证 100% 抗断电(极端情况还有 flash 写入过程中掉电导致数据损坏的风险),但对于大多数记录任务来说,每条数据都flush()已经足够可靠。
从另一个角度想,如果你的采集间隔很长(比如一分钟一条),那每一条数据都很宝贵,丢一条都可惜,所以这个场景下我强烈建议每条都flush()。如果采集间隔很短(比如 10 毫秒一条),那么丢失最后几毫秒的数据可能无所谓,但也要保证批量丢数据不会影响整体。
5.4 时间戳:ticks_ms 不能满足所有需求
代码里用的time.ticks_ms()只是相对时间,也就是说,你不知道某条数据对应的“真实时间”是几点几分。如果只是分析温度变化趋势,这个够用;但如果你要知道某一天某个时刻的温度,就不行了。
解决办法有几种:
- 接一个外部 RTC 模块,比如 DS3231 或 DS1302,通过 I2C 接口读取实时时间。
- 从电脑同步时间:如果你用 Thonny 连接着电脑,MicroPython 有
ntptime模块,但 Pico 自身不带 WiFi,除非你用 Pico W 并联网。Pico W 可以通过 WiFi 连接 NTP 服务器获取时间,这个问题就迎刃而解。 - 手动设置一个起始时间:上电后从电脑串口输入当前时间,后续按 ticks 递增计算。
Pico W 用户可以直接这样拿到网络时间:
import ntptime import network wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect("你的SSID", "你的密码") ntptime.settime()然后通过time.localtime()获取真实时间。我早期做数据记录器时也走过弯路,用 ticks_ms 记录之后才发现没法对应具体时间,后面才补上 RTC 方案。建议你从一开始就决定好:只要是要长期记录,必须上真实时间。
6. 进阶玩法:从“能跑”到“好用”
6.1 数据格式升级:从 CSV 到带日期时间
前面提到,用纯 CSV 存数据最简单。你如果加了 RTC 模块,写入的每行可以带上完整时间格式,比如2025-01-15 14:30:00,26.32。这样导出后直接可视化,非常方便。
时间戳格式化可以这样写:
import time # 假设 RTC 已经设置好 t = time.localtime() timestamp = "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format( t[0], t[1], t[2], t[3], t[4], t[5] )6.2 增量记录:温度变化快时加密采样
固定频率采集的好处是简单,坏处是在温度平稳时浪费存储空间。更聪明的方式是“变化驱动”记录:温度变化超过阈值才记录,平稳期自动降低采集频率。
这个逻辑写起来也不复杂:
last_temp = read_temperature() last_record_temp = last_temp while True: temp = read_temperature() if abs(temp - last_record_temp) > 0.5: # 变化超过0.5℃才记录 append_temperature(temp) last_record_temp = temp time.sleep(5)这样可以大幅减少数据量,同时对温度变化的捕捉比固定间隔更灵敏。
6.3 把数据自动拷贝到电脑
如果你和我一样,懒得每次手动在 Thonny 里点下载,还可以写一个脚本自动拉取数据。关键工具就是 mpremote:
mpremote connect auto cp :temperature.csv ./data/$(date +%Y%m%d_%H%M%S).csv放到定时任务里,每天自动从 Pico 把数据拉取到电脑归档,Pico 端还可以继续记录新数据。这个用法适合长时间挂机运行的记录任务。
6.4 把 Pico 变成无线传感器
如果你用 Pico W,可以再进一步:把温度数据不仅写入文件,还定期通过 WiFi 发送到电脑或云端的 MQTT broker。文件系统仍然作为本地备份留存,网络作为实时通道。这种“本地存储 + 网络上报”的双通道方案,是很多 IoT 数据采集设备的标准做法。
核心代码大概是这样:
import network from umqtt.simple import MQTTClient wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect("SSID", "PASSWORD") client = MQTTClient("pico_temp", "192.168.1.100", port=1883) client.connect() # 每次采集后发送主题 client.publish("sensor/temperature", "{:.2f}".format(temp))本地文件继续存,网络消息也发送,两边都不耽误。
6.5 文件按天自动轮转
如果记录时间很长,所有数据堆在一个文件里,文件会越来越大,打开也越来越慢。可以做一个简单的“按天轮转”:每天 0 点新建一个以日期命名的文件,比如temp_20250115.csv。
实现思路是:每次写入前判断当前日期,如果和当前文件名的日期不一致,就新建文件。使用真实 RTC 时间才能做这个逻辑。
current_date = "{:04d}-{:02d}-{:02d}".format(t[0], t[1], t[2]) if current_date != CURRENT_DATE: DATA_FILE = "/temp_{}.csv".format(current_date) init_file() CURRENT_DATE = current_date这样每天的记录独立成文件,后期分析可按天归档,也不担心单个文件过大。
写在最后
这个项目看着简单,但麻雀虽小五脏俱全。做完一遍,你会熟悉 Pico 的 ADC 读取、MicroPython 的文件系统、文件读写在不同模式下的区别,还会踩一遍断电丢数据、文件被覆盖这类坑——这些经验在以后做更复杂的项目时都是通用的。
我个人实际操作下来的体会是:MicroPython 入门的捷径不是看一堆文档,而是尽快做出一个能独立运行、持续产出数据的小东西。温度记录器就是这样一个恰到好处的起点——“传感器采集、数据落地、导出分析”这三步环环相扣,每一步都有成就感。
最后分享一个小技巧:写完代码后,把main.py保存到 Pico 里,再按一下板上的 RESET 键。这样 Pico 会在上电时自动运行main.py,完全脱离电脑独立运行。插上充电宝,放在房间里,它就是一台小小的温度记录仪了。这个“脱机运行”的感觉,和连在电脑上跑程序是完全不一样的,建议你一定试一下。