1. 项目概述与核心价值在嵌入式开发的世界里微控制器与外界的对话几乎都依赖于那几根看似简单的物理引脚。无论是点亮一串炫酷的RGB灯带还是从GPS模块读取经纬度坐标亦或是让一块小小的开发板模拟成键盘鼠标其背后都离不开对硬件通信接口的精准掌控。对于刚接触CircuitPython的开发者来说最常遇到的困惑之一就是“我手上的这块板子哪些引脚能用来做硬件SPIUART和I2C是不是只能用在标着TX/RX、SDA/SCL的特定引脚上” 这些问题不解决项目就卡在了第一步。本文要解决的正是这个从“知道协议”到“能用起来”的关键跨越。我们将深入探讨在CircuitPython环境下如何验证和使用硬件SPI、UART、I2C以及HID人机接口设备这四大核心接口。不同于照本宣科的理论手册我将结合自己多年在Adafruit、Seeed Studio等各类开发板上“踩坑”的经验为你提供一套即查即用的“实战工具箱”。你会发现通过几个精巧的脚本你可以快速探测出板卡上所有可用的硬件通信引脚组合而不仅仅是依赖板子丝印上的那几个默认选项。这对于需要连接多个同类型外设比如两个I2C传感器或者引脚资源紧张的项目来说无疑是雪中送炭。本文适合所有正在使用或打算使用CircuitPython进行嵌入式开发的爱好者、创客和工程师。无论你是想驱动DotStar LED获得更流畅的动画效果还是想与老式的串口设备通信或是快速搭建一个传感器网络甚至是想把你的开发板变成一个自定义的宏键盘或游戏手柄这里的内容都将为你扫清障碍。我们将从原理的简要对比入手但重点会放在那些能直接复制粘贴到你的code.py中并立刻看到结果的代码示例、接线图以及最重要的——那些官方文档里可能不会明说但却能让你少走弯路的注意事项和排查技巧。2. 硬件通信接口原理与选型浅析在动手写代码和接线之前花几分钟理解一下这三种主流串行通信协议的本质区别和适用场景能让你在后续的选型和排错中更加得心应手。它们就像是微控制器与外部设备对话的三种不同“语言”和“礼仪”。2.1 SPI追求速度的“专线电话”SPISerial Peripheral Interface是一种高速、全双工的同步通信总线。你可以把它想象成一个主设备通常是你的微控制器与一个或多个从设备如LED驱动器、屏幕、SD卡之间的“专线电话”系统。核心特点它需要四根线SCK时钟主设备发出、MOSI主出从入、MISO主入从出和CS/SS片选每个从设备一根。时钟信号由主设备严格掌控数据在时钟边沿被采样因此通信速率可以非常高且稳定。CircuitPython中的价值当使用硬件SPI时数据的搬移由芯片内部的专用硬件电路完成不占用CPU核心去模拟时序。这就是为什么在驱动类似DotStar这类LED时硬件SPI能获得远高于“位碰撞”bit-banging软件模拟的刷新率动画效果会无比流畅没有卡顿。应用场景高速ADC/DAC、存储器如Flash、SD卡、高刷新率显示屏、LED像素驱动器如APA102/DotStar。2.2 UART简单随性的“异步信件”UARTUniversal Asynchronous Receiver/Transmitter是一种异步串行通信协议。它就像两个人写信不需要共同的时钟来协调但双方必须事先约定好写信的格式波特率、数据位、停止位等。核心特点通常只需要两根线TX发送和RX接收。通信双方依靠起始位、停止位和预先设定好的波特率来同步每一帧数据。因为没有时钟线所以布线简单但速率和抗干扰能力通常不如SPI。CircuitPython中的价值UART是连接那些“老派”但极其常见的模块的桥梁比如GPS模块、蓝牙HC-05/06、一些老款传感器和另一个微控制器Arduino等。在CircuitPython中busio.UART对象让读取这些设备的串行数据流变得非常简单。应用场景GPS模块、蓝牙/Wi-Fi串口透传模块、与PC或其他MCU进行调试通信、工业传感器。2.3 I2C节省引脚的“共享总线”I2CInter-Integrated Circuit是一种多主多从、半双工的两线制串行总线。它像一个“共享电话线”总线上可以挂很多设备大家通过唯一的地址来区分。核心特点仅需两根线SDA数据线和SCL时钟线。总线通过上拉电阻保持高电平任何设备都可以通过拉低线路来通信。每个设备都有一个7位或10位的唯一地址。主设备产生时钟信号。CircuitPython中的价值I2C极大地节省了宝贵的GPIO引脚资源。你可以在两条线上挂载数十个传感器如温湿度、气压、光强、运动传感器。CircuitPython的board.I2C()单例模式和丰富的传感器库如adafruit_bme280,adafruit_tsl2591使得连接和读取数据异常便捷。应用场景各类低功耗传感器、EEPROM存储器、RTC时钟模块、IO扩展芯片。重要提示Adafruit的微控制器板如Feather、ItsyBitsy通常没有内置I2C上拉电阻。而大多数Adafruit的传感器分线板都内置了上拉电阻。但如果你是自己焊接的传感器或者使用其他品牌的模块务必在SDA和SCL线上各接一个2.2KΩ到10KΩ的电阻到3.3V否则I2C总线根本无法工作。理解了这些你就知道为什么驱动LED要优先找硬件SPI引脚为什么GPS模块要接UART以及为什么你的传感器库需要I2C地址了。接下来我们就进入实战环节看看如何“挖掘”出你板子上所有这些接口的潜力。3. 硬件SPI引脚验证与实战应用很多开发板为了方便用户会直接将硬件SPI的引脚通常是SCK和MOSI标记出来比如Feather M4 Express上的SCK和MOSI。但硬件SPI的功能可能远不止这两根引脚。通过芯片内部的引脚功能复用Pin Mux其他GPIO也可能被配置为SPI功能。如何快速找出它们盲目尝试只会得到ValueError我们需要一个聪明的探测脚本。3.1 SPI引脚验证脚本深度解析下面这个脚本是你探测硬件SPI能力的“瑞士军刀”。它的核心思想是尝试用你指定的两个引脚时钟和数据初始化一个busio.SPI对象。如果初始化成功说明这对引脚支持硬件SPI如果抛出ValueError则说明不支持。CircuitPython Essentials Hardware SPI pin verification script import board import busio def is_hardware_spi(clock_pin, data_pin): try: p busio.SPI(clock_pin, data_pin) p.deinit() return True except ValueError: return False # 提供你想要测试的两个引脚 if is_hardware_spi(board.A1, board.A2): print(This pin combination is hardware SPI!) else: print(This pin combination isn‘t hardware SPI.)代码逐行解读与实操要点导入核心库board库提供了对你当前使用的特定开发板引脚定义的访问。busio库则包含了SPI、I2C、UART等硬件通信对象的实现。定义探测函数is_hardware_spi函数接受两个参数时钟引脚和数据引脚。在try块中它尝试用这两个引脚创建busio.SPI对象。这里有一个关键细节创建成功后我们立即调用p.deinit()来释放这个SPI对象。这是一个好习惯确保资源被及时清理避免后续操作冲突。异常处理如果引脚不支持硬件SPIbusio.SPI()初始化会引发ValueError。我们在except中捕获它并返回False。执行测试脚本最后测试了board.A1和board.A2这一组合。你需要将这两个参数替换成你想测试的任何两个引脚对象例如board.D5和board.D6。如何使用这个脚本将整个脚本复制到你的开发板的code.py文件中。根据你的需求修改is_hardware_spi(board.A1, board.A2)中的引脚名称。保存文件电路板会自动重启运行。打开串行监视器Serial Console在Mu编辑器、Thonny或VS Code的CircuitPython插件中都可以找到查看打印结果。3.2 扩展自动扫描所有可能的SPI引脚对手动修改引脚测试效率太低。我们可以写一个增强版脚本自动遍历板子上所有可用的引脚找出所有能作为硬件SPI时钟和数据的引脚组合。CircuitPython Hardware SPI Pin Scanner import board import busio from microcontroller import Pin def is_hardware_spi(clock_pin, data_pin): try: p busio.SPI(clock_pin, data_pin) p.deinit() return True except ValueError: return False def get_unique_pins(): # 排除一些非标准GPIO或特殊功能引脚 exclude [NEOPIXEL, APA102_MOSI, APA102_SCK] pins [pin for pin in [ getattr(board, p) for p in dir(board) if p not in exclude] if isinstance(pin, Pin)] # 去重因为有些引脚可能有多个别名 unique [] for p in pins: if p not in unique: unique.append(p) return unique unique_pins get_unique_pins() print(Scanning for hardware SPI pin pairs...) found_any False for clk_pin in unique_pins: for data_pin in unique_pins: if clk_pin is data_pin: continue # 时钟和数据不能是同一个引脚 if is_hardware_spi(clk_pin, data_pin): print(fHardware SPI found: CLK{clk_pin}, DATA{data_pin}) found_any True if not found_any: print(No hardware SPI pin pairs found (unlikely on most boards).) else: print(Scan complete.)这个脚本会列出当前板卡上所有可能的硬件SPI引脚对。运行它你可能会惊喜地发现除了标明的SCK/MOSI还有另外几组引脚也支持硬件SPI。这在设计复杂项目、需要避开某些已被占用的引脚时非常有用。3.3 硬件SPI驱动DotStar LED实例找到硬件SPI引脚后我们来看一个实际应用驱动APA102DotStarLED。软件模拟SPI在LED数量多时刷新率会急剧下降导致动画闪烁。硬件SPI则能保持流畅。接线示意图以通用板为例LED DI数据输入- 微控制器的MOSI引脚或你验证过的硬件SPI数据引脚LED CI时钟输入- 微控制器的SCK引脚或你验证过的硬件SPI时钟引脚LED VCC- 5V或3.3V查看你的LED灯带规格LED GND- GND注意如果灯带较长请在电源正负极之间并联一个470μF以上的电解电容靠近灯带接入以防止上电时的电流冲击导致单片机复位。代码示例首先确保你的/lib文件夹下有adafruit_dotstar.mpy库。import board import adafruit_dotstar import time # 使用硬件SPI引脚初始化DotStar # 这里以常见的默认SPI引脚为例你可以替换成你扫描到的其他硬件SPI引脚 dots adafruit_dotstar.DotStar(board.SCK, board.MOSI, 30, brightness0.2) # 控制30颗灯 # 填充红色 dots.fill((255, 0, 0)) dots.show() time.sleep(1) # 跑马灯效果 for i in range(len(dots)): dots[i] (0, 255, 0) # 设置一颗灯为绿色 dots.show() time.sleep(0.05) dots[i] (0, 0, 0) # 关闭关键点adafruit_dotstar.DotStar初始化时前两个参数就是时钟和数据引脚。使用硬件SPI引脚dots.show()的调用会非常高效即使同时更新上百颗LED也能达到很高的帧率。4. UART串口通信配置与GPS数据读取UART是嵌入式项目中最经典的“串口”用于与大量现成的模块通信。CircuitPython的busio.UART类让串口通信变得非常简单。4.1 基础UART通信示例以下代码创建了一个UART对象并不断读取可能收到的数据。我们用一个LED闪烁来指示数据接收事件这在调试时非常直观。CircuitPython Essentials UART Serial example import board import busio import digitalio # 初始化板载LED大多数板子 led digitalio.DigitalInOut(board.LED) led.direction digitalio.Direction.OUTPUT # 创建UART对象使用默认的TX和RX引脚波特率9600 uart busio.UART(board.TX, board.RX, baudrate9600) # 注意有些板子如Gemma M0的TX/RX没有明确标记需要查引脚图。 print(UART Ready. Waiting for data...) while True: data uart.read(32) # 尝试读取最多32字节 if data is not None: # 如果收到数据 led.value True # LED亮表示收到数据 # 将字节数组bytearray转换为字符串打印 data_string .join([chr(b) for b in data]) print(data_string, end) led.value False # LED灭代码细节与避坑指南uart.read(num)这是非阻塞调用。它会立即返回当前接收缓冲区中的所有数据最多num字节。如果缓冲区为空则返回None。千万不要用while uart.read() is None:这样的循环去等待数据这会浪费大量CPU资源。正确的做法是像上面一样在主循环中定期检查或者配合time.sleep()进行适度轮询。引脚交叉这是UART接线中最常见的错误记住一个原则发送端TX应该连接接收端RX。即你的微控制器的TX引脚应连接到GPS模块的RX引脚微控制器的RX引脚连接到GPS模块的TX引脚。如果连接后收不到数据第一反应就是调换这两根线试试。波特率匹配baudrate参数必须与通信对方如GPS模块设置的波特率完全一致。常见的波特率有9600, 115200等。不匹配会导致收到乱码。4.2 连接GPS模块实战我们以Adafruit Ultimate GPS模块为例。接线时除了TX/RX交叉连接别忘了连接电源VIN和地GND。通用接线表以3.3V逻辑电平模块为例你的开发板GPS模块线缆颜色建议3.3VVIN红色GNDGND黑色RXTX蓝色TXRX白色重要提示不同开发板的供电能力不同。像Trinket M0、QT Py M0这类通过USB直接供电的板子其VUSB或3V引脚可能无法为功耗较大的GPS模块提供足够电流。如果遇到模块工作不稳定尝试使用外部电源单独为GPS模块供电并确保共地。将上面的基础UART代码上传到正确接线的开发板打开串口监视器你就能看到源源不断的NMEA语句输出例如$GPGGA,....。即使GPS模块尚未定位没有“锁星”它也会输出时间等基本信息方便你在室内测试。4.3 发现隐藏的UART引脚和SPI一样许多基于SAMD21等架构的微控制器支持在多个引脚上复用UART功能。以下脚本可以帮你找出所有可用的RX/TX引脚对。CircuitPython UART possible pin-pair identifying script import board import busio from microcontroller import Pin def is_hardware_uart(tx, rx): try: p busio.UART(tx, rx) p.deinit() return True except ValueError: return False def get_unique_pins(): exclude [NEOPIXEL, APA102_MOSI, APA102_SCK] pins [pin for pin in [ getattr(board, p) for p in dir(board) if p not in exclude] if isinstance(pin, Pin)] unique [] for p in pins: if p not in unique: unique.append(p) return unique print(Searching for possible hardware UART pin pairs (TX, RX)...) for tx_pin in get_unique_pins(): for rx_pin in get_unique_pins(): if rx_pin is tx_pin: continue if is_hardware_uart(tx_pin, rx_pin): print(fTX: {tx_pin}, \t RX: {rx_pin})运行这个脚本输出可能会很长。它列出了所有可以组成有效硬件UART的引脚对。这意味着你可以在同一个项目中使用多个UART设备只要它们占用不同的引脚对。4.4 Trinket M0上的特殊注意事项在大多数板子上UART和I2C的初始化顺序无关紧要。但Trinket M0是一个特例。由于其引脚复用的特殊性你必须先创建UART对象再创建I2C对象。正确顺序import board uart board.UART() # 这会使用引脚4(TX)和3(RX)波特率9600 i2c board.I2C() # 这会使用引脚2(SCL)和0(SDA)错误顺序会导致ValueError: Invalid pinsimport board i2c board.I2C() # 先创建I2C uart board.UART() # 再创建UART会失败这是一个经典的“坑”如果你在Trinket M0上同时使用UART和I2C务必记住这个顺序。5. I2C设备扫描与传感器数据读取I2C因其简洁的两线制成为连接多个传感器的首选。在CircuitPython中使用I2C通常分为两步扫描总线找到设备地址然后使用对应的库与设备通信。5.1 I2C总线扫描找到你的设备在连接任何I2C设备后第一步总是先扫描确认设备是否被正确识别。这能帮你快速排除接线错误、电源问题或地址冲突。CircuitPython I2C Device Address Scan import time import board # 使用默认的I2C总线board.SCL和board.SDA i2c board.I2C() # 锁定I2C总线以便扫描 while not i2c.try_lock(): pass try: while True: # 扫描并打印所有发现的7位I2C地址16进制格式 addresses i2c.scan() if addresses: print(I2C addresses found:, [hex(addr) for addr in addresses]) else: print(No I2C devices found.) time.sleep(2) # 每2秒扫描一次 finally: # 使用CtrlC退出循环后确保解锁I2C总线 i2c.unlock()代码原理与操作解析board.I2C()这是一个单例Singleton调用。无论你在代码中调用多少次board.I2C()它返回的都是同一个I2C对象这避免了资源冲突。它默认使用板子上标记的SCL和SDA引脚。i2c.try_lock()I2C总线是一个共享资源。在对其进行扫描或读写操作前必须“锁定”它以防止其他代码或REPL中的手动操作同时访问导致冲突。try_lock()会尝试获取锁如果成功则返回True。i2c.scan()这个方法会向所有可能的7位I2C地址0x08 到 0x77发送探测信号。如果有设备应答则该地址会被加入返回的列表。i2c.unlock()在finally块中解锁总线是一个好习惯。即使程序因异常或键盘中断CtrlC而停止也能确保总线被释放避免板子“死锁”需要硬重启。常见问题排查扫描不到任何地址首先检查接线SDA, SCL, VCC, GND。最可能的原因是缺少上拉电阻。如果你的传感器板没有内置上拉很多非Adafruit模块没有必须在SDA和SCL线上各接一个2.2KΩ-10KΩ的电阻到3.3V。地址与预期不符查阅传感器数据手册。有些设备的地址可以通过焊接地址选择引脚ADDR, A0等来改变。例如TSL2591的默认地址是0x29但BME280可能是0x76或0x77。5.2 连接并读取TSL2591光照传感器假设我们已通过扫描确认TSL2591在地址0x29上。接下来使用专用的库来读取数据。准备工作将adafruit_tsl2591.mpy库和其依赖的adafruit_bus_device文件夹放入开发板的/lib目录中。接线使用STEMMA QT/QWIIC连接器是最简单的方式即插即用。对于没有这种接口的板子参考以下通用接线传感器 VIN- 开发板3.3V传感器 GND- 开发板GND传感器 SCL- 开发板SCL传感器 SDA- 开发板SDA数据读取代码CircuitPython Essentials I2C sensor example using TSL2591 import time import board import adafruit_tsl2591 # 初始化I2C总线 i2c board.I2C() # 创建传感器对象传入I2C总线对象 sensor adafruit_tsl2591.TSL2591(i2c) # 可选配置传感器增益和积分时间以适应不同光照环境 # sensor.gain adafruit_tsl2591.GAIN_LOW (1x) 默认 # sensor.gain adafruit_tsl2591.GAIN_MED (25x) # sensor.gain adafruit_tsl2591.GAIN_HIGH (428x) # sensor.gain adafruit_tsl2591.GAIN_MAX (9876x) # sensor.integration_time adafruit_tsl2591.INTEGRATIONTIME_100MS (默认) print(TSL2591 Lux Sensor) print(Gain: , sensor.gain) print(Integration time: , sensor.integration_time) while True: try: # 读取并打印照度值单位勒克斯 Lux lux sensor.lux if lux is not None: print(fLux: {lux:.2f}) else: print(Lux: Sensor saturated, reduce gain!) except Exception as e: print(fRead error: {e}) time.sleep(1.0) # 每秒读取一次库的使用模式这是CircuitPython传感器库的典型用法。首先通过board.I2C()获取I2C总线对象然后将这个总线对象作为参数传递给传感器类的构造函数如TSL2591(i2c)。之后你就可以通过传感器对象的属性如sensor.lux或方法轻松读取数据。这种设计非常清晰将底层的通信细节完全封装在库中。5.3 探索更多I2C引脚可能性同样对于SAMD21、SAMD51和nRF52840等芯片I2C功能也可以映射到多个引脚上。使用下面的脚本可以发现所有可用的SCL/SDA引脚对。CircuitPython I2C possible pin-pair identifying script import board import busio from microcontroller import Pin def is_hardware_i2c(scl, sda): try: p busio.I2C(scl, sda) p.deinit() return True except ValueError: return False except RuntimeError: # 某些情况下即使引脚有效初始化时也可能因总线忙返回RuntimeError # 这通常也意味着该引脚对在硬件上是支持的 return True def get_unique_pins(): exclude [NEOPIXEL, APA102_MOSI, APA102_SCK] pins [pin for pin in [ getattr(board, p) for p in dir(board) if p not in exclude] if isinstance(pin, Pin)] unique [] for p in pins: if p not in unique: unique.append(p) return unique print(Searching for possible hardware I2C pin pairs (SCL, SDA)...) for scl_pin in get_unique_pins(): for sda_pin in get_unique_pins(): if scl_pin is sda_pin: continue if is_hardware_i2c(scl_pin, sda_pin): print(fSCL: {scl_pin}, \t SDA: {sda_pin})这个脚本的输出能告诉你除了默认的I2C引脚你还有哪些备用选择。这对于需要多个I2C总线例如一个总线接3.3V设备另一个接5V设备通过电平转换器隔离的高级应用非常有用。你可以使用busio.I2C(board.SCL1, board.SDA1)这样的方式来创建第二个I2C对象。6. HID键盘与鼠标模拟实战CircuitPython最有趣的功能之一就是HID人机接口设备。这意味着你的开发板可以伪装成一个USB键盘或鼠标向连接的电脑发送按键和鼠标移动指令。你可以用它制作宏键盘、演示遥控器、无障碍辅助设备甚至游戏控制器。6.1 将开发板变成按键触发器下面的示例将两个引脚A1和A2设置为接地触发按键。当引脚通过导线或按钮接地时它会模拟按下键盘按键。CircuitPython Essentials HID Keyboard example import time import board import digitalio import usb_hid from adafruit_hid.keyboard import Keyboard from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS from adafruit_hid.keycode import Keycode # 定义用于触发按键的引脚内部上拉默认高电平 keypress_pins [board.A1, board.A2] # 定义按下后发送的按键或字符串 keys_pressed [Keycode.A, Hello World!\n] # 第一个发送A第二个发送字符串 # 定义组合键如Shift, Control, Alt。这里用Shift配合A打出大写A。 control_key Keycode.SHIFT # 初始化按键引脚对象数组 key_pin_array [] for pin in keypress_pins: key_pin digitalio.DigitalInOut(pin) key_pin.direction digitalio.Direction.INPUT key_pin.pull digitalio.Pull.UP # 启用内部上拉电阻 key_pin_array.append(key_pin) # 初始化键盘对象 time.sleep(1) # 等待USB HID稳定避免竞争条件 keyboard Keyboard(usb_hid.devices) keyboard_layout KeyboardLayoutUS(keyboard) # 使用美式键盘布局 # 初始化状态LED led digitalio.DigitalInOut(board.LED) led.direction digitalio.Direction.OUTPUT print(Waiting for key pin...) while True: for i, key_pin in enumerate(key_pin_array): if not key_pin.value: # 检测引脚是否被拉低接地 print(fPin #{i} ({keypress_pins[i]}) is grounded.) led.value True # 点亮LED表示动作触发 # 等待引脚释放防抖和防止重复触发 while not key_pin.value: time.sleep(0.01) # 短延时降低CPU占用 # 执行按键动作 key keys_pressed[i] if isinstance(key, str): keyboard_layout.write(key) # 输入字符串 else: keyboard.press(control_key, key) # 按下组合键 keyboard.release_all() # 释放所有按键必须调用 led.value False # 熄灭LED time.sleep(0.01) # 主循环短延时核心机制与注意事项上拉电阻与接地触发我们将引脚设置为输入模式并启用内部上拉电阻。引脚常态为高电平value为True。当用导线将引脚连接到GND时引脚被拉低value变为False触发按键动作。防抖与等待释放while not key_pin.value:循环会一直等待直到用户断开接地引脚恢复高电平。这实现了简单的“按下-释放”检测并避免了在接地期间连续触发。keyboard.release_all()至关重要当你使用keyboard.press()按下按键后必须调用keyboard.release_all()来释放它们。否则电脑会认为该按键一直被按住导致“按键粘滞”。键盘布局我们使用了KeyboardLayoutUS。如果你需要其他布局如德语、法语需要在/lib中安装对应的库如adafruit_hid.keyboard_layout_de并修改导入和初始化语句。扩展你可以轻松添加更多引脚到keypress_pins数组和对应的动作到keys_pressed数组。动作可以是单个Keycode如Keycode.F1、字符串甚至是多个键码的列表以实现复杂宏。6.2 用摇杆模拟鼠标这个例子将一个双轴摇杆带按键变成一个鼠标。摇杆的X、Y轴模拟鼠标移动按键模拟鼠标左键点击。接线摇杆 VCC - 3.3V摇杆 GND - GND摇杆 X轴 - A0模拟输入摇杆 Y轴 - A1模拟输入摇杆 按键SW - A2数字输入上拉代码实现CircuitPython Essentials HID Mouse example import time import analogio import board import digitalio import usb_hid from adafruit_hid.mouse import Mouse mouse Mouse(usb_hid.devices) # 初始化摇杆模拟输入和按键 x_axis analogio.AnalogIn(board.A0) y_axis analogio.AnalogIn(board.A1) select digitalio.DigitalInOut(board.A2) select.direction digitalio.Direction.INPUT select.pull digitalio.Pull.UP # 按键按下时接地 # 校准参数需要根据你的摇杆实际电压范围调整 # 读取摇杆居中时的电压值作为“死区”中心 pot_min 0.00 # 摇杆推到一端的最小电压 pot_max 3.29 # 摇杆推到另一端的最大电压接近3.3V step (pot_max - pot_min) / 20.0 # 将电压范围划分为20步 def get_voltage(pin): 将ADC读取的原始值0-65535转换为电压0-3.3V return (pin.value * 3.3) / 65536 def steps(axis): 将电压值映射到0-20的步进值10为居中 return round((axis - pot_min) / step) while True: x get_voltage(x_axis) y get_voltage(y_axis) # 检查按键是否被按下接地 if not select.value: mouse.click(Mouse.LEFT_BUTTON) time.sleep(0.2) # 按键去抖延时 # 根据X轴位置移动鼠标左右 x_steps steps(x) if x_steps 11: # 向右轻微移动 mouse.move(x1) elif x_steps 9: # 向左轻微移动 mouse.move(x-1) elif x_steps 19: # 向右快速移动 mouse.move(x8) elif x_steps 1: # 向左快速移动 mouse.move(x-8) # 根据Y轴位置移动鼠标上下 y_steps steps(y) if y_steps 11: # 向下轻微移动 (注意屏幕坐标系Y轴向下为正) mouse.move(y1) elif y_steps 9: # 向上轻微移动 mouse.move(y-1) elif y_steps 19: # 向下快速移动 mouse.move(y8) elif y_steps 1: # 向上快速移动 mouse.move(y-8) time.sleep(0.01) # 控制循环速度影响鼠标移动灵敏度校准与调优心得死区设置代码中9到11的区间是死区。当摇杆的电压值映射到steps后落在这个范围内鼠标不会移动。这很重要因为摇杆在物理上很难精确回到中心点没有死区会导致鼠标漂移。你需要通过串口打印x_steps和y_steps的值观察摇杆在自然松开时的中心值并据此调整死区阈值例如10为中心8-12为死区。移动速度mouse.move(x1)的参数控制单次移动的像素量。你可以创建多级速度如代码中所示轻微偏移时移动1像素推到边缘时移动8像素实现加速效果。Y轴方向注意屏幕坐标系中Y轴正方向是向下的。所以当摇杆向上推物理上我们发送y-1让鼠标指针向上移动。按键去抖机械按键在按下和释放时会产生抖动可能导致多次误触发。time.sleep(0.2)是一个简单的软件去抖方法。对于要求更高的场景可以考虑更复杂的去抖逻辑。通过组合键盘和鼠标模拟你可以创造出非常有趣的交互项目比如用几个电容触摸传感器做一个简单的音乐键盘或者用距离传感器控制鼠标滚轮。HID功能为CircuitPython项目打开了通往桌面自动化的大门。