1. 项目概述:为什么选择CircuitPython?
如果你之前接触过Arduino或者MicroPython,可能会觉得微控制器编程总是绕不开复杂的C/C++语法、繁琐的编译烧录流程,以及让人头疼的内存管理。CircuitPython的出现,正是为了解决这些问题。它本质上是一个基于Python 3的开源微控制器解释器,由Adafruit主导开发并维护。它的核心价值在于,让嵌入式开发变得像在电脑上写Python脚本一样简单直观。
想象一下,你拿到一块支持CircuitPython的开发板(比如Adafruit的Feather系列、Circuit Playground Express等),当你用USB线将它连接到电脑时,电脑上会直接弹出一个名为CIRCUITPY的U盘。你的代码文件code.py就放在这个U盘里。你只需要用任何文本编辑器修改这个文件,保存,代码就会自动在板子上重启运行。整个过程没有编译,没有烧录工具,就像在修改一个文本文档一样自然。这种“编辑-保存-运行”的即时反馈循环,极大地加速了原型开发和调试过程,尤其适合教育、艺术装置、物联网设备原型等需要快速迭代的场景。
我最初被CircuitPython吸引,就是因为它的“无感部署”。你不需要安装复杂的IDE,不需要配置编译链,甚至初学者用系统自带的记事本都能开始编程。这种极低的入门门槛,并不意味着它功能薄弱。相反,通过其强大的库生态系统,你可以轻松驱动各种传感器、显示屏、执行器,处理网络请求,甚至实现USB HID设备(如键盘、鼠标)功能。接下来,我将从最基础的代码编辑讲起,带你走完从点亮一个LED到管理复杂项目依赖的全流程。
2. 核心工作流:代码编辑、保存与自动重载
CircuitPython的核心体验建立在“文件即代码”的简单哲学上。理解并熟练运用这一工作流,是高效开发的基础。
2.1 理解code.py与自动执行机制
当你将CircuitPython固件刷入开发板后,首次连接电脑,CIRCUITPY驱动器内通常会有一个默认的code.py文件。这个文件就是板子上电或复位后自动执行的入口脚本。CircuitPython会按照code.txt->code.py->main.txt->main.py的顺序寻找并执行第一个找到的文件。虽然code.py是推荐名称,但了解其他选项很重要。一个常见的坑是,如果你不小心创建了main.py,即使你一直在修改code.py,板子也只会执行main.py。所以,如果你的修改似乎没有生效,第一件事就是检查CIRCUITPY根目录下是否有其他优先级更高的代码文件。
让我们从最经典的“Hello, World!”硬件版——闪烁LED开始。将以下代码保存为CIRCUITPY驱动器根目录下的code.py:
import board import digitalio import time # 初始化板载LED引脚(大多数Adafruit板子都适用) led = digitalio.DigitalInOut(board.LED) led.direction = digitalio.Direction.OUTPUT while True: led.value = True # LED亮 time.sleep(0.5) # 等待0.5秒 led.value = False # LED灭 time.sleep(0.5) # 等待0.5秒保存文件后,你会立刻看到板载LED开始以1秒为周期(亮0.5秒,灭0.5秒)闪烁。这就是CircuitPython的“热重载”特性:当你保存code.py时,解释器会检测到文件变化,自动重启并运行新代码。
2.2 实时代码编辑与参数调整实践
现在,让我们进行第一次交互式实验。打开你的code.py,找到time.sleep(0.5)这两行。尝试将第一个0.5改为0.1,然后保存。
while True: led.value = True time.sleep(0.1) # 改为0.1秒 led.value = False time.sleep(0.5)保存瞬间,LED的闪烁行为就改变了:它会非常短暂地亮一下(0.1秒),然后熄灭0.5秒。接着,把第二个0.5也改成0.1:
while True: led.value = True time.sleep(0.1) led.value = False time.sleep(0.1) # 也改为0.1秒现在LED会以很高的频率(周期0.2秒)闪烁,由于人眼的视觉暂留,你可能会觉得它是在持续发光但亮度较低。反之,如果你把两个参数都改成1.0,LED就会以2秒为周期缓慢闪烁。
实操心得:理解阻塞与非阻塞延迟
time.sleep()是一个“阻塞”函数,它会让整个程序暂停指定的秒数。在简单的闪烁LED例子中这没问题,但在需要同时处理多个任务(比如同时读取传感器和响应按钮)时,长时间的sleep会导致程序“卡住”。在后续进阶项目中,我们会介绍使用time.monotonic()进行非阻塞定时的方法。
这个简单的练习演示了CircuitPython开发的核心循环:编辑 -> 保存 -> 观察硬件反应。所有复杂的项目迭代都建立在这个快速反馈之上。
2.3 禁用自动重载以应对特殊场景
绝大多数情况下,自动重载(Auto-reload)是我们需要的。但在某些特殊场景下,比如你正在通过串口与板子进行复杂交互,或者代码重启会导致外部硬件状态异常时,你可能希望临时禁用它。这可以通过在code.py文件开头添加两行代码实现:
import supervisor supervisor.runtime.autoreload = False # 你其余的代码... import board import digitalio ...设置autoreload = False后,保存code.py将不会触发自动重启。你需要手动复位板子(按复位键)或通过串口发送CTRL+D(后面会讲到)来重新加载代码。请注意,这个设置本身也是代码的一部分,修改它并保存后,需要一次手动重启才能生效。通常只在调试特定问题时才需要关闭自动重载,日常开发建议保持开启。
3. 串口控制台:你的调试与交互窗口
如果说CIRCUITPY驱动器是代码的“入口”,那么串口控制台(Serial Console)就是程序的“输出窗口”和“调试终端”。它是连接你的电脑和微控制器内部运行状态的桥梁,对于排查问题、输出信息、甚至交互式编程至关重要。
3.1 串口控制台的作用与连接
在桌面编程中,我们使用print(“Hello”)在屏幕上输出信息。在CircuitPython中,print函数的内容会被发送到串口,我们需要一个终端程序来接收和显示这些信息。这就是串口控制台。
连接串口控制台通常有三种方式:
- 使用Mu编辑器(推荐给初学者):Mu是一款专为教育设计的Python编辑器,内置了CircuitPython模式和串口控制台。安装Mu后,插入开发板,打开Mu并选择“CircuitPython”模式,点击顶部的“串行”按钮,底部就会弹出控制台窗口。Mu会自动检测并连接你的板子,是最省心的选择。
- 使用其他编辑器+独立终端程序:如果你使用VS Code、Thonny或其他编辑器,你需要一个独立的终端程序。
- Windows:常用
PuTTY或Tera Term。你需要知道板子的COM端口号(在设备管理器中查看)。 - macOS/Linux:系统自带终端就好用。使用
ls /dev/tty.*或ls /dev/cu.*(macOS)以及ls /dev/ttyACM*(Linux)查找设备,然后用screen命令连接,例如:screen /dev/ttyACM0 115200。
- Windows:常用
- 使用Web串口终端(Chrome/Edge):一些在线工具或本地Web服务(如
https://adafruit.github.io/Adafruit_WebSerial_ESTool/)可以利用浏览器的Web Serial API连接设备,无需安装任何软件。
连接参数:CircuitPython串口通常使用115200波特率、8位数据位、无奇偶校验、1位停止位(8N1),无需流控制。
3.2 使用Print语句进行调试
让我们修改之前的闪烁代码,加入print语句来观察程序运行。
import board import digitalio import time led = digitalio.DigitalInOut(board.LED) led.direction = digitalio.Direction.OUTPUT counter = 0 # 新增一个计数器 while True: led.value = True print(“LED ON, Counter:“, counter) # 打印状态和计数器 time.sleep(0.5) led.value = False print(“LED OFF, Counter:“, counter) time.sleep(0.5) counter += 1 # 每次循环计数器加1保存代码后,打开串口控制台,你会看到源源不断的输出:
LED ON, Counter: 0 LED OFF, Counter: 0 LED ON, Counter: 1 LED OFF, Counter: 1 ...这就是“打印调试法”(Print Debugging),是最简单直接的调试手段。你可以通过打印变量值、函数执行到哪一步等信息,快速定位程序逻辑问题。
3.3 解读错误信息(Traceback)
程序出错时,串口控制台是你最好的朋友。我们故意制造一个错误来看看。将上面代码中的led.value = True改为led.value = Tru(去掉e),然后保存。
led.value = Tru # 这里拼写错误保存后,LED会停止闪烁,板子上的状态灯可能改变颜色(例如变成黄色),表示代码出错。此时查看串口控制台,你会看到类似这样的信息:
Traceback (most recent call last): File “code.py“, line 10, in <module> NameError: name ‘Tru‘ is not defined这个“回溯”(Traceback)信息非常宝贵:
Traceback (most recent call last):告诉你接下来是错误堆栈。File “code.py“, line 10, in <module>明确指出错误发生在code.py文件的第10行,在主模块中。NameError: name ‘Tru‘ is not defined这是具体的错误类型和描述:名称错误,Tru这个变量没有被定义。
根据这些信息,你就能快速定位到第10行,发现True拼写错误。修复后保存,程序恢复正常。养成一出错就第一时间查看串口控制台的习惯,能节省大量盲目排查的时间。
注意事项:跨平台文件保存问题有时在Windows上,你可能会遇到保存
code.py后板子没有反应,控制台也没有输出新错误的情况。这可能是由于文件系统同步或编辑器锁定问题。一个经过验证的解决方法是:在code.py文件的最顶端(在所有import之前)添加以下两行代码:import storage storage.remount(“/“, readonly=False, disable_concurrent_write_protection=True)这段代码会以禁用并发写保护的方式重新挂载文件系统,可以解决大多数Windows下的保存问题。请注意,这并非官方推荐的首选方案,仅在遇到保存问题时尝试。更通用的建议是使用Mu编辑器,或确保你的编辑器(如VS Code)在保存后完全释放了文件句柄。
4. 深入REPL:交互式编程与探索
REPL(Read-Eval-Print Loop)是CircuitPython的交互式编程环境。如果说串口控制台是“听”程序说话,那么REPL就是让你和程序“对话”。你可以在这里逐行执行代码、查询模块、测试函数,而无需修改和保存code.py文件。
4.1 进入与退出REPL
要进入REPL,你需要先连接到串口控制台(用Mu或终端程序)。连接成功后:
- 按下
Ctrl+C。这会中断当前正在运行的code.py程序。 - 如果程序被中断,你会看到提示
Press any key to enter the REPL. Use CTRL-D to reload.,此时按键盘任意键。 - 你会看到
>>>提示符,这就表示你已经进入了REPL环境。
进入REPL后,你可能会先看到一些板子信息,例如:
Adafruit CircuitPython 8.2.10 on 2024-06-17; Adafruit Feather RP2040 with rp2040 >>>要退出REPL并重新运行code.py,只需在REPL中输入Ctrl+D。这会软复位板子,重新执行主程序。
4.2 使用REPL进行探索与测试
在>>>提示符后,你可以直接输入Python代码并立即看到结果。
- 获取帮助:输入
help(),然后回车,会显示基础帮助信息,其中最重要的一条是To list built-in modules type help(“modules”)。 - 查看内置模块:输入
help(“modules”),会列出当前固件版本中所有内置的模块,比如board,time,digitalio,analogio,pwmio等。这是了解板子能力的好方法。 - 探索
board模块:board模块包含了板子上所有可用的引脚定义。>>> import board >>> dir(board) # 列出board模块的所有属性 [‘A0‘, ‘A1‘, ‘A2‘, ‘A3‘, ‘D4‘, ‘D5‘, ‘LED‘, ‘SCL‘, ‘SDA‘, ‘TX‘, ‘RX‘, ...] >>> board.LED # 查看LED引脚对应的具体对象 <board.Pin object at 2000a000> - 执行单行代码:你可以直接测试代码片段。
>>> import time >>> start = time.monotonic() >>> time.monotonic() - start # 计算从start到现在过了多少秒 5.123456 >>> print(“Hello from REPL!“) Hello from REPL! - 测试硬件:你甚至可以不写完整程序,直接操作硬件。
(注意:在REPL中输入多行代码如>>> import board, digitalio, time >>> led = digitalio.DigitalInOut(board.LED) >>> led.direction = digitalio.Direction.OUTPUT >>> led.value = True # LED亮 >>> led.value = False # LED灭 >>> for i in range(5): ... led.value = not led.value # 状态翻转 ... time.sleep(0.2) ... >>>for循环时,次级行以...开头,需要手动缩进。输入空行结束多行输入并执行。)
重要警告:REPL的临时性在REPL中输入的所有代码都是临时的!一旦你按下
Ctrl+D复位或断开连接,这些代码就会消失。REPL是一个强大的测试和调试工具,但永远不要把它当作编写最终程序的地方。任何你想保留的代码,都必须写在code.py或其他文件里。
5. 库管理:扩展CircuitPython的能力
CircuitPython的内置模块提供了访问硬件的基础能力,但它的强大之处在于其丰富的库生态系统。库(Libraries)是别人写好的、实现特定功能的代码包,比如驱动某个型号的传感器、显示屏,或者实现网络协议。学会管理库,是构建复杂项目的关键。
5.1 库的存放位置与类型
CircuitPython的库文件存放在CIRCUITPY驱动器下的lib文件夹内。如果你的板子第一次使用,可能没有这个文件夹,手动创建一个即可。
库文件主要有两种格式:
.mpy文件:这是经过编译压缩的库文件,体积小,加载快,是分发时的首选格式。你从官方Bundle下载的通常就是这种。.py文件:这是纯Python源代码文件。有时你可能需要查看或修改库的源码,或者某个库暂时没有.mpy版本,就会使用.py文件。一个库可能由单个.mpy/.py文件构成,也可能是一个包含多个文件的文件夹。
库分为两大类:
- 内置库:随着CircuitPython固件一起烧录到芯片中的库,如
board,time,digitalio等。它们不需要放在lib文件夹里。 - 外置库:需要手动下载并放入
lib文件夹的库。这包括Adafruit官方维护的库和社区贡献的库。
5.2 如何获取所需的库:项目包与库包
方法一:使用项目包(Project Bundle - 最推荐给初学者)
在Adafruit的教程网站(Learn Adafruit)上,几乎每个项目教程的完整代码部分,都会有一个“Download Project Bundle”按钮。点击它会下载一个zip文件,这个文件里包含了运行这个项目所需的一切:code.py、lib文件夹(内含所有依赖库)、图片、字体等资源文件。
操作步骤:
- 找到教程中的“Download Project Bundle”按钮并下载ZIP。
- 解压ZIP文件。
- 打开解压后的文件夹,根据你的CircuitPython版本(如
7.x)找到对应的目录。 - 将该目录下的所有内容(特别是
code.py和lib文件夹)复制到你的CIRCUITPY驱动器根目录。 - 如果提示覆盖,选择“是”。
警告:覆盖风险项目包会覆盖你
CIRCUITPY盘上现有的所有文件!在复制之前,请务必备份你已有的code.py和其他重要文件。
方法二:手动下载并安装库包(Library Bundle)
当你需要为现有项目添加新功能,或者项目教程没有提供Bundle时,就需要手动管理库。你需要去下载对应你CircuitPython版本的“库包”。
- 确定你的CircuitPython版本:查看
CIRCUITPY盘根目录下的boot_out.txt文件,第一行就是版本信息,或者进入REPL时第一行也会显示。 - 下载对应的库包:
- Adafruit官方库包:包含Adafruit维护的所有传感器、显示驱动等库。访问
circuitpython.org/libraries,下载与你的主版本号匹配的adafruit-circuitpython-bundle-py-版本-mpy-日期.zip(例如adafruit-circuitpython-bundle-py-8.x-mpy-20240617.zip)。选择-mpy版本。 - 社区库包:包含社区成员贡献的库。在同一页面,下载
circuitpython-community-library-bundle-py-版本-日期.zip。
- Adafruit官方库包:包含Adafruit维护的所有传感器、显示驱动等库。访问
- 解压并查找库:解压下载的zip文件,库文件都在解压出的
lib文件夹内。 - 复制所需库:打开你的
CIRCUITPY盘下的lib文件夹,从解压的lib文件夹中找到你需要的库文件或文件夹,复制进去。
5.3 如何确定需要哪些库:解读Import语句
当你拿到一段示例代码,如何知道需要安装哪些库呢?答案就在import语句里。
假设你看到这样一段代码开头:
import time import board import neopixel import adafruit_lis3dh import usb_hid from adafruit_hid.consumer_control import ConsumerControl from adafruit_hid.consumer_control_code import ConsumerControlCode分析步骤:
区分内置模块与外置库:首先,运行
help(“modules”)(在REPL中)可以列出所有内置模块。对比列表:time,board,usb_hid通常在列表里 →它们是内置的,无需安装。neopixel,adafruit_lis3dh不在列表里 →它们是外置库,需要安装。
处理
from ... import ...语句:这种语句中,from后面的部分指明了库的位置。from adafruit_hid.consumer_control import ...这里的库名是adafruit_hid。你需要去Bundle的lib文件夹里找到adafruit_hid这个文件夹,并将整个文件夹复制到你的lib下。
查找并复制:
- 在Adafruit库包的
lib文件夹中,找到neopixel.mpy文件,复制到CIRCUITPY/lib。 - 找到
adafruit_lis3dh.mpy文件,复制到CIRCUITPY/lib。 - 找到
adafruit_hid文件夹,整个文件夹复制到CIRCUITPY/lib。
- 在Adafruit库包的
一个关键技巧:依赖关系有时库A会依赖库B。如果你只安装了库A,运行代码时可能会在串口控制台看到ImportError: no module named ‘some_dependency‘。这时,你需要根据错误信息,再去Bundle里找到some_dependency库并安装。这就是为什么对于复杂项目,直接使用“项目包”往往更省心。
5.4 解决“ImportError”实战
让我们模拟一个常见的错误。假设你的lib文件夹是空的,然后你运行了下面这个需要simpleio库的代码:
import board import time import simpleio # 这个库我们没有安装 led = simpleio.DigitalOut(board.LED) while True: led.value = True time.sleep(0.5) led.value = False time.sleep(0.5)保存后,LED不会闪烁。打开串口控制台,你会看到类似这样的错误:
Traceback (most recent call last): File “code.py“, line 3, in <module> ImportError: no module named ‘simpleio‘解决流程:
- 阅读错误:明确告诉你缺少
simpleio模块。 - 查找库:打开你下载的对应版本的Adafruit库Bundle,在
lib文件夹中搜索simpleio。 - 安装库:找到
simpleio.mpy文件,将其复制到CIRCUITPY/lib文件夹。 - 自动重载:因为
code.py保存后出错导致程序停止,复制库文件这个操作本身不会重启程序。你需要按一下板子上的复位按钮,或者**在串口控制台(如果还连着)按Ctrl+D**来软复位。复位后,程序会重新运行,此时应该就能正常闪烁了。
这个过程就是解决库依赖问题的标准流程:看错误 -> 找库 -> 放库 -> 重启。
6. 高级技巧与最佳实践
掌握了基础工作流后,一些技巧能让你的开发过程更加顺畅。
6.1 组织大型项目:多文件与模块化
当项目代码超过几百行后,全部堆在code.py里会难以维护。CircuitPython支持模块化。你可以在CIRCUITPY盘上创建其他.py文件,然后在code.py中导入它们。
例如,创建一个sensor_reader.py文件:
# sensor_reader.py import time import analogio import board def read_temperature(pin): # 模拟读取温度传感器的值(此处为示例) sensor = analogio.AnalogIn(pin) raw_value = sensor.value # 假设的转换公式 temperature = (raw_value / 65535) * 100 return temperature然后在code.py中导入并使用:
# code.py import board import time import sensor_reader # 导入我们自定义的模块 while True: temp = sensor_reader.read_temperature(board.A0) print(“Temperature:“, temp) time.sleep(1)注意:自定义模块需要和code.py放在同一级目录,或者放在Python的搜索路径中。对于简单项目,放在CIRCUITPY根目录即可。
6.2 资源管理与性能考量
尽管CircuitPython易用,但微控制器的资源(内存、存储)有限。
- 内存:避免创建过大的列表、字符串或频繁进行内存分配。使用
gc.mem_free()(需要import gc)可以查看剩余内存,辅助调试内存问题。 - 存储空间:
CIRCUITPY驱动器的剩余空间是有限的。定期清理不需要的.py文件、旧的库或测试文件。.mpy库比.py库更省空间。 - 执行效率:对于实时性要求高的任务(如精确控制PWM频率、读取高速传感器),纯Python的解释执行可能不够快。此时可以考虑:
- 使用内置的硬件模块(如
pwmio,countio),它们由底层C代码驱动,效率高。 - 将最关键的循环部分用
viper或native装饰器优化(高级用法)。 - 如果性能是首要考虑,可能需要评估是否换用Arduino(C++)或MicroPython(部分场景性能更好)等方案。
- 使用内置的硬件模块(如
6.3 版本兼容性:固件与库的匹配
这是一个极易踩坑的地方:CircuitPython的库与固件主版本号必须匹配。
- CircuitPython 8.x 的库不能用在 7.x 的固件上,反之亦然。
- 每次升级CircuitPython固件后,通常需要重新下载并安装对应版本的库包。
检查与更新流程:
- 查看当前版本:
boot_out.txt文件或REPL启动信息。 - 访问
circuitpython.org/downloads,为你的开发板下载最新固件(.uf2文件)。 - 进入板子的BOOTLOADER模式(通常双击复位键),将下载的
.uf2文件拖入出现的RPI-RP2或BOOT驱动器,完成固件升级。 - 访问
circuitpython.org/libraries,下载与新固件主版本号相同的库包(如固件是8.2.10,就下载8.x的库包)。 - 用新库包里的库,替换
CIRCUITPY/lib下的旧库。
遵循“固件与库版本匹配”原则,可以避免绝大多数莫名其妙的ImportError或运行时错误。
从在code.py里修改一个数字改变LED闪烁频率,到通过串口控制台洞察程序内部状态,再到在REPL中交互式探索硬件,最后通过管理库来构建功能丰富的项目——这就是CircuitPython为开发者铺就的一条从入门到精通的平滑路径。它用Python的优雅语法掩盖了底层硬件的复杂性,却又通过串口和文件系统保留了足够的透明度和控制力。这种设计哲学使得无论是教育领域的初学者,还是需要快速验证想法的专业工程师,都能从中获得极高的开发效率。真正的熟练始于动手,现在就去拿一块支持CircuitPython的板子,从让一颗LED听你指挥开始,一步步构建属于你的智能设备吧。