1. 项目概述与核心价值如果你刚开始接触嵌入式开发面对琳琅满目的传感器、通信协议和存储需求可能会感到无从下手。今天我想分享一个基于CircuitPython的综合性实践项目它串联了从模拟信号采集、数字通信到数据存储的多个核心环节。CircuitPython作为MicroPython的一个分支以其极低的入门门槛和丰富的硬件抽象库成为了快速原型开发的利器。这个项目不仅仅是几个代码片段的堆砌而是试图还原一个真实的开发场景如何让一块微控制器板感知环境模拟输入与I2C传感器、实现人机交互电容触摸与NeoPixel状态指示并可靠地记录数据文件系统存储。我们将使用一块常见的、支持CircuitPython的开发板例如Adafruit Feather系列或Seeed Studio XIAO系列通过电位器、电容触摸引脚、I2C温度传感器和板载NeoPixel LED构建一个微型的“环境监测与交互终端”。无论你是想了解嵌入式开发的基本工作流还是希望为你的物联网设备寻找可靠的数据采集方案这篇文章都能提供从硬件连接到软件实现的完整路径和避坑指南。2. 硬件选型与电路设计思路2.1 核心控制器与外围器件选型选择一款合适的微控制器是项目的第一步。对于CircuitPython项目我强烈推荐选择那些官方支持良好、社区资源丰富的板卡例如Adafruit的Feather系列、SparkFun的Thing Plus系列或者国内较易获取的Seeed Studio XIAO系列如XIAO RP2040或XIAO ESP32C3。这些板卡通常预装了CircuitPython固件并且其引脚定义已被board模块完美支持能省去大量底层配置的麻烦。本次实践我以一块Adafruit Feather RP2040为例它基于RP2040双核处理器拥有充足的GPIO、模拟输入和I2C引脚并且板载了NeoPixel LED和用户按键非常适合我们的综合实验。外围器件方面我们选择了以下几样10kΩ旋转电位器用于模拟输入实验。它是一个经典的三端器件两端接电源和地中间抽头输出分压电压。选择10kΩ是因为其阻值适中电流消耗小与微控制器的ADC输入阻抗匹配良好能提供稳定的读数。MCP9808高精度I2C温度传感器选择它而非更常见的DS18B20或DHT11原因在于I2C通信的简洁性仅需两根线以及MCP9808极高的精度±0.25°C。它自带STEMMA QT/Qwiic接口使用4芯防反插连接线极大简化了接线避免了接错线的风险。这对于快速验证和可靠部署至关重要。1MΩ电阻若干用于电容触摸引脚下拉。这是电容触摸检测可靠工作的关键。许多教程会省略这个电阻导致触摸检测不稳定或误触发。注意关于上拉/下拉电阻的选择I2C总线需要上拉电阻通常2.2kΩ-10kΩ而电容触摸需要下拉电阻1MΩ。幸运的是大多数现代I2C传感器模块包括MCP9808和开发板如Feather都已内置了I2C上拉电阻。但电容触摸的下拉电阻通常需要外接。务必根据数据手册和实际情况判断缺失必要的电阻是导致通信失败或触摸不灵的最常见硬件原因。2.2 电路连接与原理图解析正确的硬件连接是软件工作的基础。下面我将详细说明每个部分的连接方式和背后的电气原理。电位器连接将电位器的两端引脚分别连接到开发板的3.3V和GND。将中间抽头滑动端连接到开发板的A0引脚或其他任何标有ADC功能的引脚如A1,A2等。原理电位器在此充当一个可调电阻分压器。当旋转旋钮时中间抽头相对于GND的电压会在0V到3.3V之间线性变化。微控制器的ADC引脚将这个连续的电压信号采样并转换为一个数字值。MCP9808传感器连接使用一根4芯STEMMA QT连接线一端插入开发板的STEMMA QT端口另一端插入MCP9808模块的端口。引脚对应关系连接线内部已经固定好3.3V-VIN,GND-GND,SDA-SDA,SCL-SCL。这种防呆设计彻底杜绝了接错线的可能。原理I2C是同步、半双工、多主多从的串行总线。SCL是时钟线由控制器我们的开发板产生同步数据传输节奏。SDA是数据线用于双向传输数据。每个从设备如MCP9808都有一个唯一的7位地址MCP9808默认为0x18控制器通过地址来选中与之通信的设备。电容触摸引脚连接选择两个支持电容触摸的GPIO引脚例如A3和D24。在每个引脚与GND之间连接一个1MΩ的电阻。原理微控制器的电容触摸功能本质上是测量该引脚对地的电容微小变化。当手指靠近或触摸该引脚或连接的导体如一根导线或一块铜箔时会引入额外的对地电容导致RC充电时间常数变化。内部电路通过测量充放电时间的变化来判定“触摸”事件。1MΩ的下拉电阻为引脚提供了一个确定的直流偏置确保在未触摸时引脚处于稳定的低电平状态提高抗干扰能力和检测稳定性。NeoPixel LED这是板载的无需额外连接。整个系统的连接示意图如下文字描述[Feather RP2040] | |-- 3.3V --- [Potentiometer Pin 1] |-- GND --- [Potentiometer Pin 3] [1MΩ Resistor to GND for A3/D24] |-- A0 --- [Potentiometer Wiper (Pin 2)] | |-- STEMMA QT Port ---(4-wire cable)--- [MCP9808 Sensor] | (内含 SCL, SDA, 3.3V, GND) | |-- A3 --- [1MΩ Resistor] --- GND (Touch Pin 1) |-- D24 -- [1MΩ Resistor] --- GND (Touch Pin 2) | -- (板载 NeoPixel LED)3. 软件环境配置与核心库解析3.1 CircuitPython固件与驱动安装首先确保你的开发板已刷入最新版本的CircuitPython固件。前往CircuitPython官网根据你的板卡型号下载对应的.uf2文件。将板卡置于Bootloader模式通常通过双击复位按钮实现此时电脑会出现一个名为RPI-RP2对于RP2040或类似的可移动磁盘将下载的.uf2文件拖入即可完成刷写。刷写成功后磁盘名会变为CIRCUITPY。接下来是库文件的准备。CircuitPython的核心功能如time,board,analogio是内置的但像neopixel、adafruit_mcp9808这样的设备专用库需要手动放置。访问Adafruit的CircuitPython Library Bundle发布页面下载与你固件版本匹配的库包。解压后你会看到一个lib文件夹。对于本项目你需要将以下库文件或文件夹复制到CIRCUITPY磁盘的lib目录下neopixel.mpy用于控制NeoPixel LED。adafruit_mcp9808.mpyMCP9808传感器的驱动库。可选adafruit_bus_device一些I2C设备库的依赖通常adafruit_mcp9808会需要它请一并放入。实操心得库文件管理不要将整个庞大的库包解压到CIRCUITPY盘这会导致空间不足。只复制你项目必需的库。另外注意.mpy文件是预编译的字节码比.py源文件体积更小、加载更快优先使用。如果遇到ImportError首先检查库文件是否放对了位置CIRCUITPY/lib/其次检查库的版本是否与CircuitPython固件版本兼容。3.2 核心Python模块深度解析理解你将导入的模块是写出健壮代码的前提。board这是硬件抽象层的核心。它定义了当前这块开发板上所有引脚的用户友好名称。例如board.A0、board.SCL、board.D24。使用board模块而非硬编码引脚编号保证了代码在不同型号但功能相似的Adafruit板卡间的可移植性。analogio提供模拟输入输出功能。我们主要使用AnalogIn类。创建analogio.AnalogIn(board.A0)对象后通过读取其.value属性可以获得一个0到65535之间的整数对于16位ADC对应0V到参考电压通常是3.3V的输入。touchio用于电容触摸检测。TouchIn对象初始化后其.value属性在触摸时为True。其内部通过测量引脚电容变化来实现下拉电阻对于稳定检测至关重要。busio/board.I2C()busio是底层总线协议模块I2C, SPI, UART。board.I2C()是一个便利函数它返回一个基于默认SCL/SDA引脚配置好的busio.I2C单例对象。对于有STEMMA QT接口的板子board.STEMMA_I2C()是更好的选择它明确使用了该连接器的I2C总线。neopixel控制WS2812B等可寻址RGB LED。初始化时需要指定引脚和LED数量。亮度(brightness)是0.0-1.0的浮点数。颜色通过RGB元组(R, G, B)设置每个分量范围0-255。storage这是一个关键模块用于管理CIRCUITPY文件系统的读写状态。它允许我们在代码中挂载文件系统为只读或可写是实现数据记录功能的核心。microcontroller提供对微控制器底层功能的访问例如读取CPU温度microcontroller.cpu.temperature。4. 核心功能实现与代码逐行解读4.1 模拟电压读取与ADC原理实践让我们从最简单的模拟输入开始。将电位器连接到A0引脚后创建code.py文件输入以下代码import time import board import analogio # 1. 初始化模拟输入对象 analog_pin analogio.AnalogIn(board.A0) def get_voltage(pin): 将ADC原始值转换为电压值 (假设参考电压为3.3V) # 2. ADC分辨率Feather RP2040的ADC为16位最大值65535对应参考电压。 # 电压 (原始值 / 最大计数值) * 参考电压 return (pin.value * 3.3) / 65535 while True: # 3. 读取并打印原始值和电压值 raw_value analog_pin.value voltage get_voltage(analog_pin) print(fRaw: {raw_value:6d} | Voltage: {voltage:.2f} V) time.sleep(0.1) # 降低采样率便于观察代码解读与注意事项初始化analogio.AnalogIn()会配置指定引脚的硬件ADC通道。在RP2040上多个模拟引脚可能复用到同一个ADC硬件单元但库会帮你处理。转换函数get_voltage函数是理解ADC工作的关键。pin.value读取的是ADC转换后的数字量。公式电压 (原始值 / 65535) * 3.3V基于一个理想假设ADC是线性的且参考电压Vref就是3.3V。对于大多数3.3V系统微控制器这个假设成立。打印与延时使用f-string格式化输出清晰易读。time.sleep(0.1)将循环速度降至10Hz避免串口监视器数据刷屏过快。在实际数据记录应用中你可能需要更高的采样率但要注意处理数据的速度。常见问题排查读数跳动即使电位器不动最后几位数字也可能轻微跳动。这是正常的ADC噪声。可以通过软件滤波如连续采样多次取平均来平滑。例如def read_avg(pin, samples10): total 0 for _ in range(samples): total pin.value time.sleep(0.001) # 微小延时确保采样独立 return total / samples电压范围不全你可能发现旋转到头电压也达不到0V或3.3V。这通常是由于电位器本身的端到端电阻、ADC的输入阻抗以及参考电压的微小偏差造成的。只要变化范围足够例如0.2V - 3.1V在代码中做线性映射即可。引脚无读数检查接线是否牢固电位器中间抽头是否确实接到了ADC引脚两端是否接了电源和地。用万用表测量中间抽头电压确认其随旋转变化。4.2 NeoPixel状态指示与色彩控制板载的NeoPixel LED是一个极佳的状态指示器。我们将用它来显示系统状态例如数据记录中、触摸触发等。import time import board import neopixel from rainbowio import colorwheel # 用于生成彩虹色 # 初始化NeoPixel。参数1控制引脚参数2LED数量板载通常为1个 pixel neopixel.NeoPixel(board.NEOPIXEL, 1) # 设置亮度1.0最亮但非常刺眼。0.3是个舒适的值。 pixel.brightness 0.3 def set_status_color(mode): 根据模式设置LED颜色 colors { idle: (0, 10, 0), # 暗绿色表示系统待机 logging: (0, 0, 20), # 暗蓝色表示正在记录数据 touch: (20, 0, 0), # 暗红色表示触摸事件 error: (30, 30, 0), # 黄色表示错误如I2C通信失败 } pixel.fill(colors.get(mode, (0, 0, 0))) # 默认为熄灭 def rainbow_cycle(wait): 彩虹循环效果可用于启动自检或特定庆祝状态 for j in range(255): pixel[0] colorwheel(j) pixel.show() time.sleep(wait) # 启动自检显示彩虹效果一次 rainbow_cycle(0.02) set_status_color(idle) # 在主循环中可以根据不同条件调用 set_status_color()关键点解析pixel.fill((R, G, B))这是设置颜色的主要方法。参数是一个包含红、绿、蓝三分量的元组每个值在0-255之间。(0,0,0)为熄灭。亮度控制brightness属性在颜色输出前进行全局缩放。这意味着即使你设置颜色为(255,0,0)如果亮度是0.5实际输出的PWM占空比也只有50%LED会显示为暗红色。务必在设置颜色前配置好亮度。colorwheel函数它接受一个0-255的整数返回一个对应的RGB元组非常适合生成平滑的彩虹色过渡。省电与寿命长时间点亮LED尤其是白色全亮会消耗较多电流并可能影响LED寿命。在仅需状态指示时使用低亮度、低饱和度的颜色。4.3 电容触摸检测的实现与抗干扰设计电容触摸提供了无需机械按钮的交互方式。实现代码如下import time import board import touchio # 初始化两个触摸输入通道并指定对应的引脚 touch_pin_1 touchio.TouchIn(board.A3) touch_pin_2 touchio.TouchIn(board.D24) # 可选设置触摸阈值如果库支持。有些库的TouchIn对象有.threshold属性。 # 默认阈值通常是自动校准的但在某些噪声环境下可能需要手动调整。 # touch_pin_1.threshold 2000 # 示例值需要根据实测调整 last_touch_state_1 False last_touch_state_2 False debounce_time 0.05 # 50毫秒防抖时间 last_debounce_time_1 0 last_debounce_time_2 0 while True: current_time time.monotonic() raw_touch_1 touch_pin_1.value raw_touch_2 touch_pin_2.value # 防抖处理 for Pin 1 if raw_touch_1 ! last_touch_state_1: last_debounce_time_1 current_time if (current_time - last_debounce_time_1) debounce_time: if raw_touch_1: print(Touch Pad 1 Activated!) # 触发相关动作例如改变NeoPixel颜色 # 更新稳定后的状态 last_touch_state_1 raw_touch_1 # 防抖处理 for Pin 2 (同理) if raw_touch_2 ! last_touch_state_2: last_debounce_time_2 current_time if (current_time - last_debounce_time_2) debounce_time: if raw_touch_2: print(Touch Pad 2 Activated!) last_touch_state_2 raw_touch_2 time.sleep(0.01) # 主循环短延时深度解析与抗干扰技巧下拉电阻的必要性代码中并未体现电阻但硬件上必须连接1MΩ下拉电阻到地。这个电阻为触摸引脚提供了一个确定的直流路径确保在未触摸时引脚处于明确的低电平状态防止因浮空输入而导致的误触发或读数不稳定。软件防抖电容触摸容易受到环境噪声如电源纹波、电磁干扰的影响导致value在True和False之间快速抖动。上面的代码实现了一个简单的状态防抖逻辑只有当触摸状态的变化持续超过debounce_time如50ms时才认为是一次有效的触摸事件。这是工业级应用中保证可靠性的关键一步。阈值校准某些CircuitPython实现允许设置.threshold。你可以先运行一个校准程序分别读取触摸和未触摸时的原始计数值通常是touch_pin_1.raw_value然后取一个中间值作为阈值。这能适应不同的触摸介质如导线长度、覆铜面积。time.monotonic()的使用这里使用time.monotonic()而非time.sleep()进行时间差计算是因为它不受系统时间调整的影响且是单调递增的非常适合用于测量时间间隔。4.4 I2C通信扫描、寻址与传感器数据读取I2C是连接多个传感器的骨干。我们分两步走先扫描总线确认设备存在再读取数据。步骤一I2C总线扫描在连接任何I2C设备前进行总线扫描是一个好习惯可以验证硬件连接和设备地址。import time import board # 方法1使用板载默认I2C引脚 i2c board.I2C() # 使用 board.SCL 和 board.SDA # 方法2推荐如果板子有STEMMA QT接口使用专用连接器总线 # i2c board.STEMMA_I2C() while not i2c.try_lock(): pass # 等待并获得I2C总线锁 try: print(Scanning I2C bus...) # i2c.scan() 返回一个包含所有发现的7位地址的列表 addresses i2c.scan() if not addresses: print(No I2C devices found!) else: print(I2C devices found at addresses:, [hex(addr) for addr in addresses]) # 常见的传感器地址 # MCP9808: 0x18 # BMP280: 0x76 or 0x77 # SSD1306 OLED: 0x3C # 你可以根据发现的地址判断设备是否正常连接 finally: i2c.unlock() # 非常重要必须释放总线锁 time.sleep(2)步骤二使用专用库读取MCP9808数据扫描到设备地址应为0x18后使用其专用库进行数据读取会更简单可靠。import time import board import adafruit_mcp9808 # 初始化I2C总线同上 i2c board.I2C() # 或 board.STEMMA_I2C() # 初始化传感器对象 # 注意adafruit_mcp9808库会自动处理I2C通信细节 sensor adafruit_mcp9808.MCP9808(i2c) # 可选配置传感器参数查看库文档以了解可用选项 # sensor.resolution 3 # 例如设置分辨率模式 (0-3) print(MCP9808 Temperature Sensor Initialized.) print(Resolution:, sensor.resolution) while True: try: # 直接读取温度值摄氏度。库函数内部完成了所有寄存器读取和转换。 temp_c sensor.temperature temp_f temp_c * 9.0 / 5.0 32.0 # 转换为华氏度 print(fTemperature: {temp_c:.2f} °C / {temp_f:.2f} °F) # 你可以根据温度值触发其他操作例如 # if temp_c 30: # set_status_color(warning) # # 或者通过I2C控制其他设备... except OSError as e: # I2C通信错误处理 print(fI2C communication error: {e}) set_status_color(error) # 可以尝试重新初始化传感器 # sensor adafruit_mcp9808.MCP9808(i2c) time.sleep(2) # 每2秒读取一次I2C实践要点与排错总线锁try_lock()和unlock()是必须的因为在CircuitPython中I2C总线是共享资源。确保在finally块中解锁即使发生异常也能释放总线防止系统死锁。地址冲突确保总线上每个I2C设备的地址是唯一的。有些传感器如BMP280有地址选择引脚可以通过拉高或拉低来改变地址。上拉电阻如果使用非STEMMA QT的杜邦线连接且传感器模块没有内置上拉电阻你必须在SCL和SDA线上各接一个2.2kΩ到10kΩ的电阻到3.3V。没有上拉电阻I2C总线无法正常工作。线长与干扰I2C总线对电容敏感。过长的连接线30cm或过多的并联设备会增加总线电容导致波形畸变和通信失败。在长距离或干扰环境可以考虑降低I2C时钟频率在busio.I2C初始化时指定frequency参数或使用电平转换器和屏蔽线。4.5 数据存储文件系统操作与boot.py机制让CircuitPython直接向自身的CIRCUITPY驱动器写入数据是实现离线数据记录Data Logging的关键。这需要理解一个特殊的机制boot.py。原理CIRCUITPY盘通常被你的电脑挂载为可读写模式。如果CircuitPython运行时也尝试写入这个盘就会造成冲突可能导致文件系统损坏。因此我们需要一个方法在CircuitPython启动时将文件系统设置为对电脑只读对CircuitPython可写。这个任务由boot.py完成。创建 boot.py 在CIRCUITPY盘的根目录下创建一个名为boot.py的文件如果不存在。这个文件在板子每次硬复位断电重启或按复位键时自动运行早于code.py。# boot.py - 在CircuitPython启动时运行 import board import digitalio import storage # 假设我们使用板载的一个按钮例如BOOT/USR按钮来控制写入模式。 # 如果按钮按下接地则允许CircuitPython写入电脑只读。 # 如果按钮未按下上拉为高则允许电脑写入CircuitPython只读。 # 请根据你的板子修改按钮引脚。 write_pin digitalio.DigitalInOut(board.BUTTON) # Feather RP2040上的用户按钮 write_pin.switch_to_input(pulldigitalio.Pull.UP) # 启用内部上拉电阻 # 关键操作重新挂载根文件系统。 # readonly参数是从CircuitPython的角度看的 # 当 write_pin.value False (按钮按下) 时readonlyFalseCircuitPython可写电脑只读。 # 当 write_pin.value True (按钮未按下) 时readonlyTrueCircuitPython只读电脑可写。 storage.remount(/, readonlywrite_pin.value) # 提示当前模式 if write_pin.value: print(Boot: CIRCUITPY is READ-ONLY to CircuitPython (Computer can write).) else: print(Boot: CIRCUITPY is WRITABLE to CircuitPython (Computer is read-only).)创建数据记录 code.py 现在在boot.py将文件系统设置为CircuitPython可写的模式下即按钮按下时启动你的code.py就可以安全地创建和写入文件了。# code.py - 主程序记录温度数据 import time import board import microcontroller import digitalio # 初始化状态LED led digitalio.DigitalInOut(board.LED) led.switch_to_output() # 尝试打开/创建日志文件 log_file_path /temperature_log.txt header_written False try: # 以追加模式(a)打开文件。如果文件不存在则创建。 with open(log_file_path, a) as log_file: # 如果是新文件写入表头 if log_file.tell() 0: # 文件指针在开头说明是新文件或空文件 log_file.write(timestamp,temperature_c\n) header_written True print(Log file header written.) print(Starting temperature logging...) log_count 0 while True: # 1. 采集数据 temp_c microcontroller.cpu.temperature # 读取MCU内部温度 # 或者使用之前连接的MCP9808: temp_c sensor.temperature timestamp time.monotonic() # 获取一个单调递增的时间戳 # 2. 格式化数据行 # 使用逗号分隔值(CSV)格式便于后续用Excel或Python分析 data_line f{timestamp:.1f},{temp_c:.2f}\n # 3. 写入文件 log_file.write(data_line) log_file.flush() # 立即将数据从缓冲区写入磁盘防止断电丢失 # 4. 状态指示与输出 led.value True # 写入时点亮LED log_count 1 if log_count % 10 0: # 每10次记录打印一次到串口 print(fLogged: {data_line.strip()} | Total entries: {log_count}) time.sleep(0.5) # 短暂亮灯 led.value False # 5. 记录间隔 time.sleep(9.5) # 总共10秒间隔 except OSError as e: # 文件系统错误处理例如文件系统未以可写模式挂载 print(fFilesystem error: {e}) print(Make sure boot.py is configured correctly and the write-enable pin/button is active.) # 让LED快速闪烁表示错误 while True: led.value not led.value time.sleep(0.2)数据记录模式工作流程准备阶段按住板载用户按钮BUTTON然后给板子上电或按复位键。此时boot.py检测到按钮按下valueFalse以readonlyFalse模式挂载文件系统即CircuitPython可写电脑只读。串口会打印相应提示。记录阶段松开按钮。code.py开始运行每10秒将时间戳和温度数据追加写入temperature_log.txt文件。写入瞬间LED会闪烁一下。此时如果你将板子通过USB连接到电脑你会发现CIRCUITPY盘是只读的无法直接删除或修改temperature_log.txt文件这保护了数据不被意外破坏。数据读取阶段需要读取数据时先确保板子没有在记录状态即code.py没有在运行或已通过CtrlC停止。然后按住用户按钮并按复位键。boot.py检测到按钮按下但这次它会以readonlyTrueCircuitPython只读电脑可写模式挂载。这样你就可以在电脑上自由地复制、查看或删除日志文件了。极其重要的警告绝对不要在电脑可以写入CIRCUITPY盘即文件系统对CircuitPython只读时运行试图写文件的code.py程序。这会导致OSError通常是错误码30只读文件系统。更危险的是如果两个系统同时写入极有可能导致文件系统损坏丢失所有数据甚至需要重新格式化CIRCUITPY盘。boot.py机制就是为了强制避免这种冲突。5. 系统集成与高级应用示例将以上所有功能集成到一个完整的应用中可以构建一个智能的环境监测节点。# code.py - 综合应用环境监测与交互终端 import time import board import analogio import touchio import neopixel import adafruit_mcp9808 import microcontroller import digitalio import storage # 1. 硬件初始化 print(System Initializing...) # NeoPixel状态灯 pixel neopixel.NeoPixel(board.NEOPIXEL, 1) pixel.brightness 0.2 pixel.fill((0, 5, 0)) # 初始化绿色 # 模拟输入 (电位器) potentiometer analogio.AnalogIn(board.A0) # 电容触摸 (两个通道) touch_pad_1 touchio.TouchIn(board.A3) touch_pad_2 touchio.TouchIn(board.D24) # I2C温度传感器 try: i2c board.STEMMA_I2C() # 使用STEMMA QT总线 temp_sensor adafruit_mcp9808.MCP9808(i2c) print(fMCP9808 found at 0x{temp_sensor._device_address:02x}) i2c_ok True except (ValueError, RuntimeError, OSError) as e: print(fFailed to initialize MCP9808: {e}) print(Using internal CPU temperature as fallback.) i2c_ok False temp_sensor None # 文件系统状态检查 (依赖于boot.py的配置) # 我们假设boot.py已正确配置这里只做提醒 print(Filesystem ready for logging. if not storage.getmount(/).readonly else WARNING: Filesystem is read-only to CircuitPython. Cannot log data.) # 用户按钮 (用于控制日志开关) log_button digitalio.DigitalInOut(board.BUTTON) log_button.switch_to_input(pulldigitalio.Pull.UP) # 2. 全局变量与状态机 logging_active False last_log_time 0 log_interval 10.0 # 日志间隔(秒) last_touch_state {pad1: False, pad2: False} touch_debounce_delay 0.05 # 3. 辅助函数 def read_voltage(analog_in): 读取并返回电压值带软件滤波 samples 5 total 0 for _ in range(samples): total analog_in.value time.sleep(0.001) return (total / samples) * 3.3 / 65535 def set_led_color(state): 根据系统状态设置LED颜色 colors { idle: (0, 10, 0), # 绿 - 待机 logging: (0, 0, 15), # 蓝 - 记录中 touch1: (15, 5, 0), # 橙 - 触摸1触发 touch2: (10, 0, 10), # 紫 - 触摸2触发 error: (20, 20, 0), # 黄 - 错误 warning: (20, 10, 0), # 橙黄 - 警告 } pixel.fill(colors.get(state, (0, 0, 0))) def log_data(timestamp, voltage, temp_ext, temp_int): 将数据记录到CSV文件 if storage.getmount(/).readonly: print(Cannot log: Filesystem is read-only.) return False try: with open(/sensor_log.csv, a) as f: if f.tell() 0: # 新文件写表头 f.write(timestamp,voltage_v,temp_ext_c,temp_int_c\n) f.write(f{timestamp:.1f},{voltage:.3f},{temp_ext:.2f},{temp_int:.2f}\n) f.flush() return True except OSError as e: print(fLog write failed: {e}) set_led_color(error) return False # 4. 主循环 print(Entering main loop.) set_led_color(idle) while True: current_time time.monotonic() # --- 读取所有传感器数据 --- pot_voltage read_voltage(potentiometer) if i2c_ok and temp_sensor: try: external_temp temp_sensor.temperature except OSError: external_temp -999.0 # 错误标志值 set_led_color(warning) else: external_temp -999.0 internal_temp microcontroller.cpu.temperature # --- 处理电容触摸输入 (带防抖) --- raw_touch_1 touch_pad_1.value raw_touch_2 touch_pad_2.value # 简单的防抖逻辑 if raw_touch_1 and not last_touch_state[pad1]: print(Touch Pad 1 pressed!) set_led_color(touch1) # 这里可以关联一个动作例如改变日志间隔 # log_interval max(1.0, pot_voltage) # 用电压控制间隔示例 last_touch_state[pad1] True elif not raw_touch_1: last_touch_state[pad1] False if raw_touch_2 and not last_touch_state[pad2]: print(Touch Pad 2 pressed!) set_led_color(touch2) # 另一个动作例如切换NeoPixel亮度模式 # pixel.brightness 0.5 if pixel.brightness 0.5 else 0.1 last_touch_state[pad2] True elif not raw_touch_2: last_touch_state[pad2] False # --- 处理日志按钮 --- if not log_button.value: # 按钮按下 (接地) if not logging_active: logging_active True last_log_time current_time print(Data logging STARTED.) set_led_color(logging) # 检查是否到达记录间隔 if current_time - last_log_time log_interval: if log_data(current_time, pot_voltage, external_temp, internal_temp): last_log_time current_time # 快速闪烁一下表示写入成功 pixel.brightness 0.4 time.sleep(0.05) pixel.brightness 0.2 else: if logging_active: logging_active False print(Data logging STOPPED.) set_led_color(idle) # --- 串口输出状态信息 (降低频率避免刷屏) --- if int(current_time) % 5 0: # 每5秒输出一次 print(f[{current_time:.1f}s] V: {pot_voltage:.2f}V | fT_ext: {external_temp:.1f}C | T_int: {internal_temp:.1f}C | fLogging: {ON if logging_active else OFF}) time.sleep(0.1) # 防止一秒内多次打印 time.sleep(0.05) # 主循环延迟降低CPU占用这个集成示例展示了如何将各个模块协调工作构建一个具有状态指示、用户交互、环境感知和数据记录功能的完整系统。通过状态机管理日志开关防抖处理触摸输入以及全面的错误处理使得系统更加健壮适合作为更复杂项目的起点。