
1. 从零开始CircuitPython环境搭建与核心概念如果你之前玩过Arduino可能会觉得C语言写起来有点“硬核”每次改个代码都得编译、上传调试起来也费劲。我第一次接触CircuitPython时感觉像是打开了一扇新的大门——它把Python的简洁和易读性直接带到了微控制器上。简单来说CircuitPython是Adafruit主导开发的一个开源Python解释器专门为像ESP32、RP2040这类微控制器设计。它的最大魅力在于“即插即用”你把支持CircuitPython的开发板比如Adafruit的Feather、QT Py系列通过USB连上电脑电脑上会直接弹出一个名为CIRCUITPY的U盘。你的代码文件code.py就放在这个盘里保存即运行修改代码就像在电脑上编辑文本文件一样直观。这种设计彻底改变了嵌入式开发的流程。你不再需要复杂的IDE集成开发环境和编译工具链任何一个能编辑文本的软件从记事本到VS Code都可以成为你的开发工具。这对于教育、快速原型制作以及创客项目来说极大地降低了入门门槛。整个开发体验非常“Pythonic”——你可以直接使用import来引入丰富的内置库和社区库控制GPIO、读取传感器、驱动显示屏代码逻辑清晰易懂。那么谁适合学习CircuitPython呢我认为主要有三类人首先是教育者和初学者Python本身语法友好加上这种免编译的即时反馈学习曲线非常平缓其次是创客和硬件爱好者你想快速验证一个物联网点子或者给一个小装置添加智能功能CircuitPython能让你专注于逻辑而非底层细节最后甚至是经验丰富的嵌入式工程师当你需要快速搭建一个概念验证PoC或者开发一个需要复杂逻辑但硬件资源要求不高的设备时CircuitPython也是一个高效的“瑞士军刀”。2. 核心工作流CIRCUITPY驱动管理与文件系统优化当你把刷好CircuitPython固件的板子连接到电脑后CIRCUITPY这个驱动器就是你与板子交互的主战场。理解并管理好这个空间是高效开发的第一步。2.1 理解CIRCUITPY的目录结构一个典型的CIRCUITPY驱动器刚连接时可能包含以下内容code.py: 这是主程序入口。板子上电或复位后会自动执行这个文件里的代码。boot.py: 这是一个在code.py之前运行的脚本。通常用于一些启动时的特殊配置比如在某些板子上设置特定引脚的状态。对于大多数入门项目你可以暂时忽略它。lib/目录: 这是存放第三方库的地方。CircuitPython的核心功能是内置的但当你需要驱动特定的传感器、显示屏或使用网络功能时就需要把对应的库文件通常是.mpy或.py文件放到这个文件夹里。其他用户文件: 你还可以存放字体文件.pcf或.bdf、图片、配置文件等。这个驱动器的空间通常很有限早期的一些板子可能只有2MB甚至更少。因此高效利用每一KB空间都至关重要。2.2 清理隐藏文件释放宝贵空间在MacOS系统下工作一个常见但容易被忽视的问题是系统自动生成的隐藏文件它们会悄无声息地占用CIRCUITPY的宝贵空间。这些文件通常以._为前缀例如._code.py是MacOS的AppleDouble格式文件用于存储资源派生信息如自定义图标在微控制器文件系统里完全无用。为什么必须用命令行清理你在Finder访达里是默认看不到这些隐藏文件的。即使开启了显示隐藏文件Command Shift .直接手动删除也容易遗漏而且操作繁琐。命令行工具提供了最直接、最彻底的控制。实操步骤与命令详解列出所有文件包括隐藏文件 打开你的终端Terminal首先需要导航到CIRCUITPY驱动器。它通常挂载在/Volumes/目录下。cd /Volumes/CIRCUITPY然后使用ls命令的-a参数来显示所有文件ls -la-l参数表示以长列表格式显示-a参数表示显示所有文件包括以.开头的隐藏文件。执行后你很可能会看到一堆._开头的文件。批量删除所有._文件 这是关键的一步。我们使用rm命令配合通配符*来一次性删除所有匹配的文件。rm ._*这个命令的意思是删除当前目录下所有以._开头的文件。通配符*可以匹配任意字符序列非常高效。验证空间释放 删除完成后可以使用df命令查看驱动器剩余空间。-h参数会让结果以人类易读的格式如K, M, G显示。df -h .注意命令最后的.它代表当前目录即/Volumes/CIRCUITPY。对比删除前后的可用空间你可能会发现多出了几十甚至上百KB这对于空间紧张的板子来说是一笔巨大的“财富”。注意rm命令是直接删除不会经过回收站。在执行rm ._*前请务必确认你当前所在的目录是/Volumes/CIRCUITPY并且你要删除的确实是那些._垃圾文件避免误删重要的code.py或库文件。一个安全的习惯是先执行ls ._*看看会匹配到哪些文件确认无误后再执行rm。2.3 设备异常恢复安全模式Safe Mode的使用即使是经验丰富的开发者也难免会写出让设备“卡死”的代码。常见的情况包括死循环while True循环内没有合理的延时或退出条件导致程序无法响应。硬件资源冲突错误地配置了同一个引脚或者以错误的方式访问了某个外设。内存耗尽在循环中不断创建对象而不释放导致内存泄漏。当你的code.py或boot.py中存在严重错误时板子可能表现为连接后CIRCUITPY驱动器不出现、LED无规律闪烁 boot loop 、或者串口无输出。这时常规的修改文件方法就失效了。安全模式Safe Mode就是为此设计的“救命稻草”。当板子进入安全模式后它会跳过执行code.py和boot.py但依然会挂载CIRCUITPY驱动器。这样你就可以像平常一样访问文件系统删除或修复有问题的代码文件。如何进入安全模式不同板子的具体操作略有不同但核心原理是在板子复位或上电的瞬间让系统检测到一个特定的触发条件。最常见的方法是对于大多数板子在按下复位Reset按钮的同时或复位后立即快速双击一下板子上的用户按钮Boot/User Button。有些板子如某些ESP32-S2型号可能需要你在复位时将某个特定引脚如GPIO0拉低到地GND。最准确的方法是查阅你所使用板子的具体指南。通常板子上的LED会以一种特殊的模式闪烁比如缓慢闪烁来指示已进入安全模式。此时电脑上应该会重新出现CIRCUITPY驱动器。进入安全模式后该怎么办打开CIRCUITPY驱动器。将code.py重命名为code.bak或其他名字或者直接删除它。如果怀疑是boot.py的问题也一并处理。安全弹出驱动器然后按一下板子的复位键。此时板子应该会正常启动因为没有主程序了并重新挂载驱动器。现在你可以重新创建一个简单的code.py比如只是一个print(“Hello”)来测试或者分析刚才重命名的code.bak文件找出问题所在。3. 网络功能实战WiFi连接与settings.toml配置让设备“上网”是很多物联网项目的基础。CircuitPython通过wifi、socketpool等库让网络连接变得非常简单。从CircuitPython 8开始官方推荐使用settings.toml文件来管理敏感信息这比之前的secrets.py更规范。3.1 创建并配置你的settings.toml文件settings.toml是一个纯文本配置文件采用TOML格式。它的核心目的是将WiFi密码、API密钥等敏感信息与你的主程序代码code.py分离。这样你可以放心地分享代码而不用担心泄露隐私。文件内容示例与解析在你的CIRCUITPY驱动器根目录下创建一个名为settings.toml的新文件用文本编辑器打开并输入以下内容# 这是一个注释以‘#’开头 CIRCUITPY_WIFI_SSID 你的WiFi名称 CIRCUITPY_WIFI_PASSWORD 你的WiFi密码 # 如果你使用Adafruit IO可以添加以下信息 ADAFRUIT_AIO_USERNAME 你的Adafruit IO用户名 ADAFRUIT_AIO_KEY 你的Adafruit IO Active Key # 你可以定义其他自定义变量 MY_API_TOKEN some_secret_token CITY_TIMEZONE Asia/Shanghai配置要点与避坑指南格式严格变量名如CIRCUITPY_WIFI_SSID在等号左边等号右边是字符串值必须用双引号括起来。整数不需要引号。变量名匹配这是最容易出错的地方代码里使用os.getenv(“CIRCUITPY_WIFI_SSID”)来获取值那么settings.toml里的变量名就必须一字不差地是CIRCUITPY_WIFI_SSID。有些教程或旧代码可能使用WIFI_SSID你需要根据你运行的代码示例来调整。文件位置settings.toml必须放在CIRCUITPY的根目录不能放在任何子文件夹里。字符编码如果需要存储非ASCII字符如中文WiFi名或emoji请确保你的文本编辑器以UTF-8 without BOM的格式保存文件。在VS Code中你可以在底部状态栏看到编码格式并点击进行更改。3.2 编写一个完整的网络测试程序理解了配置我们来写一个功能全面的网络测试脚本。这个脚本会依次执行扫描WiFi、连接指定网络、测试网络连通性Ping、获取网页文本内容、解析JSON数据。将以下代码保存为CIRCUITPY驱动器上的code.py# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries # SPDX-License-Identifier: MIT import os import time import ipaddress import ssl import wifi import socketpool import adafruit_requests # 定义要访问的测试URL TEXT_URL http://wifitest.adafruit.com/testwifi/index.html JSON_QUOTES_URL https://www.adafruit.com/api/quotes.php JSON_STARS_URL https://api.github.com/repos/adafruit/circuitpython print(CircuitPython网络客户端测试) print( * 40) # 1. 打印本机MAC地址 print(f本机MAC地址: {[hex(i) for i in wifi.radio.mac_address]}) # 2. 扫描并列出周围的WiFi网络 print(正在扫描可用的WiFi网络...) try: networks [] # 启动扫描返回一个可迭代对象 for network in wifi.radio.start_scanning_networks(): # 将SSID从bytes转换为字符串并记录信号强度(RSSI)和信道 networks.append((str(network.ssid, utf-8), network.rssi, network.channel)) wifi.radio.stop_scanning_networks() # 按信号强度从强到弱排序 networks.sort(keylambda x: x[1], reverseTrue) for ssid, rssi, channel in networks: # RSSI为负值越接近0信号越好 print(f\t{ssid:20} 信号强度: {rssi:4} dBm 信道: {channel}) except Exception as e: print(f扫描网络时出错: {e}) print( * 40) # 3. 连接WiFi (从settings.toml读取配置) wifi_ssid os.getenv(CIRCUITPY_WIFI_SSID) wifi_password os.getenv(CIRCUITPY_WIFI_PASSWORD) if not wifi_ssid or not wifi_password: print(错误: 请在 settings.toml 中设置 CIRCUITPY_WIFI_SSID 和 CIRCUITPY_WIFI_PASSWORD) while True: time.sleep(1) print(f正在连接网络: {wifi_ssid}) try: wifi.radio.connect(wifi_ssid, wifi_password) print(f连接成功!) print(f获取到的IP地址: {wifi.radio.ipv4_address}) except Exception as e: print(f连接失败: {e}) while True: time.sleep(1) print( * 40) # 4. 测试网络连通性 (Ping) print(正在测试网络连通性 (Ping 8.8.8.8)...) ping_target ipaddress.IPv4Address(8.8.8.8) # Google的公共DNS try: # ping操作返回值为延迟秒失败返回None delay wifi.radio.ping(ipping_target) if delay is None: # 第一次失败重试一次 delay wifi.radio.ping(ipping_target) if delay is not None: print(fPing 成功! 延迟: {delay * 1000:.2f} 毫秒) else: print(Ping 失败但可能不影响后续HTTP访问。) except Exception as e: print(fPing测试异常: {e}) print( * 40) # 5. 初始化网络会话 pool socketpool.SocketPool(wifi.radio) # 创建一个requests会话用于HTTP请求 requests_session adafruit_requests.Session(pool, ssl.create_default_context()) # 6. 获取并显示普通网页文本 print(f正在获取文本内容从: {TEXT_URL}) try: response requests_session.get(TEXT_URL) # .text属性获取响应内容的文本形式 print(网页内容 (前200字符):) print(- * 40) print(response.text[:200]) print(- * 40) response.close() # 记得关闭响应释放资源 except Exception as e: print(f获取文本失败: {e}) # 7. 获取并解析JSON数据 (名言接口) print(f\n正在获取JSON数据从: {JSON_QUOTES_URL}) try: response requests_session.get(JSON_QUOTES_URL) # .json()方法将响应内容解析为Python字典或列表 data response.json() print(收到一条名言:) print(- * 40) # 该接口返回一个列表里面包含字典 if isinstance(data, list) and len(data) 0: quote data[0] print(f作者: {quote.get(author, 未知)}) print(f内容: {quote.get(text, 无)}) response.close() except Exception as e: print(f获取名言失败: {e}) # 8. 获取并解析更复杂的JSON (GitHub API) print(f\n正在获取GitHub项目信息从: {JSON_STARS_URL}) try: response requests_session.get(JSON_STARS_URL) data response.json() print(CircuitPython仓库信息:) print(- * 40) print(f项目全名: {data.get(full_name)}) print(f星标(Stars)数量: {data.get(stargazers_count)}) print(f仓库描述: {data.get(description)}) response.close() except Exception as e: print(f获取GitHub信息失败: {e}) print(\n * 40) print(网络测试全部完成)代码逐段解析与注意事项导入模块wifi和socketpool是核心网络模块。adafruit_requests是一个第三方库需要提前放入lib文件夹它提供了类似Python标准库requests的易用接口。网络扫描wifi.radio.start_scanning_networks()返回一个生成器遍历它可以获得所有扫描到的网络对象。务必在扫描结束后调用stop_scanning_networks()来释放无线电资源否则可能影响后续连接。连接WiFi使用os.getenv()从settings.toml读取配置。这里加了错误检查如果没找到配置程序会报错并停止避免后续操作失败。Ping测试wifi.radio.ping()是一个简单的连通性测试。注意并非所有网络都允许ICMP协议Ping有些防火墙会禁用它。因此Ping失败不一定代表网络不可用所以代码里只是警告并继续执行HTTP测试。HTTP请求使用adafruit_requests库。关键点在于每次get()请求后如果不再需要响应体最好显式调用response.close()。虽然在某些情况下垃圾回收会处理但在内存受限的设备上主动管理资源是好习惯。错误处理网络操作极易因信号、配置、服务器问题而失败。用try...except包裹关键步骤并给出有意义的错误提示对于调试至关重要。运行这个脚本通过串口监视器如Mu编辑器、VS Code的串口插件或screen / cu命令查看输出。你会看到从扫描网络到获取网络数据的完整流程这为你构建任何网络应用打下了坚实基础。4. 融入开源海洋如何为CircuitPython社区做贡献CircuitPython的强大一半在于其优雅的设计另一半则源于其活跃、友好的开源社区。参与贡献不仅是回馈更是深入学习、结识同道中人的绝佳途径。贡献的形式多种多样远不止提交代码。4.1 从Discord开始融入社区讨论Adafruit的Discord服务器是社区的心脏。这里聚集了从世界各地的初学者到核心开发者的所有人。#help-with-circuitpython这是提问的黄金频道。无论你的问题多么基础比如“为什么我的LED不亮”都可以在这里发问。提问时请尽量提供详细信息你用的什么板子完整的错误信息是什么你的code.py内容是什么一张接线图往往抵得上千言万语。#show-and-tell展示你的作品无论是完成了第一个闪烁LED还是做了一个复杂的天气站分享出来。收获点赞和鼓励是持续创作的重要动力你的项目也可能启发他人。#circuitpython-dev如果你对开发本身感兴趣可以在这里参与关于新功能、库架构等更深层次的讨论。在Discord贡献的秘诀贡献不一定是回答问题。当你看到一个自己也曾遇到过并已解决的问题时可以分享你的解决步骤。当你看到别人展示酷炫的项目时不吝啬一句“Awesome”。这种积极的氛围营造本身就是对社区极有价值的贡献。4.2 通过GitHub进行代码与文档贡献GitHub是协作开发的核心平台。CircuitPython生态系统主要分为两部分用C语言编写的核心在circuitpython主仓库和用Python编写的库众多以Adafruit_CircuitPython_开头的仓库。对于初学者我强烈建议从库Libraries的贡献开始因为Python更易读易懂而且每个库功能相对独立容易入手。贡献路径一解决现有问题Issues访问 CircuitPython库贡献门户circuitpython.org/contributing。这个页面聚合了所有库的待处理事项。点击“Open Issues”标签页。这里列出了所有库需要帮助的问题。使用标签筛选器。找到“Good first issue”标签并点击。这些问题通常是文档改进、简单的bug修复或功能增强范围明确非常适合新手。选择一个感兴趣的问题阅读描述。如果涉及硬件确认你手头有对应的传感器或板子。在Discord相应的频道或该Issue下留言表示你想尝试解决它。这可以避免重复劳动有时维护者还会给你一些额外的指导。Fork仓库在本地进行修改编写测试如果原有测试的话然后提交Pull Request (PR)。PR的描述中应清晰说明你修改了什么以及为什么这样修改。贡献路径二审查他人的代码Pull Request Review这是另一种极其重要且能快速学习的方式。在贡献门户的“Pull Requests”标签页下有很多等待审查的PR。你可以阅读代码变更思考逻辑是否正确代码风格是否符合项目规范CircuitPython有明确的Black代码格式化要求和pre-commit检查。即使你无法用硬件测试也可以检查文档字符串的格式、示例代码的语法、README的拼写错误。在PR下留下一条有建设性的评论比如“代码逻辑我看懂了没问题”或者“这里有个拼写错误建议修改”都是非常棒的贡献。这能大大减轻核心维护者的负担。贡献路径三翻译Localization如果你掌握英语以外的语言可以通过Weblate平台帮助翻译CircuitPython核心的错误信息和用户界面字符串。这让非英语用户能获得更好的体验。翻译工作对编程技能要求不高但需要对技术术语有准确的理解。4.3 提交有效的错误报告Bug Report当你使用CircuitPython或某个库时遇到了问题提交一个清晰的错误报告是巨大的贡献。一个糟糕的报告是“XX库不能用。” 一个优秀的报告应包含标题简要概括问题如“adafruit_bme280在读取湿度时在特定板子上返回恒定值”。环境硬件型号如Adafruit Feather ESP32-S3CircuitPython版本在REPL中输入import os; os.uname()查看涉及的库及其版本在lib文件夹查看复现步骤提供能100%复现问题的最简代码Minimal Reproducible Example。import board, time, adafruit_bme280 i2c board.I2C() sensor adafruit_bme280.Adafruit_BME280_I2C(i2c) while True: print(fHumidity: {sensor.humidity:.2f} %) time.sleep(1)实际结果描述你看到的现象如“湿度始终显示0.00%”并附上完整的错误回溯信息Traceback。期望结果描述你认为正常的行为应该是什么。额外信息接线图照片、你已尝试过的排查步骤等。这样一份报告能帮助开发者快速定位问题效率极高。5. 项目实战从Blink到网络天气站掌握了基础我们来串联一个综合性的小项目一个能通过网络获取天气信息并显示在板载屏幕上的简易天气站。我们以一块带有显示屏的ESP32-S3板为例。5.1 硬件与库准备硬件Adafruit Feather ESP32-S3 TFT或其他带WiFi和显示屏的板子。所需库确保你的CIRCUITPY/lib文件夹下有adafruit_display_textadafruit_bitmap_font部分字体可能需要adafruit_requests用于连接特定天气API的库例如adafruit_io如果使用Adafruit IO或通用的json库CircuitPython已内置。5.2 代码实现获取并显示天气我们将创建一个code.py它每小时从公共天气API例如Open-Meteo获取一次数据并在屏幕上实时显示。import os import time import wifi import socketpool import adafruit_requests import board import displayio from adafruit_display_text import label import terminalio # --- 网络配置从settings.toml读取--- SSID os.getenv(CIRCUITPY_WIFI_SSID) PASSWORD os.getenv(CIRCUITPY_WIFI_PASSWORD) # 使用Open-Meteo免费API获取柏林当前天气可替换为你的城市 LAT 52.52 LON 13.41 WEATHER_URL fhttps://api.open-meteo.com/v1/forecast?latitude{LAT}longitude{LON}current_weathertrue # --- 显示初始化 --- display board.DISPLAY splash displayio.Group() display.root_group splash # 创建文本标签 title_label label.Label(terminalio.FONT, textWeather Station, color0xFFFFFF, x10, y20) temp_label label.Label(terminalio.FONT, textTemp: -- °C, color0x00FF00, x10, y50) wind_label label.Label(terminalio.FONT, textWind: -- km/h, color0x0000FF, x10, y80) update_label label.Label(terminalio.FONT, textLast: --:--, color0xFFFF00, x10, y110) for lbl in [title_label, temp_label, wind_label, update_label]: splash.append(lbl) # --- 网络连接 --- def connect_wifi(): print(Connecting to WiFi...) wifi.radio.connect(SSID, PASSWORD) print(Connected! IP:, wifi.radio.ipv4_address) def fetch_weather(): 获取天气数据并更新显示 try: pool socketpool.SocketPool(wifi.radio) requests adafruit_requests.Session(pool, ssl.create_default_context()) response requests.get(WEATHER_URL) data response.json() response.close() current data.get(current_weather, {}) temperature current.get(temperature, N/A) windspeed current.get(windspeed, N/A) weathercode current.get(weathercode, 0) # WMO天气代码 # 更新显示 temp_label.text fTemp: {temperature} °C wind_label.text fWind: {windspeed} km/h update_label.text fLast: {time.localtime()[3]:02d}:{time.localtime()[4]:02d} # 根据天气代码简单改变标题颜色 (例如下雨为蓝色晴天为黄色) if weathercode 80: # 阴天、下雨等 title_label.color 0x4444FF else: title_label.color 0xFFFF00 print(fWeather updated: {temperature}C, {windspeed}km/h) return True except Exception as e: print(Failed to fetch weather:, e) update_label.text Update Failed update_label.color 0xFF0000 return False # --- 主循环 --- connect_wifi() last_fetch time.monotonic() - 3600 # 设置为1小时前促使立即获取一次 while True: now time.monotonic() # 每3600秒1小时获取一次天气 if now - last_fetch 3600: if fetch_weather(): last_fetch now else: # 如果失败5分钟后重试 time.sleep(300) # 短暂休眠以降低功耗同时保持响应 time.sleep(0.1)项目要点与优化方向API选择我们使用了无需API密钥的Open-Meteo。对于生产项目你可能需要考虑请求频率限制、数据的准确性或者使用其他提供更丰富数据的API如OpenWeatherMap需要注册获取API key并妥善存放在settings.toml。错误处理网络请求可能因各种原因失败。代码中使用了try...except来捕获异常并在失败时更新界面提示而不是让程序崩溃。功耗考虑主循环中的time.sleep(0.1)让CPU大部分时间在休眠降低了功耗。对于电池供电的设备可以进一步使用ESP32的深度睡眠模式每小时唤醒一次获取数据这样功耗可以做到极低。显示优化这里使用了内置的terminalio.FONT它是一种点阵字体。你可以将更漂亮的.pcf或.bdf字体文件放入CIRCUITPY根目录或fonts/文件夹并使用adafruit_bitmap_font加载让界面更美观。扩展功能你可以很容易地添加更多信息显示如湿度、天气图标用displayio绘制简单图形、未来几小时预报解析API中hourly数据等。这个项目虽然小但涵盖了CircuitPython开发的典型流程硬件初始化、网络连接、数据获取、解析、用户交互和错误处理。你可以以此为骨架扩展出无限可能。