CircuitPython硬件通信接口实战:SPI、UART、I2C与HID引脚验证与应用
2026/5/15 21:56:28 网站建设 项目流程

1. 项目概述与核心价值

在嵌入式开发的世界里,微控制器与外界的对话,几乎都依赖于那几根看似简单的物理引脚。无论是点亮一串炫酷的RGB灯带,还是从GPS模块读取经纬度坐标,亦或是让一块小小的开发板模拟成键盘鼠标,其背后都离不开对硬件通信接口的精准掌控。对于刚接触CircuitPython的开发者来说,最常遇到的困惑之一就是:“我手上的这块板子,哪些引脚能用来做硬件SPI?UART和I2C是不是只能用在标着TX/RX、SDA/SCL的特定引脚上?” 这些问题不解决,项目就卡在了第一步。

本文要解决的,正是这个从“知道协议”到“能用起来”的关键跨越。我们将深入探讨在CircuitPython环境下,如何验证和使用硬件SPI、UART、I2C以及HID(人机接口设备)这四大核心接口。不同于照本宣科的理论手册,我将结合自己多年在Adafruit、Seeed Studio等各类开发板上“踩坑”的经验,为你提供一套即查即用的“实战工具箱”。你会发现,通过几个精巧的脚本,你可以快速探测出板卡上所有可用的硬件通信引脚组合,而不仅仅是依赖板子丝印上的那几个默认选项。这对于需要连接多个同类型外设(比如两个I2C传感器)或者引脚资源紧张的项目来说,无疑是雪中送炭。

本文适合所有正在使用或打算使用CircuitPython进行嵌入式开发的爱好者、创客和工程师。无论你是想驱动DotStar LED获得更流畅的动画效果,还是想与老式的串口设备通信,或是快速搭建一个传感器网络,甚至是想把你的开发板变成一个自定义的宏键盘或游戏手柄,这里的内容都将为你扫清障碍。我们将从原理的简要对比入手,但重点会放在那些能直接复制粘贴到你的code.py中并立刻看到结果的代码示例、接线图以及最重要的——那些官方文档里可能不会明说,但却能让你少走弯路的注意事项和排查技巧。

2. 硬件通信接口原理与选型浅析

在动手写代码和接线之前,花几分钟理解一下这三种主流串行通信协议的本质区别和适用场景,能让你在后续的选型和排错中更加得心应手。它们就像是微控制器与外部设备对话的三种不同“语言”和“礼仪”。

2.1 SPI:追求速度的“专线电话”

