1. CircuitPython与MicroPython的核心模块差异
第一次接触CircuitPython的开发者,往往会惊讶于它与MicroPython在模块设计上的巨大差异。虽然两者都源自Python的嵌入式实现,但在实际使用中你会发现,从MicroPython迁移项目到CircuitPython时,很多基础模块的调用方式完全不同。
最明显的区别就是machine模块的消失。在MicroPython中,machine模块是硬件操作的核心,包含了GPIO控制、定时器、PWM等基础功能。而CircuitPython将其拆分为多个专用模块:
- microcontroller:替代machine的基础引脚控制
- digitalio:专门处理数字输入输出
- analogio:模拟信号处理
- busio:总线协议(I2C/SPI/UART)
这种模块化设计带来了更清晰的代码结构,但也意味着直接移植的代码几乎无法运行。比如在MicroPython中点亮LED的代码:
from machine import Pin led = Pin(2, Pin.OUT) led.value(1)在CircuitPython中需要改写为:
import digitalio import microcontroller led = digitalio.DigitalInOut(microcontroller.pin.GPIO2) led.direction = digitalio.Direction.OUTPUT led.value = True2. 硬件接口的兼容层实践
2.1 GPIO映射方案
由于引脚定义方式完全不同,我们需要建立一个映射层。以下是我在实际项目中使用的引脚映射方案:
import sys CIRCUITPYTHON = (sys.implementation.name == 'circuitpython') if CIRCUITPYTHON: from microcontroller import pin as CPin # 建立数字引脚映射表 pin_mapper = {} for name in dir(CPin): if name.startswith('GPIO'): num = int(name[4:]) pin_mapper[num] = getattr(CPin, name) else: from machine import Pin使用时通过pin_mapper[引脚编号]获取对应引脚对象。这种方法既保持了MicroPython的数字引脚习惯,又兼容了CircuitPython的命名体系。
2.2 总线协议适配
SPI和I2C的差异尤为明显。MicroPython使用统一的machine模块:
from machine import SPI spi = SPI(1, baudrate=5000000, polarity=0, phase=0)而CircuitPython使用busio模块:
import busio from microcontroller import pin spi = busio.SPI(pin.GPIO10, pin.GPIO11, pin.GPIO12)建议封装一个兼容层:
def create_spi(instance=1, baudrate=1000000, polarity=0, phase=0): if CIRCUITPYTHON: if instance == 1: return busio.SPI(pin.GPIO10, pin.GPIO11, pin.GPIO12) else: raise ValueError('Only SPI1 available') else: return SPI(instance, baudrate=baudrate, polarity=polarity, phase=phase)3. 常用功能模块的兼容实现
3.1 时间模块处理
时间模块的差异经常被忽视。MicroPython的utime与CircuitPython的time模块在以下方面不同:
- 时间戳精度(毫秒vs秒)
- 本地时间结构(8元素vs9元素元组)
- sleep函数的参数单位
这是我使用的兼容方案:
if CIRCUITPYTHON: from time import * def sleep_ms(ms): sleep(ms/1000) def ticks_ms(): return int(time()*1000) else: from utime import * ticks_ms = ticks_ms3.2 按键检测实现
按键检测是嵌入式开发的常见需求,两种环境的实现方式截然不同:
MicroPython版本依赖中断:
from machine import Pin btn = Pin(2, Pin.IN, Pin.PULL_UP) btn.irq(handler=lambda p:print("Pressed"), trigger=Pin.IRQ_FALLING)CircuitPython使用事件队列:
import keypad keys = keypad.Keys((microcontroller.pin.GPIO2,), value_when_pressed=False) while True: event = keys.events.get() if event and event.pressed: print("Pressed")兼容方案可以抽象为:
class Button: def __init__(self, pin_num): if CIRCUITPYTHON: self._key = keypad.Keys((pin_mapper[pin_num],), value_when_pressed=False) else: self._btn = Pin(pin_num, Pin.IN, Pin.PULL_UP) self._btn.irq(handler=self._callback, trigger=Pin.IRQ_FALLING) def _callback(self, pin): self._last_press = ticks_ms() def is_pressed(self): if CIRCUITPYTHON: event = self._key.events.get() return event is not None return ticks_ms() - self._last_press < 1004. 高级功能兼容方案
4.1 文件系统操作
CircuitPython默认以只读模式挂载文件系统,这点与MicroPython不同。需要在代码中显式启用写权限:
if CIRCUITPYTHON: import storage storage.remount("/", readonly=False)4.2 网络功能差异
WiFi模块的实现也有显著区别。MicroPython使用network模块:
import network wlan = network.WLAN(network.STA_IF) wlan.connect('SSID','PASSWORD')CircuitPython使用wifi模块:
import wifi wifi.radio.connect('SSID','PASSWORD')建议的兼容层实现:
def wifi_connect(ssid, password): if CIRCUITPYTHON: wifi.radio.connect(ssid, password) return wifi.radio.ipv4_address else: wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(ssid, password) return wlan.ifconfig()[0]4.3 图形显示处理
图形显示方面,framebuf模块的差异尤为明显。CircuitPython的framebufferio是MicroPython framebuf的子集,缺少blit等关键功能。解决方案是使用兼容版本:
try: from framebufferio import FrameBuffer except ImportError: from framebuf import FrameBuffer对于需要高性能图形处理的场景,建议直接使用CircuitPython的displayio模块,它虽然学习曲线较陡,但提供了更现代的图形架构。