1. 项目概述与核心问题
最近在折腾Seeeduino XIAO RP2040这块板子,想用CircuitPython来驱动它的扩展板做点小项目。结果一上手就踩了个不大不小的坑:官方Wiki上说可以直接用树莓派Pico的CircuitPython固件,但实际用起来,特别是接上扩展板用I2C驱动OLED屏的时候,直接报错提示SDA和SCL线上缺少上拉电阻。我试了从4.7k到47k的各种电阻,统统没用。这感觉就像拿到一把号称“万能”的钥匙,结果连自家门都打不开。
后来在Seeed的论坛里泡了半天,才发现问题根源。有用户提到,这块板子的I2C引脚映射和Pico并不完全一样,而且最关键的是,CircuitPython官方其实已经为XIAO RP2040做好了专属的“端口”(Port),只是不知道为什么没有在下载页面正式发布。这意味着,要想让所有功能,尤其是扩展板上的I2C、RTC、SD卡槽都正常工作,最稳妥的办法就是自己动手,从源码编译一份针对这块板子的CircuitPython固件。这听起来有点硬核,但实际操作下来,只要环境搭对,也就是几条命令的事。编译好的固件刷进去之后,整个世界都清净了,I2C通信立马恢复正常。接下来,我就把从固件编译到驱动OLED、读取传感器、控制伺服电机这一整套流程,结合我踩过的坑和总结的技巧,详细拆解一遍。
2. 开发环境搭建与固件编译
自己编译固件听起来是高级玩家的操作,但其实CircuitPython团队已经把流程做得相当友好了。核心就是准备好编译环境,然后针对你的特定板子型号进行编译。对于XIAO RP2040,我们需要编译的是raspberrypi这个端口下的seeeduino_xiao_rp2040板型。
2.1 编译环境准备
首先需要一个Linux环境。如果你用Windows,最省事的方法是使用WSL2(Windows Subsystem for Linux),我用的就是Ubuntu 22.04 LTS。在Linux终端里,依次执行以下命令来安装必要的依赖工具。这些工具包括编译器、Python3以及一些库文件,是编译工作的基础。
sudo apt-get update sudo apt-get install -y git build-essential libreadline-dev libffi-dev git pkg-config cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib接下来,我们需要获取CircuitPython的源代码。使用git命令克隆仓库,并进入其目录。这里要注意,仓库比较大,包含所有支持的板子代码,下载需要一点时间。
git clone https://github.com/adafruit/circuitpython.git cd circuitpython然后,我们需要获取子模块。CircuitPython依赖一些外部库(如mpy-cross交叉编译器),这些以子模块形式管理。这一步至关重要,缺少子模块编译一定会失败。
git submodule sync git submodule update --init --recursive注意:网络环境可能会影响子模块的拉取速度,如果遇到某个子模块下载失败,可以多试几次,或者手动检查
git submodule status看看是哪个模块出了问题。
2.2 执行编译
环境准备好后,编译本身反而很简单。关键是要进入正确的“端口”目录。对于基于RP2040芯片的板子(包括XIAO RP2040),我们都使用ports/raspberrypi这个端口。
cd ports/raspberrypi接下来就是最重要的编译命令。make BOARD=后面跟的必须是官方支持的板型名称。对于Seeeduino XIAO RP2040,这个名称就是seeeduino_xiao_rp2040。务必确认拼写正确。
make BOARD=seeeduino_xiao_rp2040 -j$(nproc)命令末尾的-j$(nproc)表示使用你电脑上所有的CPU核心进行并行编译,可以显著加快速度。第一次编译会花费较长时间(可能10-30分钟,取决于电脑性能),因为它需要编译工具链和所有依赖库。编译成功后,你会在当前目录下看到build-seeeduino_xiao_rp2040文件夹。
编译生成的固件文件是firmware.uf2,路径为:ports/raspberrypi/build-seeeduino_xiao_rp2040/firmware.uf2。这个.uf2文件就是我们要刷入开发板的固件。
2.3 固件烧录与验证
XIAO RP2040的烧录方式非常简便,采用了UF2 Bootloader。首先,用USB-C数据线将板子连接到电脑。然后,找到板子上的“BOOT”按钮和“RST”按钮(它们通常在一起)。先按住“BOOT”按钮不放,再短按一下“RST”按钮,随后松开“BOOT”按钮。此时,电脑上应该会弹出一个名为RPI-RP2的可移动磁盘。
将刚才编译好的firmware.uf2文件直接拖拽或复制到这个RPI-RP2磁盘里。复制完成后,磁盘会自动弹出,板子会自动重启。几秒钟后,电脑上会出现一个新的磁盘,名字是CIRCUITPY。这就代表刷机成功,CircuitPython已经运行在板子上了。
实操心得:烧录失败怎么办?如果操作后没有出现
CIRCUITPY磁盘,可以尝试以下步骤:1. 换一条质量好的USB数据线,劣质线可能导致供电或数据传输不稳定。2. 确保按按钮的顺序正确(先按住BOOT,再点按RST)。3. 在Windows设备管理器中检查是否有未知设备,可能需要安装RP2040的驱动(通常Windows 10/11会自动识别)。最根本的解决办法是,如果RPI-RP2磁盘能出现,但刷入固件后没反应,可以再次进入Bootloader模式,从CircuitPython官网下载一个官方发布的、其他RP2040板子的.uf2文件(比如Pico的)刷进去试试,如果能成功,再刷回自己编译的固件,这能帮你判断是硬件问题还是固件问题。
3. 核心外设驱动与代码解析
固件搞定后,就可以愉快地编程了。CircuitPython的魅力在于,你可以像在电脑上写Python一样操作硬件。下面我们针对XIAO扩展板上的几个核心外设,逐一拆解驱动方法。
3.1 I2C总线与OLED显示驱动
之前遇到的I2C问题,在刷入专用固件后迎刃而解。这是因为专用固件正确配置了XIAO RP2040的I2C引脚映射。在CircuitPython中,I2C总线通过board.SCL和board.SDA对象访问,它们已经指向了板上正确的物理引脚(对于XIAO,通常是GPIO5和GPIO4)。
驱动一个128x64的OLED屏幕(SSD1306芯片)是经典项目。首先,你需要将必要的库文件复制到CIRCUITPY磁盘的lib文件夹中。从CircuitPython官网的库包(Bundle)里,找到并复制以下库:adafruit_displayio_ssd1306.mpy、adafruit_bus_device文件夹。如果要在屏幕上显示文字,可能还需要adafruit_bitmap_font和adafruit_display_text。
下面的代码展示了如何初始化和在OLED上显示一个带边框的文本界面。displayio是CircuitPython的显示抽象层,管理显示内容和分组。
import board import busio import displayio import terminalio import adafruit_displayio_ssd1306 from adafruit_display_text import label # 释放可能被占用的显示资源 displayio.release_displays() # 初始化I2C总线 i2c = busio.I2C(scl=board.SCL, sda=board.SDA) # 创建I2C显示总线对象,指定设备地址(SSD1306通常是0x3C或0x3D) display_bus = displayio.I2CDisplay(i2c, device_address=0x3C) # 创建显示对象,传入总线、宽度和高度 display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64) # 创建一个显示组(Group),可以把它想象成一个图层容器 splash = displayio.Group() display.show(splash) # 将这个组设置为当前显示内容 # 1. 创建一个全屏的白色背景矩形 color_bitmap = displayio.Bitmap(128, 64, 1) # 1表示1位色深(单色) color_palette = displayio.Palette(1) color_palette[0] = 0xFFFFFF # 调色板索引0设置为白色 bg_sprite = displayio.TileGrid(color_bitmap, pixel_shader=color_palette, x=0, y=0) splash.append(bg_sprite) # 2. 创建一个稍小的黑色内矩形,形成边框效果 inner_bitmap = displayio.Bitmap(118, 54, 1) inner_palette = displayio.Palette(1) inner_palette[0] = 0x000000 # 黑色 inner_sprite = displayio.TileGrid(inner_bitmap, pixel_shader=inner_palette, x=5, y=4) splash.append(inner_sprite) # 3. 创建并添加文本标签 text_line1 = label.Label(terminalio.FONT, text="Hello", color=0xFFFF00, x=40, y=20) text_line2 = label.Label(terminalio.FONT, text="XIAO!", color=0xFFFF00, x=40, y=35) splash.append(text_line1) splash.append(text_line2) while True: # 主循环,保持程序运行。实际应用中这里可以更新显示内容。 pass注意事项:
displayio.release_displays()这行代码很重要。如果你的程序之前运行过其他显示代码,或者你反复修改代码并软重启(Ctrl+D),显示资源可能没有被正确释放,导致新的显示初始化失败。加上这行可以确保每次都是从干净的状态开始。另外,OLED的I2C地址需要确认,大部分是0x3C,但也有部分是0x3D,如果屏幕不亮,可以尝试修改这个地址。
3.2 板载RGB LED与用户按键控制
XIAO RP2040板载了三颗彩色LED(红、绿、蓝)和一个可编程的NeoPixel LED。RGB LED是普通的GPIO驱动型,而NeoPixel是WS2812智能LED。它们的控制方式不同。
控制RGB LED:
import time import board from digitalio import DigitalInOut, Direction # 初始化三个LED引脚 led_red = DigitalInOut(board.LED_RED) led_green = DigitalInOut(board.LED_GREEN) led_blue = DigitalInOut(board.LED_BLUE) # 设置为输出模式 led_red.direction = Direction.OUTPUT led_green.direction = Direction.OUTPUT led_blue.direction = Direction.OUTPUT while True: # 流水灯效果 led_red.value = True # 点亮红灯 time.sleep(0.5) led_red.value = False # 熄灭红灯 led_green.value = True time.sleep(0.5) led_green.value = False led_blue.value = True time.sleep(0.5) led_blue.value = False控制NeoPixel LED:板载的NeoPixel在CircuitPython中通过board.NEOPIXEL访问。注意,NeoPixel需要neopixel库,记得复制到lib文件夹。
import time import board import neopixel from rainbowio import colorwheel # 用于生成彩虹色 # 初始化NeoPixel,引脚为board.NEOPIXEL,数量为1,亮度设为0.3(避免太刺眼) pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.3, auto_write=False) # 彩虹循环效果 def rainbow_cycle(wait): for j in range(255): # colorwheel根据0-255的值返回一个颜色元组(R,G,B) pixel[0] = colorwheel(j) pixel.show() # 必须调用show()才能更新LED time.sleep(wait) while True: # 固定颜色显示 pixel.fill((255, 0, 0)) # 红色 pixel.show() time.sleep(1) pixel.fill((0, 255, 0)) # 绿色 pixel.show() time.sleep(1) pixel.fill((0, 0, 255)) # 蓝色 pixel.show() time.sleep(1) # 彩虹效果 rainbow_cycle(0.01)读取用户按键(以扩展板按键为例):扩展板的用户按键连接到了D1引脚。我们将其配置为输入,并启用内部上拉电阻,这样按键未按下时引脚为高电平,按下时变为低电平。
import time import board from digitalio import DigitalInOut, Direction, Pull led = DigitalInOut(board.LED_RED) # 用红灯作为按键指示灯 led.direction = Direction.OUTPUT button = DigitalInOut(board.D1) # 扩展板用户按键 button.direction = Direction.INPUT button.pull = Pull.UP # 启用内部上拉电阻 while True: # 按键按下时,LED亮;松开时,LED灭 led.value = not button.value # button.value为False时表示按下 time.sleep(0.01) # 短暂延时,降低CPU占用实操心得:关于
auto_write=False。在初始化NeoPixel时设置auto_write=False是个好习惯。这意味着当你修改像素颜色时(如pixel[0] = (255,0,0)),LED不会立即改变,必须调用pixel.show()才会统一更新。这样做有两个好处:一是可以避免在设置复杂动画时产生不连贯的闪烁;二是可以一次性设置好所有像素的颜色再统一刷新,效率更高。对于单个LED,影响不大,但养成这个习惯对以后驱动多个LED的灯带很有帮助。
3.3 无源蜂鸣器与PWM控制
扩展板上的蜂鸣器是无源的,这意味着它需要外部输入频率信号才能发声,可以用来播放简单的音符甚至低质量的音频。我们通过PWM(脉冲宽度调制)来产生不同频率的方波驱动它。在CircuitPython中,使用pwmio模块。
播放音阶:
import time import board import pwmio # 初始化PWM输出,引脚为A3,初始占空比为0(静音),初始频率440Hz(A4音) piezo = pwmio.PWMOut(board.A3, duty_cycle=0, frequency=440, variable_frequency=True) # 中音C大调音阶的频率(单位:Hz) notes = [262, 294, 330, 349, 392, 440, 494, 523] while True: for freq in notes: piezo.frequency = freq # 改变频率即改变音高 piezo.duty_cycle = 65535 // 2 # 设置50%的占空比,这是最响的 time.sleep(0.3) # 发声时长 piezo.duty_cycle = 0 # 占空比设为0,停止发声 time.sleep(0.05) # 音符间的短暂间隔 time.sleep(0.5)尝试播放MP3(实验性):CircuitPython支持通过audiomp3解码MP3,但请注意,RP2040芯片没有硬件解码器,全靠软件,CPU占用率会很高,而且音质和流畅度很难保证,仅适合播放非常简短的提示音。
import board import digitalio from audiomp3 import MP3Decoder # 尝试导入AudioOut,它是音频输出的通用接口 try: from audioio import AudioOut except ImportError: # 有些板子可能用PWM做音频输出 try: from audiopwmio import PWMAudioOut as AudioOut except ImportError: pass # 如果都不支持,就跳过 button = digitalio.DigitalInOut(board.D1) button.switch_to_input(pull=digitalio.Pull.UP) mp3files = ["sound1.mp3", "sound2.mp3"] # 将你的MP3文件放在CIRCUITPY根目录 mp3 = open(mp3files[0], "rb") decoder = MP3Decoder(mp3) audio = AudioOut(board.A3) # 音频输出到蜂鸣器引脚 while True: for filename in mp3files: # 重用解码器对象以节省内存 decoder.file = open(filename, "rb") audio.play(decoder) print("Playing:", filename) while audio.playing: # 等待播放完毕 pass print("Press button to play next...") while button.value: # 等待按键按下 pass注意事项:播放MP3对内存要求较高。确保你的MP3文件是单声道、低采样率(如16kHz)、低比特率的,以减小文件体积和解码压力。复杂的音乐很可能导致内存不足(
MemoryError)或播放卡顿。对于大多数应用,用PWM播放简单的蜂鸣声或音调更可靠。
3.4 SPI总线与SD卡读写
扩展板提供了SD卡槽,通过SPI接口连接。这里有一个关键点:根据原理图,XIAO RP2040的默认SPI片选(CS)引脚是D7,但在Seeeduino XIAO扩展板上,SD卡的CS引脚实际连接的是D2。如果你用D7,代码会找不到SD卡。
下面的代码演示了如何挂载SD卡并列出文件。你需要将adafruit_sdcard.mpy库复制到lib文件夹。
import os import sys import adafruit_sdcard import board import busio import digitalio import storage # 关键:片选引脚是D2,不是D7! SD_CS = board.D2 # 初始化SPI总线 spi = busio.SPI(board.SCK, board.MOSI, board.MISO) # 初始化片选引脚 cs = digitalio.DigitalInOut(SD_CS) # 创建SD卡对象并挂载文件系统 sdcard = adafruit_sdcard.SDCard(spi, cs) vfs = storage.VfsFat(sdcard) storage.mount(vfs, "/sd") # 挂载到根目录下的/sd文件夹 print("SD Card mounted successfully!") print("Root directory contents:") print("========================") # 现在可以像操作本地文件一样操作SD卡了 def list_files(path, indent=0): """递归列出目录下所有文件""" for file in os.listdir(path): full_path = path + "/" + file stats = os.stat(full_path) size = stats[6] # 文件大小 is_dir = stats[0] & 0x4000 # 判断是否为目录 # 格式化文件大小 if size < 1024: size_str = f"{size} B" elif size < 1024*1024: size_str = f"{size/1024:.1f} KB" else: size_str = f"{size/(1024*1024):.1f} MB" # 缩进显示 prefix = " " * indent display_name = prefix + file + ("/" if is_dir else "") # 对齐打印 print(f"{display_name:<40} Size: {size_str:>10}") if is_dir: list_files(full_path, indent + 1) # 列出/sd目录下的所有内容 list_files("/sd") # 示例:在SD卡上创建并写入一个文件 try: with open("/sd/test_log.txt", "a") as f: f.write("Hello from CircuitPython!\n") print("\nFile written successfully.") except OSError as e: print(f"Error writing file: {e}")排查技巧:如果代码运行后提示
OSError: [Errno 19] No such device或挂载失败,请按以下步骤检查:1.确认CS引脚:这是最常见的问题,务必检查代码中的SD_CS是否为board.D2。2.检查硬件连接:确保SD卡已正确插入卡槽,并且接触良好。可以换一张小容量的SD卡(如4GB或8GB,FAT32格式)试试。3.检查电源:SD卡读写时功耗较大,确保USB供电充足。4.检查库文件:确认adafruit_sdcard.mpy库已正确放入CIRCUITPY盘的lib文件夹。
3.5 I2C实时时钟(RTC)模块应用
扩展板集成了一个PCF8563实时时钟芯片,通过I2C通信。即使主控断电,只要板上的纽扣电池(CR1220)有电,它就能继续走时,非常适合需要记录时间的项目,比如数据记录仪。
首先,需要将adafruit_pcf8563.mpy和adafruit_register(文件夹)库复制到lib。下面的代码演示了如何设置和读取时间,并将时间显示在OLED上,组合成一个简单的电子钟。
import time import board import busio import displayio import terminalio import adafruit_displayio_ssd1306 from adafruit_pcf8563.pcf8563 import PCF8563 from adafruit_display_text import label # 释放显示资源,初始化I2C displayio.release_displays() i2c = busio.I2C(board.SCL, board.SDA) # 初始化RTC和OLED rtc = PCF8563(i2c) display_bus = displayio.I2CDisplay(i2c, device_address=0x3C) oled = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64) # 设置字体 font = terminalio.FONT # 首次运行时可以设置一次时间,之后注释掉if True块 if False: # 改为True来设置时间,设置一次后改回False # 设置时间:年,月,日,时,分,秒,星期几(0-6, 0是周一) # 注意:PCF8563的年份范围是2000-2099 set_time = time.struct_time((2024, 5, 27, 14, 30, 0, 0, -1, -1)) print("Setting time to:", set_time) rtc.datetime = set_time # 星期名称映射 weekdays = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") while True: # 检查RTC时间是否有效(是否曾被设置过) if rtc.datetime_compromised: print("RTC time is not set or invalid!") # 可以在这里添加初始化时间的逻辑 else: current = rtc.datetime # 格式化时间:12小时制,带AM/PM hour_12 = current.tm_hour % 12 if hour_12 == 0: hour_12 = 12 am_pm = "AM" if current.tm_hour < 12 else "PM" time_str = f"{hour_12}:{current.tm_min:02d}:{current.tm_sec:02d} {am_pm}" date_str = f"{current.tm_year}-{current.tm_mon:02d}-{current.tm_mday:02d}" weekday_str = weekdays[current.tm_wday] # 在OLED上创建文本标签 text_time = label.Label(font, text=time_str, color=0xFFFFFF) text_date = label.Label(font, text=date_str, color=0xFFFFFF) text_weekday = label.Label(font, text=weekday_str, color=0xFFFFFF) # 计算文本宽度并居中显示 for label_obj, y_pos in [(text_time, 10), (text_date, 25), (text_weekday, 40)]: label_obj.x = oled.width // 2 - label_obj.bounding_box[2] // 2 label_obj.y = y_pos # 创建显示组并刷新 group = displayio.Group() group.append(text_time) group.append(text_date) group.append(text_weekday) oled.show(group) # 也在串口打印输出,方便调试 print(f"{date_str} {time_str} {weekday_str}") time.sleep(1) # 每秒更新一次核心原理:
time.struct_time是Python/CircuitPython中表示时间的一个元组结构,包含9个元素:(年, 月, 日, 时, 分, 秒, 星期几, 一年中的第几天, 夏令时)。PCF8563芯片内部有寄存器存储这些值。当我们执行rtc.datetime = set_time时,代码会将这个结构体里的值写入芯片的相应寄存器。芯片依靠外部32.768kHz的晶振和后备电池,持续维护这个时间。读取时,rtc.datetime再从寄存器中把值读回来,转换回struct_time格式。datetime_compromised属性非常有用,如果芯片因首次使用或电池耗尽导致时间丢失,这个属性会返回True,提示你需要重新设置时间。
3.6 综合项目:温湿度气象站
最后,我们把前面学的知识串起来,做一个综合性的小项目:一个带有OLED显示的温湿度气象站,同时显示时间。我们需要用到DHT11温湿度传感器(连接扩展板的D0引脚)、RTC和OLED。
接线与准备:
- 将DHT11传感器连接到扩展板的
D0引脚(Grove接口)。 - 确保以下库已放入
lib文件夹:adafruit_dht.mpy,adafruit_pcf8563.mpy,adafruit_displayio_ssd1306.mpy,adafruit_display_text,adafruit_bus_device。 - DHT11是单总线协议,对时序要求严格,代码中需要异常处理。
完整代码解析:
import time import board import busio import displayio import terminalio import adafruit_displayio_ssd1306 import adafruit_dht from adafruit_pcf8563.pcf8563 import PCF8563 from adafruit_display_text import label # 初始化硬件 displayio.release_displays() i2c = busio.I2C(board.SCL, board.SDA) dht_sensor = adafruit_dht.DHT11(board.D0) # DHT11接在D0引脚 rtc = PCF8563(i2c) # 初始化OLED display_bus = displayio.I2CDisplay(i2c, device_address=0x3C) oled = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64) font = terminalio.FONT # 变量用于存储上一次有效的读数,防止读取失败时显示空白 last_temp = 20.0 last_hum = 50.0 def format_time_12h(tm): """将24小时制时间转换为12小时制并返回字符串""" hour = tm.tm_hour % 12 if hour == 0: hour = 12 am_pm = "AM" if tm.tm_hour < 12 else "PM" return f"{hour}:{tm.tm_min:02d}:{tm.tm_sec:02d} {am_pm}" def update_display(time_str, date_str, temp_str, hum_str): """更新OLED显示内容""" # 清空之前的显示组 new_group = displayio.Group() # 创建四个文本标签 labels = [] texts = [time_str, date_str, temp_str, hum_str] y_positions = [5, 20, 35, 50] for text, y in zip(texts, y_positions): text_label = label.Label(font, text=text, color=0xFFFFFF) # 居中显示 text_label.x = oled.width // 2 - text_label.bounding_box[2] // 2 text_label.y = y labels.append(text_label) new_group.append(text_label) # 显示新组 oled.show(new_group) return new_group # 返回引用,防止被垃圾回收 # 主循环 current_group = None while True: try: # 尝试读取传感器数据 temperature = dht_sensor.temperature humidity = dht_sensor.humidity # 数据有效则更新缓存 if temperature is not None and humidity is not None: last_temp = temperature last_hum = humidity else: # 读取到None,使用上一次的有效值 print("Sensor read returned None, using cached values.") except RuntimeError as error: # DHT传感器非常容易读取失败,特别是连续快速读取时 print(f"DHT read error: {error.args[0]}") # 使用缓存值继续显示,避免屏幕闪烁或空白 temperature, humidity = last_temp, last_hum # 从RTC获取当前时间 current_time = rtc.datetime time_str = format_time_12h(current_time) date_str = f"{current_time.tm_mon}/{current_time.tm_mday}/{current_time.tm_year}" temp_str = f"Temp: {temperature:.1f}C" hum_str = f"Hum: {humidity:.1f}%" # 更新显示 current_group = update_display(time_str, date_str, temp_str, hum_str) # 在串口也打印出来,方便调试和记录 print(f"{date_str} {time_str} | {temp_str} | {hum_str}") # 重要:DHT11两次读取之间需要至少2秒的间隔 time.sleep(3)避坑指南与性能优化:
- DHT11读取异常处理:DHT11传感器通过单总线协议通信,对时序极其敏感,在CircuitPython这类非实时系统中很容易读取失败(抛出
RuntimeError)。代码中使用了try...except捕获异常,并用last_temp和last_hum缓存上一次成功的数据。这样即使某次读取失败,显示屏上也不会出现空白或错误数据,用户体验更稳定。- 显示刷新优化:在
update_display函数中,我们每次创建新的displayio.Group并添加标签,然后用oled.show(new_group)显示。旧的显示组会被自动垃圾回收。这种方式在动态更新内容时很清晰。注意,我们将函数返回的组赋值给current_group,是为了在循环外保持一个引用,防止Python的垃圾回收器过早清理掉正在显示的对象,导致屏幕闪烁或清空。- 读取间隔:DHT11的数据手册要求两次读取之间至少有2秒的间隔。这里设置
time.sleep(3)是保守且可靠的做法。频繁读取不仅会失败,还可能影响I2C总线(OLED和RTC)的稳定性。- 电源稳定性:传感器、OLED、RTC和SD卡都通过扩展板供电。如果同时工作,特别是SD卡进行写入时,电流需求可能较大。如果出现传感器读数不稳定或OLED闪烁,可以尝试给开发板单独供电(如通过外部5V电源),而不是仅靠USB。
3.7 伺服电机(舵机)控制
扩展板有一个专门的舵机接口(连接D6引脚),方便连接标准的三线舵机(信号、电源、地)。舵机的控制原理是通过PWM信号产生一个周期为20ms(50Hz)的脉冲,通过脉冲宽度(高电平持续时间)来控制旋转角度,通常0.5ms对应0度,2.5ms对应180度。
硬件连接注意:舵机接口的VCC通常是5V,可以直接为小型舵机供电。但如果驱动多个或扭矩大的舵机,务必使用外部5V电源单独为舵机供电,避免开发板USB口过载导致复位或损坏。
代码实现:
import time import board from digitalio import DigitalInOut, Direction, Pull import pwmio from adafruit_motor import servo # 初始化板载LED作为状态指示 led = DigitalInOut(board.LED) led.direction = Direction.OUTPUT # 初始化用户按键(用于切换模式) button = DigitalInOut(board.D1) button.direction = Direction.INPUT button.pull = Pull.UP # 初始化舵机PWM输出 # 舵机控制需要50Hz的频率(周期20ms) pwm = pwmio.PWMOut(board.D6, duty_cycle=0, frequency=50) # 创建舵机对象,min_pulse和max_pulse需要根据你的舵机规格微调 my_servo = servo.Servo(pwm, min_pulse=500, max_pulse=2500) current_angle = 90 # 初始角度 mode = 0 # 0: 角度模式,1: 扫描模式 def blink(times): """LED闪烁函数,用于反馈""" for _ in range(times): led.value = False time.sleep(0.1) led.value = True time.sleep(0.1) print("Servo Control Ready. Press button to change mode.") while True: # 检测按键按下(下降沿触发) if not button.value: # 按键被按下 time.sleep(0.02) # 消抖延时 if not button.value: # 再次确认 mode = (mode + 1) % 2 blink(mode + 1) # 模式1闪1下,模式2闪2下 print(f"Switched to Mode: {mode}") while not button.value: # 等待按键释放 time.sleep(0.01) if mode == 0: # 模式0:按键控制角度递增 # 每次循环角度+10度,到180后归零 my_servo.angle = current_angle print(f"Angle set to: {current_angle}") current_angle = (current_angle + 10) % 181 # 0到180循环 time.sleep(1) # 每隔1秒动一次 else: # 模式1:自动来回扫描 print("Auto scanning...") for angle in range(0, 181, 5): # 从0到180度,步进5度 my_servo.angle = angle time.sleep(0.05) for angle in range(180, -1, -5): # 从180到0度 my_servo.angle = angle time.sleep(0.05)参数调校与常见问题:
min_pulse和max_pulse:这是控制舵机精度的关键参数,单位是微秒。理论上标准舵机是500-2500us对应0-180度。但实际舵机有差异,可能达不到理论角度范围。如果你的舵机转动角度不足180度,可以尝试减小min_pulse(如450)或增大max_pulse(如2550)。务必小幅调整,一次调50us,避免脉冲宽度超出舵机机械极限导致堵转损坏。- 舵机抖动或吱吱响:如果舵机在到达指定角度后不停抖动或发出噪音,通常是供电不足导致的。USB口提供的500mA电流可能不足以驱动某些舵机,特别是在有阻力时。务必使用外部5V电源为舵机供电,并将舵机的地线(GND)与开发板的地线连接在一起。
- 控制无反应:首先检查接线是否正确(信号线-黄/白/橙色接
D6,红线接5V,黑/棕色接地)。然后检查代码中PWM引脚是否为board.D6。可以用万用表测量D6引脚在程序运行时是否有电压变化(应在0-3.3V间跳变)。最后,尝试更换一个舵机,排除舵机本身故障。
4. 项目总结与进阶思考
走完这一整套流程,从固件编译的“从无到有”,到逐个驱动外设,最后整合成一个能显示时间、温湿度的小型气象站,你应该对如何在Seeeduino XIAO RP2040上玩转CircuitPython有了比较扎实的体验。这个过程的核心,其实是对嵌入式开发中“软硬件结合”思维的实践。自己编译固件,是为了让软件层(CircuitPython解释器)精确匹配硬件层(XIAO RP2040的特定引脚和功能)。这解决了最根本的兼容性问题。
几个让我印象深刻的点:一是库管理,CircuitPython的“库文件扔进lib文件夹”的方式极其简单,但务必注意库的版本兼容性,最好使用与固件版本匹配的库包。二是资源管理,RP2040虽然有264KB内存,但在同时驱动多个外设、处理显示和传感器数据时,依然要警惕内存不足。像displayio创建大量对象时,注意及时release_displays或复用对象。三是时序与稳定性,DHT11的读取间隔、舵机PWM信号的精度,都要求代码不能“随心所欲”,必须考虑硬件的物理限制。
这个气象站项目可以作为一个起点进行很多扩展。比如,利用SD卡模块,将温湿度数据连同时间戳以CSV格式定期写入文件,做成一个离线数据记录仪。再进一步,可以尝试连接Wi-Fi模块(如ESP-01S,通过串口AT指令控制),将数据上传到云端进行可视化。或者,结合舵机,做一个根据温湿度自动开关小风扇的简易控制器。硬件平台的潜力,正是在这样一个个具体项目的打磨中被挖掘出来的。最后,关于固件,虽然现在官方下载页面可能已经有了XIAO RP2040的预编译固件,但掌握从源码编译的能力,意味着你不仅能应对任何冷门板卡,还能在需要时定制功能,比如启用某些默认关闭的模块,这才是从“使用者”迈向“开发者”的关键一步。