CircuitPython实战:PWM精准控制舵机与可编程LED灯带
1. 项目概述与核心思路如果你玩过Arduino对舵机、RGB灯带这些玩意儿肯定不陌生。但当你从Arduino的C世界切换到CircuitPython时那种“即写即得”的爽快感以及用Python语法轻松操控硬件的便利完全是另一番体验。我最近在几个物联网和智能硬件项目里深度用上了CircuitPython来控制伺服电机和可编程LED灯带NeoPixel和DotStar整个过程踩了不少坑也总结了不少实战经验。简单来说这个项目就是教你如何用CircuitPython这块“瑞士军刀”去精准驱动两种最常见的执行器伺服电机和两种最炫酷的反馈器可编程LED。伺服电机负责“动”无论是让机械臂摆个角度还是让小车轮子转起来LED灯带负责“亮”用流光溢彩的灯光来指示状态、烘托氛围或者纯粹为了好看。而连接它们的核心技术就是PWM脉宽调制。别看PWM听起来高大上其实理解起来很简单它就像用一个快速开关的水龙头通过控制“开”和“关”的时间比例占空比来模拟出不同的“水流大小”平均电压。舵机根据这个“水流”的宽度来理解你要它转到的角度而LED则根据红、绿、蓝三个通道的“水流”比例来混合出千万种颜色。为什么选择CircuitPython而不是MicroPython或其他对我来说最大的吸引力在于其极低的上手门槛和强大的库生态。你不需要复杂的编译环境直接把.py文件拖进开发板就能运行。Adafruit为几乎所有常见的传感器和执行器都提供了维护良好的驱动库比如本文用到的adafruit_motor和neopixel这让代码变得异常简洁。本文将从最基础的PWM和舵机控制讲起逐步深入到NeoPixel和DotStar的复杂动画效果我会把代码里每一行关键配置背后的“为什么”都掰开揉碎并分享那些官方文档里不会写的接线细节、性能调优和避坑指南。无论你是刚接触嵌入式Python的爱好者还是想寻找更高效原型开发工具的工程师相信都能从中找到可以直接“抄作业”的干货。2. 环境准备与硬件选型在开始写代码之前把硬件环境搭建好是成功的一半。这一节我会详细拆解你需要准备哪些东西以及在不同场景下如何做出最合适的选择。2.1 开发板与核心库首先你需要一块支持CircuitPython的开发板。Adafruit的系列产品是首选因为它们对CircuitPython的支持最为完善。常见的选择包括Feather M4 Express / Feather M0 Express我的主力开发板引脚丰富性能足够自带锂电池管理非常适合移动项目。Metro M4 Express / Metro M0 Express类似Arduino Uno的形态接口齐全适合桌面固定项目。Circuit Playground Express (CPX)内置传感器、按钮、麦克风和10个NeoPixel LED开箱即用特别适合教育和快速原型。QT Py小巧精致适合空间受限的项目。Trinket M0 / Gemma M0极致小巧适合可穿戴设备。选定板子后第一件事是去 Adafruit的CircuitPython官网 下载对应板子的最新UF2固件文件。按住板子上的复位键或双击复位按钮将板子进入UF2启动模式这时电脑上会出现一个名为CPLAYBOOT或FEATHERBOOT等的U盘把下载的UF2文件拖进去板子会自动重启并完成固件烧录。重启后电脑上会出现一个名为CIRCUITPY的U盘这就是你的代码存储和运行空间。接下来是库文件。Adafruit提供了完整的 CircuitPython Library Bundle 你需要下载与固件版本匹配的库包。解压后找到lib文件夹根据你的项目需要将以下库文件复制到CIRCUITPY盘符下的lib文件夹中adafruit_motor用于控制伺服电机、步进电机等。neopixel.mpy用于控制NeoPixel系列LED。adafruit_dotstar.mpy用于控制DotStar系列LED。可选rainbowio.mpy提供colorwheel等色彩辅助函数让彩虹动画更易实现。注意.mpy文件是经过预编译的库执行效率比.py文件更高。务必确保你复制的是.mpy格式的文件。如果lib文件夹已存在同名文件直接覆盖即可。2.2 伺服电机选择与电源考量伺服电机主要分两类选错了类型你的项目可能根本无法工作。标准舵机Standard Servo这是我们最常见的那种。它接收PWM信号并将输出轴旋转到0-180度范围内的一个特定角度并保持住。常用于机器人关节、摄像头云台等需要精确定位的场景。连续旋转舵机Continuous Rotation Servo它看起来和标准舵机一样但内部去掉了机械限位。你通过PWM信号控制的是它的转速和方向全速正转、停止、全速反转而不是角度。常用于小车的驱动轮、传送带等需要连续运动的场景。电源是驱动舵机最关键的环节也是新手最容易翻车的地方。绝大多数开发板的3.3V或5V输出引脚其电流输出能力通常只有500mA-1A远远不足以驱动一个更别说多个舵机。舵机在堵转卡住或启动瞬间电流可以轻松达到1A以上。正确的供电方案独立供电为舵机准备一个独立的电源如4节AA电池盒、3.7V锂电池或专用的5V/2A以上电源适配器。将此外部电源的正极V连接到舵机的红线电源线负极GND连接到舵机的黑/棕线地线同时此外部电源的GND必须与开发板的GND相连以确保信号地一致。开发板仅提供信号开发板的PWM输出引脚如A2只连接舵机的信号线通常是白、黄或橙色线。开发板只负责发送“指令”PWM波不负责提供“动力”大电流。使用电容在舵机的电源正负极之间并联一个470μF至1000μF的电解电容可以有效地吸收电机启停和换向时产生的瞬间大电流浪涌防止电源电压被拉低导致开发板复位这是提升系统稳定性的廉价且有效的手段。2.3 NeoPixel与DotStar灯带选型与接线NeoPixel和DotStar都是世界级的可寻址LED但内部原理和接口不同。NeoPixel (WS2812B)单线控制。所有LED只通过一根数据线DIN串联。优点是接线简单成本较低。缺点是刷新率相对较低且因为所有数据必须依次传递当灯珠数量很多时更新整条灯带会有可见的延迟。DotStar (APA102)双线控制。需要数据线DI和时钟线CI。优点是支持硬件SPI刷新率极高可达4MHz可以实现极其流畅的动画和“光绘”效果。缺点是接线多一根成本稍高。接线与供电要点数据流向务必确认你接的是LED灯带的“输入”端DIN/DI, CIN/CI。灯带两端通常会有箭头指示数据流向接反了灯带不会亮。这是一个极其常见且令人沮丧的错误。电源分离和舵机一样永远不要试图用开发板的VCC直接驱动超过几颗LED的灯带。每颗LED在全白最亮时可能消耗60mA电流30颗就是1.8A必须使用外部5V电源并通过较粗的导线直接为灯带供电。共地外部电源的GND、开发板的GND、灯带的GND必须连接在一起。电平匹配大多数开发板GPIO输出是3.3V逻辑电平而NeoPixel/DotStar需要5V逻辑。幸运的是3.3V通常也能被识别为高电平在短距离、灯珠不多的情况下可以直接连接。但如果出现灯珠闪烁、颜色错乱或第一颗灯珠后不亮的情况就需要一个逻辑电平转换器如74AHCT125将3.3V信号提升至5V。电源滤波在灯带电源入口处并联一个1000μF的电解电容可以极大地抑制LED快速开关时对电源的噪声干扰让颜色显示更稳定同时保护你的开发板。3. 核心原理与代码深度解析硬件准备好了我们来深入代码层面。CircuitPython的优雅之处在于它用高级的Python对象封装了底层复杂的硬件操作。3.1 PWM与伺服电机控制详解在CircuitPython中我们使用pwmio库来生成PWM信号用adafruit_motor.servo库来高级地控制舵机。标准舵机控制import time import board import pwmio from adafruit_motor import servo # 1. 创建PWMOut对象 pwm pwmio.PWMOut(board.A2, duty_cycle2**15, frequency50)board.A2指定使用哪个GPIO引脚输出PWM信号。你可以换成任何支持PWM的引脚。duty_cycle2**15这是设置初始占空比。2**15是6553616位分辨率的一半即50%的占空比。对于50Hz的舵机信号这对应着1.5ms的脉冲宽度通常是舵机的中间位置90度。这个初始值很重要可以防止舵机在初始化时突然抖动到一个极端位置。frequency50舵机标准控制频率是50Hz即周期20ms。这是必须遵守的否则舵机无法正确解析信号。# 2. 创建Servo对象 my_servo servo.Servo(pwm)这里将PWM对象传递给Servo类之后我们就可以用角度来控制了。# 3. 控制角度 my_servo.angle 90 # 旋转到90度位置servo库内部帮你完成了从角度到对应脉冲宽度的换算。通常0度对应1ms脉冲180度对应2ms脉冲。但有些舵机范围可能更宽或更窄。脉冲宽度校准如果你的舵机转动范围不是标准的0-180度或者你希望用到它的最大物理行程可以通过min_pulse和max_pulse参数进行微调单位是微秒μs。my_servo servo.Servo(pwm, min_pulse500, max_pulse2500)这意味着500μs0.5ms的脉冲对应0度2500μs2.5ms的脉冲对应180度。通过示波器观察信号或者手动尝试找到舵机开始抖动的极限位置对应的脉冲值可以精确校准。连续旋转舵机控制连续旋转舵机的代码区别仅在于对象类型和控制属性。from adafruit_motor import servo my_continuous_servo servo.ContinuousServo(pwm) while True: my_continuous_servo.throttle 1.0 # 全速正转 time.sleep(2) my_continuous_servo.throttle 0.0 # 停止 time.sleep(2) my_continuous_servo.throttle -0.5 # 半速反转 time.sleep(2)throttle属性范围从-1.0全速反转到1.0全速正转0.0为停止。你可以将其理解为直流电机的调速。中点校准即使是连续旋转舵机throttle0.0也不一定代表完全静止可能会缓慢转动。这时需要用小螺丝刀调节舵机背面电位器进行物理校准或者像标准舵机一样在代码中微调min_pulse和max_pulse直到throttle0.0时电机完全停转。3.2 NeoPixel灯带编程实战控制NeoPixel的核心是neopixel库。它抽象了底层时序让我们可以像操作数组一样操作每一个LED。基础对象创建与配置import board import neopixel pixel_pin board.A1 # 数据引脚 num_pixels 30 # LED数量 pixels neopixel.NeoPixel(pixel_pin, num_pixels, brightness0.3, auto_writeFalse)brightness0.3全局亮度范围0.0-1.0。强烈建议初始化时设置为一个较低的值如0.2-0.3。NeoPixel在全亮度1.0时非常刺眼且电流极大。先调低亮度测试安全后再根据需要调整。auto_writeFalse这是性能关键设置。当设置为False时你对pixels数组的所有颜色修改都不会立即发送到灯带直到你调用pixels.show()。这允许你先准备好所有LED的颜色数据然后一次性高速发送避免了每个LED单独更新导致的动画卡顿和闪烁。在制作复杂动画时务必使用此模式。颜色设置与动画NeoPixel使用RGB元组(R, G, B)设置颜色每个分量取值0-255。# 定义一些常用颜色 RED (255, 0, 0) GREEN (0, 255, 0) BLUE (0, 0, 255) PURPLE (180, 0, 255) # 可以混合 # 设置单个LED pixels[0] RED # 索引从0开始 pixels[1] GREEN # 设置所有LED pixels.fill(BLUE) # 必须调用show()才能更新到硬件当auto_writeFalse时 pixels.show()实现流光溢彩的动画单纯的变色很无聊我们来写几个经典的动画函数。颜色追逐Color Chase像跑马灯一样让一个颜色依次流过每个LED。def color_chase(color, wait): for i in range(num_pixels): pixels[i] color pixels.show() time.sleep(wait)这个函数简单明了但每次只点亮一个LED。你可以修改pixels[i] color为pixels[i] color; pixels[i-1] (0,0,0)来实现“移动的光点”效果。彩虹循环Rainbow Cycle让整个灯带呈现平滑过渡的彩虹色。from rainbowio import colorwheel def rainbow_cycle(wait): for j in range(255): # 循环色轮值 for i in range(num_pixels): # 为每个LED计算一个偏移的色轮索引形成彩虹分布 rc_index (i * 256 // num_pixels) j pixels[i] colorwheel(rc_index 255) # 255确保索引在0-255内 pixels.show() time.sleep(wait)colorwheel函数将一个0-255的整数映射到一个彩虹色。rc_index的计算是关键i * 256 // num_pixels确保所有LED在色轮上均匀分布加上j使得整个分布随时间推移而滚动形成流动的彩虹。RGBW NeoPixel的特殊处理RGBW灯珠多了一个纯白色LED。代码上有两处关键不同创建对象时需指定pixel_orderpixels neopixel.NeoPixel(..., pixel_order(1, 0, 2, 3))。这个元组定义了数据流中四个颜色分量的顺序常见的是GRBW。颜色元组变为四元组(R, G, B, W)白色分量W也取值0-255。例如纯白色是(0, 0, 0, 255)而不是RGB的(255, 255, 255)。使用RGBW灯珠能获得更真实、更明亮的白色。3.3 DotStar灯带编程与性能优势DotStar的使用与NeoPixel类似但库是adafruit_dotstar。它最大的优势在于可启用硬件SPI获得极高的刷新率。对象创建与SPI优化import board import adafruit_dotstar # 方法一使用任意两个GPIO引脚软件SPI速度慢 pixels adafruit_dotstar.DotStar(board.A1, board.A2, 30, brightness0.1, auto_writeFalse) # 方法二推荐使用硬件SPI引脚速度极快 import busio spi busio.SPI(board.SCK, board.MOSI) # 对于大多数板子SCK和MOSI是固定的硬件SPI引脚 pixels adafruit_dotstar.DotStar(spi, 30, brightness0.1, auto_writeFalse)引脚顺序DotStar需要两个引脚数据线DI和时钟线CI。在软件SPI模式下构造函数DotStar(pin_ci, pin_di, ...)的第一个参数是时钟线第二个是数据线接反了灯带不工作。硬件SPI如果你的板子有硬件SPI引脚通常是SCK和MOSI强烈建议使用busio.SPI对象来创建DotStar。刷新率可以从软件SPI的几KHz飙升到4MHz对于长灯带或高速动画如POV“光绘”是质的飞跃。你需要查阅开发板引脚图找到硬件SPI引脚。高级切片赋值技巧DotStar库支持Python的列表切片语法进行批量赋值这让创建图案非常高效。# 点亮所有偶数索引的LED为红色 pixels[::2] [RED] * (num_pixels // 2) pixels.show() # 点亮所有奇数索引的LED为绿色 pixels[1::2] [GREEN] * (num_pixels // 2) pixels.show()[RED] * (num_pixels // 2)创建了一个包含一半数量红色元组的列表。pixels[::2]是切片语法表示“从开始到结束步长为2”即所有偶数索引。这种一次性赋值再show()的方式比用for循环逐个设置要快得多。4. 项目集成与高级应用示例掌握了单个组件的控制后我们可以将它们组合起来实现更复杂、交互性更强的项目。这里我设计一个“智能状态指示器”的示例它用一个舵机指向物理刻度盘同时用一条NeoPixel灯带显示彩虹光谱并通过触摸电容引脚来切换模式。4.1 硬件连接图假设我们使用Feather M4 Express开发板标准舵机信号线 -板子A2 电源和地接外部5V电源。NeoPixel灯带 (30颗)数据线Din -板子A1 电源和地接同一个外部5V电源注意电流需足够30颗全亮约需2A。电容触摸输入用一根导线或铜箔胶带连接到板子A0 另一端悬空作为触摸点。对于M4板需要在A0引脚和GND之间焊接一个1MΩ1兆欧的电阻这是软件电容触摸所必需的。4.2 集成代码实现# SPDX-FileCopyrightText: 2023 Your Name # SPDX-License-Identifier: MIT 智能交互式状态指示器 模式0舵机扫描 灯带彩虹循环 模式1舵机随触摸角度变化 灯带显示颜色光谱 通过触摸A0引脚切换模式。 import time import board import touchio import pwmio import neopixel from adafruit_motor import servo from rainbowio import colorwheel # --- 硬件初始化 --- # 1. 电容触摸输入 (M4板需外接1MΩ电阻到GND) touch_pad board.A0 touch touchio.TouchIn(touch_pad) # 2. 舵机初始化 pwm_servo pwmio.PWMOut(board.A2, frequency50) my_servo servo.Servo(pwm_servo, min_pulse500, max_pulse2500) # 3. NeoPixel初始化 pixel_pin board.A1 num_pixels 30 pixels neopixel.NeoPixel(pixel_pin, num_pixels, brightness0.2, auto_writeFalse) # --- 全局变量与状态 --- current_mode 0 # 0或1 last_touch_state False mode_change_debounce time.monotonic() # --- 功能函数 --- def update_led_spectrum(angle): 根据舵机角度(0-180)更新灯带颜色光谱从红到紫 hue_per_pixel 180 / num_pixels # 每个LED在色轮上占据的度数 for i in range(num_pixels): # 计算该LED的色轮值并加上角度偏移 hue int((i * hue_per_pixel angle) % 180) # 使用180度色轮范围 # 将0-180映射到0-255供colorwheel使用 color_value int((hue / 180) * 255) pixels[i] colorwheel(color_value) pixels.show() def servo_sweep_rainbow(): 模式0舵机往复扫描灯带显示彩虹循环 for angle in range(0, 180, 2): if check_mode_change(): return # 如果模式改变退出当前函数 my_servo.angle angle # 彩虹动画 for j in range(5): # 每角度更新5次彩虹让彩虹动起来 for i in range(num_pixels): rc_index (i * 256 // num_pixels) (angle j*10) % 256 pixels[i] colorwheel(rc_index 255) pixels.show() time.sleep(0.01) time.sleep(0.02) for angle in range(180, 0, -2): if check_mode_change(): return my_servo.angle angle for j in range(5): for i in range(num_pixels): rc_index (i * 256 // num_pixels) (angle j*10) % 256 pixels[i] colorwheel(rc_index 255) pixels.show() time.sleep(0.01) time.sleep(0.02) def touch_controlled_spectrum(): 模式1触摸时舵机角度和灯带光谱随触摸时间线性变化 touch_start_time None while current_mode 1: if check_mode_change(): break if touch.value: if touch_start_time is None: touch_start_time time.monotonic() # 记录开始触摸的时间 touch_duration time.monotonic() - touch_start_time # 将触摸持续时间映射到0-180度最长触摸5秒对应180度 target_angle min(int((touch_duration / 5.0) * 180), 180) # 平滑移动到目标角度 current_angle my_servo.angle or 0 step 1 if target_angle current_angle else -1 for ang in range(int(current_angle), int(target_angle), step): my_servo.angle ang update_led_spectrum(ang) time.sleep(0.01) if not touch.value or check_mode_change(): break else: touch_start_time None # 松开触摸重置计时 # 保持当前角度和光谱 time.sleep(0.05) def check_mode_change(): 检查是否发生触摸以切换模式加入防抖 global current_mode, last_touch_state, mode_change_debounce current_time time.monotonic() current_touch touch.value # 检测上升沿从没摸到摸到且防抖时间已过 if current_touch and not last_touch_state and (current_time - mode_change_debounce) 0.5: current_mode 1 if current_mode 0 else 0 # 切换模式 print(f模式切换到: {current_mode}) mode_change_debounce current_time # 切换时给一个视觉反馈灯带快速闪烁白色两次 for _ in range(2): pixels.fill((255, 255, 255)) pixels.show() time.sleep(0.1) pixels.fill((0, 0, 0)) pixels.show() time.sleep(0.1) return True # 表示模式已改变 last_touch_state current_touch return False # --- 主循环 --- print(智能状态指示器启动) print(触摸A0引脚切换模式。) print(f当前模式: {current_mode} (0:扫描彩虹, 1:触摸控制)) while True: if current_mode 0: servo_sweep_rainbow() else: touch_controlled_spectrum()4.3 代码解析与技巧状态机与模式切换使用current_mode全局变量和check_mode_change()函数构建了一个简单的状态机。这是嵌入式系统中管理复杂行为的常用模式比庞大的if-else语句更清晰。触摸防抖Debouncecheck_mode_change()函数中加入了时间判断(current_time - mode_change_debounce) 0.5。这是为了防止因触摸抖动或意外短暂接触导致的误触发。0.5秒的间隔确保了每次模式切换都是有意为之。非阻塞式动画在servo_sweep_rainbow()函数中舵机移动和彩虹动画是交织在一起的并且在每个小循环中都检查check_mode_change()。这保证了系统能实时响应模式切换命令而不会因为执行一个漫长的for循环而卡死。这是编写交互式系统的关键。视觉反馈在模式切换的瞬间让所有LED快速闪烁两次白色。这提供了即时的、明确的用户反馈让用户知道他的输入已被接受。良好的用户体验在硬件项目中同样重要。平滑运动在touch_controlled_spectrum()中舵机角度是逐步for循环变化的而不是直接my_servo.angle target_angle。这产生了平滑的动画效果观感更佳也对舵机齿轮更友好。资源管理代码中大量使用了time.monotonic()来计时而不是time.sleep()进行长延时。这保证了主循环的响应性。同时在模式函数中一旦检测到模式改变立即return确保了状态切换的干净利落。5. 调试技巧、常见问题与性能优化即使代码逻辑正确在实际硬件调试中也会遇到各种光怪陆离的问题。这一节是我多年调试经验的浓缩能帮你快速定位和解决大部分常见故障。5.1 硬件问题排查清单当你的项目毫无反应或行为异常时请按以下顺序排查电源问题占故障的70%以上症状舵机不动或抽搐LED灯带部分不亮、闪烁或颜色异常开发板自动复位。排查测量电压用万用表测量舵机/LED电源输入端的电压在电机启动或LED全亮时电压是否被拉低到4.5V以下如果是说明电源功率不足。检查接地确保开发板、外部电源、舵机、灯带的所有GND地线都连接在一起。共地不共是导致信号混乱的元凶。添加电容在舵机和灯带的电源正负极之间并联一个大电容470-1000μF注意极性立竿见影地解决电压跌落问题。独立供电务必为电机和灯带使用独立于开发板的电源。开发板的USB口或稳压器无法提供持续的大电流。信号与接线问题症状舵机完全不动只有第一颗NeoPixel亮后面的不亮DotStar完全不亮。排查数据流向再次确认NeoPixel的Din、DotStar的DI/CI是否接在了开发板的输出引脚上并且接在了灯带的输入端。用箭头标记或用胶带区分输入输出端。电平匹配对于长灯带如超过1米或高速信号3.3V可能不够。尝试在数据线上串联一个330-470Ω的电阻靠近开发板输出端这可以改善信号质量。如果问题依旧需要使用逻辑电平转换模块。接触不良杜邦线、面包板接触不良是隐形杀手。用力按紧所有连接或直接焊接。软件与代码问题症状代码上传后板子无反应串口无输出或报ImportError。排查库文件确认lib文件夹内有所需的.mpy库文件且版本与CircuitPython固件匹配。错误的库版本是ImportError的常见原因。文件命名确保主程序文件名为code.py或main.pyCircuitPython会自动运行它。code.py优先级更高。串口监视器使用Mu编辑器、Thonny或screen/putty打开串口监视器波特率通常为115200。查看是否有错误信息输出。print()语句是你的好朋友。硬复位有时板子会进入一个奇怪的状态。尝试按一下板载的复位按钮。5.2 性能优化与高级技巧当项目复杂后性能就变得重要了。优化NeoPixel刷新关键始终设置auto_writeFalse并在完成一帧所有LED的颜色设置后只调用一次pixels.show()。瓶颈分析更新一个NeoPixel的时间大约是30μs。对于30颗LED一帧就是900μs即每秒最多约1100帧。但如果你在循环里逐个设置并show()这个时间会成倍增加并且动画会卡顿。批量操作是王道。使用内存视图MemoryView对于极致的性能可以操作pixels的底层字节数据。但这属于高级技巧除非你驱动数百颗LED并需要极高帧率否则用上面的方法已足够。使用硬件SPI驱动DotStar 这是提升DotStar性能最有效的方法。查找你的开发板引脚图找到标有SCK时钟和MOSI主设备输出的引脚。使用busio.SPI初始化速度会有百倍提升。import busio import adafruit_dotstar spi busio.SPI(board.SCK, board.MOSI) pixels adafruit_dotstar.DotStar(spi, 30, brightness0.1, auto_writeFalse)省电与热管理降低亮度brightness0.2通常已经足够亮且电流只有全亮的20%。及时关闭在不需要显示时调用pixels.fill((0,0,0))和pixels.show()来关闭所有LED。对于DotStar还可以设置pixels.deinit()来彻底关闭SPI总线。舵机保持扭矩舵机保持在某个角度是需要持续电流的保持扭矩。如果不需要维持位置可以考虑在到达位置后用一个小型继电器或MOSFET电路切断其电源或者使用my_servo.angle None如果库支持来释放PWM信号。使用asyncio实现多任务 对于需要同时控制多个独立动画如灯带流水、舵机扫描、读取传感器的复杂项目CircuitPython的asyncio库是救星。它允许你用“协程”的方式编写看似并行的任务而无需复杂的多线程。import asyncio async def sweep_servo(): while True: for angle in range(0, 180, 5): my_servo.angle angle await asyncio.sleep(0.05) for angle in range(180, 0, -5): my_servo.angle angle await asyncio.sleep(0.05) async def rainbow_led(): while True: for j in range(255): for i in range(num_pixels): rc_index (i * 256 // num_pixels) j pixels[i] colorwheel(rc_index 255) pixels.show() await asyncio.sleep(0.01) async def main(): task1 asyncio.create_task(sweep_servo()) task2 asyncio.create_task(rainbow_led()) await asyncio.gather(task1, task2) # 同时运行两个任务 asyncio.run(main())这样舵机和LED的动画就能真正地、平滑地同时运行了。5.3 扩展思路从原型到产品当你熟练掌握了这些基础控制后可以尝试将它们融入更大的项目物联网状态指示器用舵机指向不同的图标如温度、湿度、网络状态用NeoPixel灯环的颜色表示数值大小如蓝色到红色表示温度变化。通过Wi-Fi或蓝牙接收数据并更新显示。交互式艺术装置结合多个电容触摸点让用户触摸不同区域时触发不同的舵机动作和灯光秀。DotStar的高刷新率适合制作视觉暂留POV效果。机器人情感表达在小机器人头部安装两个舵机控制“眼睛”用一圈NeoPixel做“嘴巴”。根据机器人的状态寻找、发现、困惑、高兴做出不同的表情和灯光效果。自动化仪表盘用连续旋转舵机驱动一个指针式电压表或温度计的指针用LED灯带作为背景光或警报指示。