SPI(Serial 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:简单随性的“异步信件”

UART(Universal Asynchronous Receiver/Transmitter)是一种异步串行通信协议。它就像两个人写信,不需要共同的时钟来协调,但双方必须事先约定好写信的格式(波特率、数据位、停止位等)。

  • 核心特点:通常只需要两根线:TX(发送)和RX(接收)。通信双方依靠起始位、停止位和预先设定好的波特率来同步每一帧数据。因为没有时钟线,所以布线简单,但速率和抗干扰能力通常不如SPI。
  • CircuitPython中的价值:UART是连接那些“老派”但极其常见的模块的桥梁,比如GPS模块、蓝牙HC-05/06、一些老款传感器和另一个微控制器(Arduino等)。在CircuitPython中,busio.UART对象让读取这些设备的串行数据流变得非常简单。
  • 应用场景:GPS模块、蓝牙/Wi-Fi串口透传模块、与PC或其他MCU进行调试通信、工业传感器。

2.3 I2C:节省引脚的“共享总线”

I2C(Inter-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上的SCKMOSI。但硬件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.")

代码逐行解读与实操要点:

  1. 导入核心库board库提供了对你当前使用的特定开发板引脚定义的访问。busio库则包含了SPI、I2C、UART等硬件通信对象的实现。
  2. 定义探测函数is_hardware_spi函数接受两个参数,时钟引脚和数据引脚。在try块中,它尝试用这两个引脚创建busio.SPI对象。这里有一个关键细节:创建成功后,我们立即调用p.deinit()来释放这个SPI对象。这是一个好习惯,确保资源被及时清理,避免后续操作冲突。
  3. 异常处理:如果引脚不支持硬件SPI,busio.SPI()初始化会引发ValueError。我们在except中捕获它并返回False
  4. 执行测试:脚本最后测试了board.A1board.A2这一组合。你需要将这两个参数替换成你想测试的任何两个引脚对象,例如board.D5board.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(f"Hardware 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引脚后,我们来看一个实际应用:驱动APA102(DotStar)LED。软件模拟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, brightness=0.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, baudrate=9600) # 注意:有些板子(如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直接供电的板子,其VUSB3V引脚可能无法为功耗较大的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(f"TX: {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 pins):

import 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: # 使用Ctrl+C退出循环后,确保解锁I2C总线 i2c.unlock()

代码原理与操作解析:

  1. board.I2C():这是一个单例(Singleton)调用。无论你在代码中调用多少次board.I2C(),它返回的都是同一个I2C对象,这避免了资源冲突。它默认使用板子上标记的SCLSDA引脚。
  2. i2c.try_lock():I2C总线是一个共享资源。在对其进行扫描或读写操作前,必须“锁定”它,以防止其他代码(或REPL中的手动操作)同时访问导致冲突。try_lock()会尝试获取锁,如果成功则返回True
  3. i2c.scan():这个方法会向所有可能的7位I2C地址(0x08 到 0x77)发送探测信号。如果有设备应答,则该地址会被加入返回的列表。
  4. i2c.unlock():在finally块中解锁总线是一个好习惯。即使程序因异常或键盘中断(Ctrl+C)而停止,也能确保总线被释放,避免板子“死锁”需要硬重启。

常见问题排查:

  • 扫描不到任何地址:首先检查接线(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(f"Lux: {lux:.2f}") else: print("Lux: Sensor saturated, reduce gain!") except Exception as e: print(f"Read 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(f"SCL: {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(f"Pin #{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) # 主循环短延时

核心机制与注意事项:

  1. 上拉电阻与接地触发:我们将引脚设置为输入模式并启用内部上拉电阻。引脚常态为高电平(valueTrue)。当用导线将引脚连接到GND时,引脚被拉低(value变为False),触发按键动作。
  2. 防抖与等待释放while not key_pin.value:循环会一直等待,直到用户断开接地(引脚恢复高电平)。这实现了简单的“按下-释放”检测,并避免了在接地期间连续触发。
  3. keyboard.release_all()至关重要:当你使用keyboard.press()按下按键后,必须调用keyboard.release_all()来释放它们。否则,电脑会认为该按键一直被按住,导致“按键粘滞”。
  4. 键盘布局:我们使用了KeyboardLayoutUS。如果你需要其他布局(如德语、法语),需要在/lib中安装对应的库(如adafruit_hid.keyboard_layout_de),并修改导入和初始化语句。
  5. 扩展:你可以轻松添加更多引脚到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(x=1) elif x_steps < 9: # 向左轻微移动 mouse.move(x=-1) elif x_steps > 19: # 向右快速移动 mouse.move(x=8) elif x_steps < 1: # 向左快速移动 mouse.move(x=-8) # 根据Y轴位置移动鼠标(上下) y_steps = steps(y) if y_steps > 11: # 向下轻微移动 (注意:屏幕坐标系Y轴向下为正) mouse.move(y=1) elif y_steps < 9: # 向上轻微移动 mouse.move(y=-1) elif y_steps > 19: # 向下快速移动 mouse.move(y=8) elif y_steps < 1: # 向上快速移动 mouse.move(y=-8) time.sleep(0.01) # 控制循环速度,影响鼠标移动灵敏度

校准与调优心得:

  1. 死区设置:代码中911的区间是死区。当摇杆的电压值映射到steps后落在这个范围内,鼠标不会移动。这很重要,因为摇杆在物理上很难精确回到中心点,没有死区会导致鼠标漂移。你需要通过串口打印x_stepsy_steps的值,观察摇杆在自然松开时的中心值,并据此调整死区阈值(例如10为中心,8-12为死区)。
  2. 移动速度mouse.move(x=1)的参数控制单次移动的像素量。你可以创建多级速度,如代码中所示,轻微偏移时移动1像素,推到边缘时移动8像素,实现加速效果。
  3. Y轴方向:注意屏幕坐标系中,Y轴正方向是向下的。所以当摇杆向上推(物理上),我们发送y=-1让鼠标指针向上移动。
  4. 按键去抖:机械按键在按下和释放时会产生抖动,可能导致多次误触发。time.sleep(0.2)是一个简单的软件去抖方法。对于要求更高的场景,可以考虑更复杂的去抖逻辑。

通过组合键盘和鼠标模拟,你可以创造出非常有趣的交互项目,比如用几个电容触摸传感器做一个简单的音乐键盘,或者用距离传感器控制鼠标滚轮。HID功能为CircuitPython项目打开了通往桌面自动化的大门。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询