从源码编译CircuitPython固件驱动Seeeduino XIAO RP2040扩展板
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的预编译固件,但掌握从源码编译的能力,意味着你不仅能应对任何冷门板卡,还能在需要时定制功能,比如启用某些默认关闭的模块,这才是从“使用者”迈向“开发者”的关键一步。
更多推荐

所有评论(0)