1. 项目概述与核心价值
如果你正在用像Adafruit Feather M0、Circuit Playground Express这类小巧的微控制器板子做项目,大概率遇到过这样的需求:设备需要独立运行,采集一些数据(比如温度、湿度、光照),然后把这些数据存下来,等方便的时候再连上电脑导出分析。这时候,一个可靠、简单的数据记录器(Data Logger)就成了刚需。今天要聊的,就是如何用CircuitPython,快速打造一个基于内置CPU温度传感器的数据记录应用。这听起来可能有点“杀鸡用牛刀”——CPU温度?但对嵌入式开发来说,这是一个绝佳的入门案例。它几乎零成本(无需外接传感器),却能让你完整走通数据采集、格式处理、文件存储、系统状态指示这一整套流程,理解CircuitPython如何与硬件、文件系统交互。
这个项目的核心,是利用了ATSAMD21、ATSAMD51或nRF52840这类微控制器内部集成的温度传感器。通过CircuitPython的microcontroller模块,我们能用一两行代码就读到它。难点和精髓不在于“读”,而在于“存”。因为当你的板子通过USB连接到电脑时,它通常以“U盘”(CIRCUITPY驱动器)的形式出现,方便你编辑代码。但这就产生了一个矛盾:你的电脑和板子上的CircuitPython程序,能同时往这个U盘里写文件吗?答案是不能,强行这么做会导致文件系统损坏。所以,我们需要一个“开关”机制,让板子知道:“现在该我写数据了,电脑你别动。”这个“开关”就是boot.py文件和一个小小的硬件引脚(或板载开关)。通过这个项目,你不仅能学会读取传感器数据,更能掌握CircuitPython中文件系统权限管理的核心机制,这是构建任何离线数据记录设备的基石。
2. 硬件准备与核心原理拆解
2.1 硬件选型与兼容性分析
不是所有CircuitPython板子都适合这个项目,关键看两点:第一,微控制器是否内置温度传感器;第二,是否是“Express”版本或具有足够的存储空间。
主流支持板型:
- Feather M0 Express / Feather M4 Express:非常经典的选择,引脚丰富,且有额外的2MB SPI闪存专供存储,不会占用运行内存。
- Metro M0 Express / Metro M4 Express:Arduino UNO外形,适合插在面包板上使用,同样具备Express的存储优势。
- Circuit Playground Express / Bluefruit:自带滑动开关和NeoPixel灯,特别适合本项目,无需外接任何线缆即可切换模式。
- ItsyBitsy M0 Express / M4 Express:体积小巧,功能齐全。
- 非Express板型(如Trinket M0, Gemma M0, QT Py M0):需要注意。这些板子没有额外的专用存储芯片,文件系统和程序代码共享内部闪存。虽然可以运行,但可用空间非常有限(约50KB),且不支持
audioio等高级模块。对于简单的温度记录,如果数据量不大(比如一天记录几千条),勉强可行,但长期运行或记录更多数据就捉襟见肘了。
注意:选择硬件时,优先考虑“Express”系列板卡。它们多出来的2MB存储空间,不仅能让你存储更多数据,也为将来扩展功能(如图形、音频、更多库)留足了余地。这多花的几美元在项目复杂度提升时会显得非常值得。
核心传感器原理:我们读取的microcontroller.cpu.temperature,读取的是芯片内核(CPU)的温度,而非环境温度。这个传感器通常位于芯片内部,用于监测芯片自身的工作温度,防止过热。因此,它的读数会受芯片自身功耗、运行频率影响。在静止状态下,它可能比环境温度高5-15摄氏度;当CPU全速运算时,温度会显著上升。所以,它更适合监测设备自身的健康状态,而非精确的环境测温。对于nRF52840芯片,还需要注意其温度传感器分辨率为0.25摄氏度,所以读数值会是0.25的整数倍。
2.2 文件系统与boot.py的权限管理机制
这是本项目最需要理解的核心概念。CircuitPython设备连接电脑后,出现的CIRCUITPY驱动器,实际上是一个“串行大容量存储设备”(USB Mass Storage Device)的虚拟实现。为了让这个驱动器既能被电脑读写(方便编程),又能被CircuitPython程序读写(记录数据),必须引入一个仲裁机制。
为什么需要boot.py?默认情况下,当板子通过USB连接到电脑时,CIRCUITPY驱动器的控制权(写权限)是交给电脑的。你的CircuitPython程序(code.py)运行时,如果尝试打开文件写入,会收到“只读文件系统”的错误。boot.py是一个特殊的文件,它在CircuitPython启动时(硬复位或重新上电)最先执行,早于code.py。它的任务之一,就是根据某个条件(比如一个物理开关的状态),来决定将驱动器的写权限分配给谁。
权限切换的逻辑:
- 条件检测:在
boot.py中,我们初始化一个数字输入引脚(如board.D2),并为其启用上拉电阻。当这个引脚通过跳线或开关连接到GND(地)时,引脚读取到低电平(False);悬空时,上拉电阻将其拉至高电平(True)。 - 重新挂载:调用
storage.remount("/", readonly=switch.value)。这里的readonly参数是针对CircuitPython而言的。- 当
switch.value为True(引脚悬空)时,readonly=True。这意味着对CircuitPython来说,文件系统是只读的,但对你的电脑是可写的。这是编程模式,你可以自由地在电脑上编辑code.py和其他文件。 - 当
switch.value为False(引脚接地)时,readonly=False。这意味着对CircuitPython来说,文件系统是可写的,但对你的电脑变成了只读。这是数据记录模式,你的程序可以安全地向CIRCUITPY写入数据文件,而不用担心电脑的误操作导致冲突。
- 当
一个关键细节:boot.py只在硬复位(拔插USB、按物理复位键)时运行。在串行控制台中按Ctrl+D进行软复位,或者保存code.py文件触发的自动重载,都不会执行boot.py。这意味着,一旦你通过接地进入数据记录模式并复位,就必须先弹出CIRCUITPY驱动器,然后物理复位板子,才能再次切换回编程模式。这个设计强制你进行安全操作,避免在数据写入过程中意外断开。
3. 软件环境搭建与代码详解
3.1 基础环境准备
首先,确保你的板子已经安装了最新版本的CircuitPython。访问 circuitpython.org/downloads ,找到对应板子的最新.uf2文件。进入板子的引导加载程序模式(通常是快速双击复位键),将下载的.uf2文件拖入出现的BOOT驱动器即可完成安装。
安装后,电脑上会出现一个名为CIRCUITPY的驱动器,里面已经有code.py等基础文件。我们接下来的所有操作都在这个驱动器内进行。
3.2boot.py文件:权限的守门员
在CIRCUITPY驱动器的根目录下,创建一个名为boot.py的新文件。这个文件的内容决定了板子的启动行为。
# SPDX-FileCopyrightText: 2017 Limor Fried for Adafruit Industries # SPDX-License-Identifier: MIT """CircuitPython Essentials Storage logging boot.py file""" import board import digitalio import storage # 根据你的板子型号,选择正确的引脚 # 对于 Gemma M0, Trinket M0, Metro M0/M4 Express, ItsyBitsy M0/M4 Express switch = digitalio.DigitalInOut(board.D2) # 对于 Feather M0/M4 Express,取消下面一行的注释,并注释掉上面一行 # switch = digitalio.DigitalInOut(board.D5) # 对于 Circuit Playground Express/Bluefruit,取消下面一行的注释,并注释掉上面一行 # switch = digitalio.DigitalInOut(board.D7) switch.direction = digitalio.Direction.INPUT switch.pull = digitalio.Pull.UP # 关键操作:根据引脚状态重新挂载根文件系统 # 如果引脚连接到GND(值为False),则CircuitPython可获得写权限 storage.remount("/", readonly=switch.value)代码解读与选型指南:
- 引脚选择:代码中提供了三种常见板型的引脚配置。
D2是许多板子的通用选择。Feather系列使用D5是因为该引脚在标准Feather引脚布局中更易接触。Circuit Playground Express (CPX) 是最方便的选择,因为它直接使用板载的滑动开关(D7),无需外接任何线缆。将开关滑到右侧(靠近耳朵图标),D7读取为False,进入数据记录模式;滑到左侧(靠近音乐图标),读取为True,回到编程模式。 switch.pull = digitalio.Pull.UP:这行代码启用了内部上拉电阻。当引脚悬空时,电阻将其电压拉高到逻辑高电平(True);当引脚通过导线连接到GND时,电压被拉低到逻辑低电平(False)。这是一种非常简洁的硬件“开关”实现。storage.remount("/", readonly=switch.value):这是核心函数。/代表根文件系统(即CIRCUITPY)。readonly=switch.value将引脚的电平状态直接传递给readonly参数。
实操心得:在编写和测试boot.py时,一个常见的困惑是:“我改了boot.py,怎么好像没生效?”记住,boot.py只在硬复位时运行。修改boot.py后,你需要:
- 在电脑上安全弹出
CIRCUITPY驱动器。 - 按下板子上的物理复位按钮(或者拔插USB线)。
- 板子重启后,
boot.py中的新配置才会被应用。在串口终端里按Ctrl+D进行软复位是没用的。
3.3code.py文件:数据记录的核心逻辑
这是我们的主程序,负责读取温度并写入文件。在CIRCUITPY根目录下创建或替换code.py。
# SPDX-FileCopyrightText: 2017 Limor Fried for Adafruit Industries # SPDX-License-Identifier: MIT """CircuitPython Essentials Storage logging example""" import time import board import digitalio import microcontroller # 初始化板载LED作为状态指示器 # 对于大多数CircuitPython板子: led = digitalio.DigitalInOut(board.LED) # 对于 QT Py M0,LED引脚是SCK,取消下面一行的注释: # led = digitalio.DigitalInOut(board.SCK) led.switch_to_output() try: # 以追加模式打开文件。如果文件不存在则创建,存在则在末尾追加。 with open("/temperature.txt", "a") as fp: while True: # 1. 读取CPU温度(单位:摄氏度) temp_c = microcontroller.cpu.temperature # 2. (可选)转换为华氏度 # temp_f = temp_c * (9 / 5) + 32 # 3. 将数据写入文件 # 使用格式化字符串,将浮点数写入,并换行 fp.write('{0:f}\n'.format(temp_c)) # 写入摄氏度 # fp.write('{0:f}\n'.format(temp_f)) # 如果要写华氏度,用这行 # 4. 立即将数据从缓冲区刷入文件,防止断电丢失 fp.flush() # 5. 翻转LED状态,指示一次记录完成 led.value = not led.value # 6. 等待1秒 time.sleep(1) except OSError as e: # 捕获文件系统错误,最常见的是不可写(错误码28是磁盘满) delay = 0.5 # 默认错误闪烁频率 if e.args[0] == 28: # 错误码28:ENOSPC (No space left on device) delay = 0.25 # 磁盘满了,加快闪烁频率以示警告 # 进入错误处理循环,不断闪烁LED while True: led.value = not led.value time.sleep(delay)代码深度解析:
文件打开模式 (
“a”): 使用追加模式“a”打开temperature.txt文件。这是数据记录器的标准做法。每次打开文件,写入指针都会位于文件末尾,新的数据会接在旧数据后面,不会覆盖之前记录的内容。如果你希望每次运行都重新开始记录,可以使用“w”写入模式,但这会清空原有文件。数据格式化与写入:
fp.write(‘{0:f}\n’.format(temp_c))这行代码做了几件事:{0:f}是一个格式化字段,表示将第一个参数(temp_c)格式化为浮点数(fixed-point)。\n是换行符。每个温度读数独占一行,这样生成的是一个标准的文本文件,每行一个数据点,后期用Excel、Python pandas或任何文本编辑器都能轻松处理。- 为什么不用简单的
fp.write(str(temp_c) + “\n”)?格式化字符串format方法更清晰,也更容易控制输出格式,比如可以指定小数位数{0:.2f}(保留两位小数)。
fp.flush()的重要性: 在写入文件时,操作系统和CircuitPython为了效率,通常会先将数据放在内存缓冲区,等攒到一定量再一次性写入磁盘。但在嵌入式数据记录场景下,突然断电是常事。fp.flush()方法强制将缓冲区中的数据立即写入物理存储。虽然这会增加一点开销并影响闪存寿命(每次写入都执行擦写),但对于确保数据不丢失至关重要。在记录关键数据时,这个调用不能省。状态指示LED: 让板载LED在每次成功写入数据后闪烁一次,这是一个极其有用的调试和状态指示手段。在黑暗中,你能一眼就知道设备是否在正常工作。如果LED常亮或常灭,说明程序可能卡在某个地方或者出错了。
异常处理 (
try…except OSError): 这是生产级代码的体现。数据记录器可能遇到各种问题:boot.py没配置好导致文件系统只读、存储空间耗尽、文件系统损坏等。用try…except包裹主循环,能捕获这些异常并进入一个优雅的错误处理状态(这里让LED以不同频率闪烁),而不是让程序彻底崩溃且无声无息。错误码28对应ENOSPC,即磁盘空间不足,这是一个需要特别关注的错误。
4. 完整部署与操作流程
4.1 硬件连接(非CPX板子)
对于使用Feather、Metro、ItsyBitsy等板子,你需要通过一根跳线或鳄鱼夹来连接“开关”引脚到GND。
- 确认引脚:根据你的板子型号,查看
boot.py中配置的引脚(例如Feather M4是D5)。 - 准备连接:找一根母-母杜邦线或鳄鱼夹线。
- 连接GND:将线的一端连接到板子上任何一个标有“GND”的引脚。
- 连接控制引脚:将线的另一端连接到
boot.py中指定的引脚(如D5)。- 重要:在连接之前,确保板子没有通电,或者处于编程模式(引脚悬空)。连接这根线,就相当于按下了“开始记录”的开关。
对于Circuit Playground Express,这一步完全省去,你只需要使用板载的滑动开关。
4.2 软件部署与模式切换操作步骤
请严格按照以下步骤操作,这是避免文件系统损坏的关键:
初始部署(编程模式):
- 确保控制引脚未连接GND(CPX开关在左侧)。
- 将编写好的
boot.py和code.py文件复制到CIRCUITPY驱动器根目录。 - 此时,你应该能在电脑上正常打开、编辑这两个文件。打开串行控制台(如Mu编辑器、PuTTY等),你会看到程序开始运行,但会立即抛出
OSError,因为此时CircuitPython没有写入权限,code.py中的try块会捕获到这个错误,LED开始以0.5秒间隔闪烁。这是正常现象,说明boot.py正在保护文件系统。
切换到数据记录模式:
- 在电脑上,安全弹出
CIRCUITPY驱动器。在Windows上点击“弹出”,在macOS上拖入垃圾桶,在Linux上umount。这一步至关重要! - 物理操作硬件“开关”:
- 对于非CPX板子:用跳线将指定的控制引脚(如
D5)与GND引脚连接起来。 - 对于CPX:将板载滑动开关拨到右侧。
- 对于非CPX板子:用跳线将指定的控制引脚(如
- 执行硬复位:按下板子上的物理复位按钮(Reset)。或者,更彻底的方法是拔掉USB线,再重新插上。
- 板子重启后,
boot.py会检测到引脚接地(False),将文件系统挂载为对CircuitPython可写。 - 此时,
CIRCUITPY驱动器可能会重新出现在电脑上,但如果你尝试向里面复制文件或修改code.py,系统会提示“磁盘被写保护”或类似错误。这就对了!说明现在CircuitPython获得了写权限。 - 观察板载LED,它应该开始以1秒的稳定节奏闪烁。同时,在
CIRCUITPY根目录下,会出现一个新的temperature.txt文件。由于文件写入不是实时同步到USB驱动器的,你可能需要稍等片刻或再次安全弹出后重新连接,才能在电脑上看到文件内容更新。
- 在电脑上,安全弹出
停止记录与数据导出:
- 当你需要停止记录并读取数据时,首先在电脑上安全弹出
CIRCUITPY驱动器。 - 物理操作硬件“开关”,断开记录状态:
- 非CPX板子:拔掉连接控制引脚和GND的跳线。
- 对于CPX:将滑动开关拨回左侧。
- 再次执行硬复位(按复位键或重新插拔USB)。
- 现在,
CIRCUITPY驱动器恢复为电脑可写状态。你可以打开temperature.txt文件,里面按行存储了所有的温度记录(时间戳需要你根据记录间隔自己在后期添加)。你可以用文本编辑器查看,或者用Python脚本、Excel进行数据分析。
- 当你需要停止记录并读取数据时,首先在电脑上安全弹出
4.3 数据文件处理与分析示例
记录一段时间后,你的temperature.txt文件内容可能如下:
28.75 28.50 28.75 29.00 29.25 ...这是一个纯文本文件,非常容易处理。这里提供一个简单的Python脚本示例,用于在电脑上读取并分析这个文件:
# analyze_temp.py import matplotlib.pyplot as plt temperatures = [] with open('temperature.txt', 'r') as f: for line in f: try: # 每行一个温度值,转换为浮点数 temp = float(line.strip()) temperatures.append(temp) except ValueError: continue # 跳过非数字行 if temperatures: print(f"共记录 {len(temperatures)} 个数据点") print(f"平均温度: {sum(temperatures)/len(temperatures):.2f} °C") print(f"最高温度: {max(temperatures):.2f} °C") print(f"最低温度: {min(temperatures):.2f} °C") # 绘制温度变化曲线 plt.plot(temperatures) plt.title('CPU Temperature Log') plt.xlabel('Sample Number') plt.ylabel('Temperature (°C)') plt.grid(True) plt.show() else: print("未找到有效温度数据。")5. 高级优化与常见问题排查
5.1 功能扩展与优化建议
基础的记录器已经完成,但一个健壮的数据记录系统还可以考虑以下优化:
添加时间戳:原始数据只有温度值,没有时间信息。你可以在
code.py中集成一个简单的实时时钟(RTC)模块,如DS3231,或者在每次记录时使用time.monotonic()记录一个相对时间戳(从开机开始的秒数)。后期分析时,你需要知道记录间隔才能还原时间轴。import time start_time = time.monotonic() # 在循环内: current_time = time.monotonic() - start_time fp.write(f'{current_time:.1f}, {temp_c:.2f}\n') # 写入“相对时间,温度”降低功耗:如果使用电池供电,每秒记录一次可能太耗电。可以增加
time.sleep()的间隔(如60秒记录一次),或者在循环中使用microcontroller.cpu.sleep()等深度睡眠功能,并在外部中断(如定时器)触发时唤醒进行记录。文件轮转:防止单个文件过大。可以检查文件大小,当超过某个阈值(如100KB)时,关闭当前文件,重命名(如
temperature_1.txt),然后创建一个新的temperature.txt继续记录。import os file_size = os.stat("/temperature.txt")[6] # 获取文件大小(字节) if file_size > 100 * 1024: # 大于100KB fp.close() os.rename("/temperature.txt", "/temperature_1.txt") fp = open("/temperature.txt", "a") # 重新打开新文件更健壮的错误恢复:当前的错误处理只是闪烁LED。可以考虑在捕获到“磁盘满”错误后,尝试删除最旧的数据文件,或者进入一个极低功耗的报警模式。
5.2 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| LED快速闪烁(0.25秒间隔) | 存储空间已满 (ENOSPC)。 | 1. 切换到编程模式,导出temperature.txt文件并备份。 2. 删除驱动器上不必要的文件(如旧的日志、.pyc缓存文件)。 3. 检查代码是否在异常生成超大文件。 |
| LED慢速闪烁(0.5秒间隔) | 文件系统对CircuitPython只读。 | 1.确认已安全弹出驱动器并硬复位。这是最常见的原因。 2. 检查boot.py中引脚配置是否正确。 3. 用万用表或代码检查控制引脚是否确实接地(输出False)。 4. 确认boot.py文件是否存在且无语法错误。 |
| LED不亮或常亮 | 程序未运行或卡死。 | 1. 检查串行控制台是否有错误输出(需在编程模式下)。 2. 检查code.py语法,特别是try块外的代码。 3. 尝试用最简单的LED闪烁代码测试板子是否正常。 |
电脑无法识别CIRCUITPY驱动器 | USB驱动问题、线缆问题或板子故障。 | 1. 更换USB数据线(确保能传输数据,而非仅充电)。 2. 尝试电脑其他USB端口。 3. 双击复位键进入UF2引导模式,看是否出现BOOT驱动器。若能,可重新刷写CircuitPython固件。 |
| 记录的数据全是0或异常值 | 传感器读取问题或代码错误。 | 1. 在编程模式下,通过REPL直接输入import microcontroller; print(microcontroller.cpu.temperature),检查原始读数是否正常。 2. 检查代码中温度变量名是否正确,计算过程是否有误(如单位转换)。 |
| 切换模式后,电脑仍可写入文件 | boot.py未生效。 | 1.确保执行了硬复位(按物理按钮或重新上电),而非软复位(Ctrl+D)。 2. 检查boot.py中的引脚电平逻辑是否正确。接地时应为False。 3. 对于CPX,确认开关拨动方向与代码注释一致(右False,左True)。 |
| 文件内容混乱或重复 | 程序在异常复位后重启,但文件以“a”模式打开。 | 这是正常现象,数据会追加。如果希望每次启动是新文件,可在boot.py中根据某个条件(如检测特定标志文件)决定在code.py中使用“w”模式覆盖写入,或生成带时间戳的文件名。 |
最后的实操心得:这个项目最精妙的地方在于用极简的硬件(一个引脚或一个开关)和软件(两个小文件)解决了一个嵌入式开发中的典型矛盾——开发便利性与运行时独立性的矛盾。boot.py作为启动仲裁者,其设计思想可以迁移到很多场景。例如,你可以用拨码开关代替单引脚接地,来实现多种启动模式的选择;或者用光敏电阻、按钮等作为条件,实现更复杂的启动逻辑。掌握了这个模式,你就掌握了让CircuitPython设备在“开发者玩具”和“独立工作节点”之间自由切换的钥匙。