
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 ExpressArduino 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(/, readonlyswitch.value)。这里的readonly参数是针对CircuitPython而言的。当switch.value为True引脚悬空时readonlyTrue。这意味着对CircuitPython来说文件系统是只读的但对你的电脑是可写的。这是编程模式你可以自由地在电脑上编辑code.py和其他文件。当switch.value为False引脚接地时readonlyFalse。这意味着对CircuitPython来说文件系统是可写的但对你的电脑变成了只读。这是数据记录模式你的程序可以安全地向CIRCUITPY写入数据文件而不用担心电脑的误操作导致冲突。一个关键细节boot.py只在硬复位拔插USB、按物理复位键时运行。在串行控制台中按CtrlD进行软复位或者保存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(/, readonlyswitch.value)代码解读与选型指南引脚选择代码中提供了三种常见板型的引脚配置。D2是许多板子的通用选择。Feather系列使用D5是因为该引脚在标准Feather引脚布局中更易接触。Circuit Playground Express (CPX) 是最方便的选择因为它直接使用板载的滑动开关D7无需外接任何线缆。将开关滑到右侧靠近耳朵图标D7读取为False进入数据记录模式滑到左侧靠近音乐图标读取为True回到编程模式。switch.pull digitalio.Pull.UP这行代码启用了内部上拉电阻。当引脚悬空时电阻将其电压拉高到逻辑高电平True当引脚通过导线连接到GND时电压被拉低到逻辑低电平False。这是一种非常简洁的硬件“开关”实现。storage.remount(/, readonlyswitch.value)这是核心函数。/代表根文件系统即CIRCUITPY。readonlyswitch.value将引脚的电平状态直接传递给readonly参数。实操心得在编写和测试boot.py时一个常见的困惑是“我改了boot.py怎么好像没生效”记住boot.py只在硬复位时运行。修改boot.py后你需要在电脑上安全弹出CIRCUITPY驱动器。按下板子上的物理复位按钮或者拔插USB线。板子重启后boot.py中的新配置才会被应用。在串口终端里按CtrlD进行软复位是没用的。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 M0LED引脚是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: # 错误码28ENOSPC (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 软件部署与模式切换操作步骤请严格按照以下步骤操作这是避免文件系统损坏的关键初始部署编程模式:确保控制引脚未连接GNDCPX开关在左侧。将编写好的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将板载滑动开关拨到右侧。执行硬复位按下板子上的物理复位按钮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.确保执行了硬复位按物理按钮或重新上电而非软复位(CtrlD)。 2. 检查boot.py中的引脚电平逻辑是否正确。接地时应为False。 3. 对于CPX确认开关拨动方向与代码注释一致右False左True。文件内容混乱或重复程序在异常复位后重启但文件以“a”模式打开。这是正常现象数据会追加。如果希望每次启动是新文件可在boot.py中根据某个条件如检测特定标志文件决定在code.py中使用“w”模式覆盖写入或生成带时间戳的文件名。最后的实操心得这个项目最精妙的地方在于用极简的硬件一个引脚或一个开关和软件两个小文件解决了一个嵌入式开发中的典型矛盾——开发便利性与运行时独立性的矛盾。boot.py作为启动仲裁者其设计思想可以迁移到很多场景。例如你可以用拨码开关代替单引脚接地来实现多种启动模式的选择或者用光敏电阻、按钮等作为条件实现更复杂的启动逻辑。掌握了这个模式你就掌握了让CircuitPython设备在“开发者玩具”和“独立工作节点”之间自由切换的钥匙。