1. 项目概述
如果你玩过CircuitPython,大概率对插上USB线后电脑上自动弹出的CIRCUITPY盘符和串口终端不陌生。这很方便,让你能像操作U盘一样拖拽代码文件,也能随时打开串口监视器查看打印信息。但当你真正想把手里的开发板变成一个“正经”的USB设备,比如一个专属的宏键盘、一个游戏手柄,或者一个不希望被用户随意修改内部文件的数据采集器时,这些默认出现的“开发工具”就显得有些碍眼了。它们不仅占用了系统资源,更关键的是,会让你的最终产品在用户电脑上看起来像个半成品开发板,而不是一个独立、专业的设备。
这正是CircuitPython USB设备自定义的核心价值所在。它允许你,作为一名开发者,在boot.py这个特殊的启动脚本中,像导演一样编排你的开发板在USB舞台上的“角色”。你可以让存储设备CIRCUITPY在演出时“隐身”,可以关闭那个用于调试的串口终端(REPL),甚至可以启用第二个纯粹的、不受干扰的数据串口。更进一步,你还能定义全新的、符合USB HID标准的自定义设备,比如一个带有特殊摇杆和按钮布局的游戏手柄,或者一个工业用的控制面板。
这个过程不仅仅是开关几个功能,它涉及到对USB底层机制的理解,比如“端点”这个硬件资源的分配,以及如何编写描述设备能力的“报告描述符”。本篇文章,我将结合自己多次将CircuitPython项目产品化的经验,从最基础的配置讲起,一直深入到自定义HID设备的开发,帮你彻底掌握如何让你的CircuitPython设备在USB世界里表现得既专业又高效。
2. 核心概念与boot.py的绝对权威
在开始动手修改之前,我们必须建立一个最重要的认知:所有USB设备的配置,都必须且只能在boot.py文件中进行。这是CircuitPython设计上的一个铁律,理解其背后的“为什么”,能帮你避开无数坑。
2.1 为什么必须是boot.py?
你可以把开发板连接电脑的过程想象成一次外交会晤。boot.py运行于会晤开始前的“内部筹备会议”阶段。在这个阶段,你的开发板还没有和电脑(主机)建立任何正式的USB通信连接。此时,你可以从容地决定这次会晤要派出哪些“代表”(USB设备),以及每个代表的“职权范围”(设备描述符)。
一旦boot.py执行完毕,CircuitPython就会根据你的配置,生成一份完整的“外交人员名单和设备能力说明书”(即USB描述符集)。紧接着,开发板才会正式向电脑“亮明身份”,开始枚举过程。电脑会读取这份说明书,并为每一个设备分配资源、安装驱动。
而你的主程序code.py,则是在枚举过程开始后才启动的。此时,USB连接的“外交框架”已经确立,木已成舟。如果你试图在code.py中调用storage.disable_usb_drive(),CircuitPython会直接抛出一个错误,因为USB大容量存储设备这个“代表”已经在会晤中开始工作了,你无法中途将其撤下。
实操心得:我早期就犯过在
code.py里尝试禁用设备的错误,结果程序直接崩溃。记住这个顺序:硬件复位 ->boot.py执行(配置USB)-> USB枚举 ->code.py执行。任何想动态切换USB设备功能(比如运行时让CIRCUITPY出现又消失)的需求,在标准CircuitPython下是无法实现的,必须在设计之初就在boot.py里定死。
2.2 硬复位与文件写入完成
与boot.py的权威性紧密相关的另一个关键点是:boot.py只在硬复位(Hard Reset)后运行。
什么是硬复位?就是你给板子重新上电,或者按下了物理的复位(RESET)按钮。在REPL里输入Ctrl+D进行软复位,或者只是修改并保存了boot.py文件,都不会触发boot.py的重新执行。
这带来一个非常重要的操作流程:每次修改boot.py后,你必须执行一次硬复位,新的配置才会生效。而且,在复位前,你必须确保修改已经完全写入板载的存储中。
避坑指南:CircuitPython的文件写入有时不是立即完成的,特别是当你通过某些编辑器或IDE保存时,可能会有缓存。最稳妥的做法是,在编辑完
boot.py后,在文件管理器中对CIRCUITPY盘执行一次“弹出”或“安全移除硬件”操作。等待系统提示“可以安全移除硬件”后,再按复位键。我曾经因为没等写入完成就复位,导致boot.py文件损坏,整个文件系统需要重新格式化,项目代码全丢。
3. 基础设备管理:隐藏、显示与串口倍增
掌握了boot.py的运作机制,我们就可以开始实操了。我们从最简单的开始:管理那些CircuitPython默认提供的标准设备。
3.1 让CIRCUITPY盘符消失
对于要作为成品交付的设备,让内部的文件系统对用户不可见是基本要求。这能防止用户误删或修改关键文件,也让设备看起来更专业。
import storage storage.disable_usb_drive() # 这行代码会让CIRCUITPY盘符在电脑上消失就这么简单。但这里藏着一个巨大的“陷阱”,原文也用了醒目的警告:不要只写这一行!
试想,你写了一个boot.py,里面只有storage.disable_usb_drive()。你把它放到板子上,复位。好了,CIRCUITPY盘符不见了,你的程序code.py开始运行,设备工作正常。但有一天,你需要更新code.py里的逻辑,或者修复一个bug,你该怎么办?你没有任何办法再把CIRCUITPY弄出来,因为你无法修改boot.py了——它所在的盘符根本看不见!
这就是所谓的“把自己锁在外面”。为了避免这种情况,你必须设计一个“逃生舱”机制。最常见的做法是使用一个硬件按钮。在boot.py中检测这个按钮的状态,如果按钮没有被按下,则禁用设备;如果检测到按钮被按下,则保持设备启用。
import storage import board import digitalio # 假设我们使用板载的按钮A,按下时连接到高电平(如Circuit Playground Express) button = digitalio.DigitalInOut(board.BUTTON_A) button.switch_to_input(pull=digitalio.Pull.DOWN) # 启用内部下拉电阻 # 仅当按钮未被按下时,才禁用USB存储 if not button.value: # 按钮按下时value为True(高电平) storage.disable_usb_drive() print(“Boot: USB Drive disabled.”) # 这个打印会进入boot_out.txt else: print(“Boot: Button held, USB Drive remains enabled.”)这样,在需要更新程序时,你只需要按住按钮再给板子上电或复位,CIRCUITPY盘符就会正常出现,供你修改文件。松开按钮再复位,它又会隐藏起来。
注意事项:
storage.disable_usb_drive()只是让电脑无法通过USB访问这个存储,在CircuitPython内部,你依然可以通过文件系统API(如open())读写CIRCUITPY上的文件。从CircuitPython 9.0.0开始,当USB驱动被禁用时,文件系统会自动对内部代码变为可读写。更早的版本可能需要手动调用storage.remount(“/”, readonly=False)。
3.2 管理串口:REPL与数据通道
CircuitPython默认启用一个串行通信设备(CDC),它直接连接到Python的REPL(交互式解释器)。这对于调试至关重要,但同样,在产品中我们可能想关闭它,或者需要额外的、纯净的数据通道。
相关的模块是usb_cdc(Communications Device Class)。它管理两个逻辑设备:
console: 连接REPL的串口。默认启用。data: 一个独立的、不与REPL交互的数据串口。默认禁用。
import usb_cdc # 方案1:完全禁用所有串口(与usb_cdc.disable()等效) usb_cdc.enable(console=False, data=False) # 方案2:仅启用REPL控制台(默认状态) usb_cdc.enable(console=True, data=False) # 方案3:同时启用控制台和数据端口(非常有用!) usb_cdc.enable(console=True, data=True) # 方案4:禁用控制台,但启用数据端口(用于纯数据产品) usb_cdc.enable(console=False, data=True)为什么需要第二个数据端口?想象你在做一个传感器数据记录器。你的code.py不断读取传感器并通过print()发送数据。如果使用默认的console端口,所有数据都会混在REPL流里。更麻烦的是,如果数据流中偶然出现了ASCII码3(Ctrl+C),主机端的串口工具会将其解释为中断信号,可能打断你的接收程序。而data端口提供了一个干净的管道,你可以用usb_cdc.data.write()发送任意二进制数据,主机端用对应的COM端口接收,完全不受REPL协议干扰。
如何在主机端区分这两个端口?当启用data端口后,电脑上会出现两个串口设备,名字可能很相似。这里推荐使用Adafruit提供的adafruit-board-toolkitPython库,它可以帮助你精准定位。
# 在电脑的终端/命令提示符中安装 pip3 install adafruit-board-toolkit# 在你的主机Python脚本中 import adafruit_board_toolkit.circuitpython_serial as cpy_serial # 获取所有连接到REPL的串口 repl_ports = cpy_serial.repl_comports() print(“REPL ports:”, [p.device for p in repl_ports]) # 获取所有数据串口 data_ports = cpy_serial.data_comports() print(“Data ports:”, [p.device for p in data_ports])3.3 管理MIDI设备
USB MIDI设备默认在大多数板子上是启用的。如果你用不到音乐功能,可以禁用它以节省USB资源。
import usb_midi usb_midi.disable()一个重要的硬件兼容性问题:在一些资源紧张的微控制器上(如STM32F4, ESP32-S2/S3),USB端点(后面会详细讲)数量有限,MIDI默认可能是禁用的。如果你想启用它,可能需要先禁用另一个设备来“腾地方”。
import usb_hid, usb_midi # 在ESP32-S2上,为了启用MIDI,我们可能需要牺牲HID功能 usb_hid.disable() # 禁用所有键盘、鼠标等HID设备 usb_midi.enable() # 现在可以启用MIDI了4. 深入HID设备:从使用到自定义
HID(人机接口设备)是USB世界里最有趣的部分之一。它让你的CircuitPython板子可以模拟成键盘、鼠标、游戏手柄,或者任何你想象得到的人机交互设备。
4.1 标准HID设备与选择启用
CircuitPython默认提供了三个HID设备:
usb_hid.Device.KEYBOARD: 标准键盘,包含数字锁定灯等状态指示。usb_hid.Device.MOUSE: 标准鼠标,支持最多5个按键和滚轮。usb_hid.Device.CONSUMER_CONTROL: 消费控制设备,用于多媒体键(播放/暂停、音量)、浏览器快捷键等。
你可以在boot.py中选择启用哪些:
import usb_hid # 启用全部三个默认设备(这也是默认行为,无需显式写出) usb_hid.enable( (usb_hid.Device.KEYBOARD, usb_hid.Device.MOUSE, usb_hid.Device.CONSUMER_CONTROL) ) # 只启用键盘(适合做宏键盘) usb_hid.enable((usb_hid.Device.KEYBOARD,)) # 注意:单个设备也要放在元组里 # 完全禁用所有HID设备 usb_hid.disable() # 或者 usb_hid.enable(()) # 启用一个空元组关于CONSUMER_CONTROL的实用技巧:很多人不知道键盘上的音量加减、播放暂停这些键并不是通过普通的键盘键值发送的。它们走的是独立的Consumer Control通道。在adafruit_hid库中,你可以找到ConsumerControlCode类来使用这些功能。这样做的好处是,这些控制键是系统全局的,不会干扰你正在输入文本的应用程序。
4.2 创建自定义HID设备
当标准设备不能满足需求时,你就需要自定义HID设备了。这需要你提供两样东西:
- HID报告描述符(Report Descriptor):一个用字节数组定义的、描述设备功能和数据格式的“蓝图”。
- 一个驱动该设备的CircuitPython类:用于生成符合描述符定义的数据报告。
编写报告描述符是一门专业学问,涉及对USB HID规范的深入理解。但对于我们大多数人来说,更实用的方法是“借鉴”和“修改”。网络上有很多现成的描述符,比如来自开源游戏手柄、绘图板的,你可以直接拿来用。
下面是一个自定义游戏手柄的描述符示例(摘自原文,但增加了详细注释):
import usb_hid # 这是一个支持16个按钮和4个模拟轴(X,Y,Z,Rz)的游戏手柄报告描述符 # 描述符是一个字节数组,每个字节或每对字节都有特定含义 GAMEPAD_REPORT_DESCRIPTOR = bytes(( 0x05, 0x01, # 用法页(Generic Desktop) - 声明这是一个通用桌面控制设备 0x09, 0x05, # 用法(Game Pad) - 进一步声明为游戏手柄 0xA1, 0x01, # 集合(Application)开始 - 定义一个应用集合 # 以下是集合内的内容 0x85, 0x04, # 报告ID (4) - 这个报告的ID是4,用于复合设备中区分不同设备 0x05, 0x09, # 用法页(Button) - 这部分描述按钮 0x19, 0x01, # 用法最小值(Button 1) 0x29, 0x10, # 用法最大值(Button 16) - 共16个按钮 0x15, 0x00, # 逻辑最小值(0) - 按钮松开状态 0x25, 0x01, # 逻辑最大值(1) - 按钮按下状态 0x75, 0x01, # 报告大小(1) - 每个按钮用1个比特表示 0x95, 0x10, # 报告数量(16) - 总共16个这样的比特 0x81, 0x02, # 输入(Data, Var, Abs) - 这些是主机从设备读取的数据 0x05, 0x01, # 用法页(Generic Desktop) - 切换回通用桌面,描述模拟轴 0x15, 0x81, # 逻辑最小值(-127) - 模拟轴的最小值 0x25, 0x7F, # 逻辑最大值(127) - 模拟轴的最大值 0x09, 0x30, # 用法(X) - X轴 0x09, 0x31, # 用法(Y) - Y轴 0x09, 0x32, # 用法(Z) - Z轴(通常作为第三个轴) 0x09, 0x35, # 用法(Rz) - Rz轴(绕Z轴旋转,常作为第四个轴) 0x75, 0x08, # 报告大小(8) - 每个轴用1个字节(8比特)表示 0x95, 0x04, # 报告数量(4) - 总共4个轴 0x81, 0x02, # 输入(Data, Var, Abs) - 这些也是输入数据 0xC0, # 集合结束 )) # 使用上面的描述符创建一个自定义HID设备对象 gamepad = usb_hid.Device( report_descriptor=GAMEPAD_REPORT_DESCRIPTOR, usage_page=0x01, # 用法页:通用桌面控制 (Generic Desktop) usage=0x05, # 用法:游戏手柄 (Game Pad) report_ids=(4,), # 报告ID元组,与描述符中的0x85, 0x04对应 in_report_lengths=(6,), # 输入报告长度:16个按钮(2字节)+ 4个轴(4字节)= 6字节 out_report_lengths=(0,), # 输出报告长度:0字节(这个手柄不从主机接收数据) ) # 启用标准设备加上我们的自定义游戏手柄 usb_hid.enable( (usb_hid.Device.KEYBOARD, usb_hid.Device.MOUSE, usb_hid.Device.CONSUMER_CONTROL, gamepad) # 将自定义设备加入元组 )关键参数解析:
report_ids: 在复合HID设备中,每个子设备需要用唯一的报告ID来区分。这里设为(4,)。in_report_lengths: 指定发送给主机的报告(Input Report)的长度。这里一个报告是6字节。out_report_lengths: 指定从主机接收的报告(Output Report)的长度。对于只发送不接收的设备(如简单手柄),设为(0,)。
创建好设备对象后,你还需要编写相应的驱动代码来组装和发送报告。这通常需要你创建一个类,根据按钮和摇杆的状态,构造一个6字节的数组,并通过usb_hid.devices找到你的设备进行发送。这部分代码相对复杂,但adafruit_hid库中的现有设备类(如Keyboard,Mouse)是极好的参考。
4.3 复合HID设备与端点管理
当你像上面那样启用多个HID设备时,CircuitPython会自动将它们合并成一个复合HID设备。这意味着从主机的角度看,只连接了一个USB HID设备,但这个设备内部包含了键盘、鼠标、自定义手柄等多个功能。它们共享一对USB端点(一个IN,一个OUT),依靠不同的报告ID来区分彼此的数据包。
什么是端点?你可以把端点想象成USB设备上的“专用车道”。每条车道(端点对)负责运输一种特定类型的数据。HID设备共用一条车道,MIDI用另一条,每个CDC串口各用两条车道,CIRCUITPY存储设备也用一条。微控制器芯片的USB硬件所能提供的“车道”总数是有限的,这就是硬件端点限制。
下表列出了常见微控制器的端点对数量(不含控制端点0):
| 微控制器系列 | 可用端点对数量 | 典型代表芯片 |
|---|---|---|
| SAMD21 (M0) | 7 | Arduino Zero, Feather M0 |
| SAMD51 (M4) | 7 | Metro M4, Feather M4 |
| nRF52840 | 7 | Circuit Playground Bluefruit, Clue |
| RP2040 | 7 | Raspberry Pi Pico, Feather RP2040 |
| STM32F4 | 3 | 某些STM32F4开发板 |
| ESP32-S2/S3 | 4 (有效) | ESP32-S2/S3开发板 |
| Spresense | 6 (固定分配) | Sony Spresense |
端点需求计算:
CIRCUITPY(MSC): 需要1对端点。MIDI: 需要1对端点。CDC(串口):每个CDC设备需要2对端点(一对控制,一对数据)。如果同时启用console和data,则需要4对。HID(复合): 所有HID设备共享1对端点。
一个典型的“满配”场景(SAMD51开发板):CIRCUITPY(1) +MIDI(1) +CDC console&data(4) +HID composite(1) = 7对端点。刚好用完所有资源。
一个受限制的场景(ESP32-S3): 它只有4对可用端点。如果你想同时使用CDC console和data(4对),那么CIRCUITPY、MIDI和HID就都无法启用了。你必须做出取舍,例如只启用console(2对),这样还能再启用CIRCUITPY(1对)和HID(1对)。
硬件选型建议:如果你的项目需要丰富的USB功能(比如同时需要存储、双串口和复杂的HID),优先选择基于SAMD51、nRF52840或RP2040的板子,它们提供了最宽松的端点资源。ESP32-S2/S3在无线功能上强大,但在复杂USB应用上限制较大。
5. 高级主题与疑难排解
掌握了基本配置和自定义后,我们来看看一些高级用法和开发中必然会踩的坑。
5.1 Boot Keyboard/Mouse模式
USB HID协议中有一个“Boot Protocol”子类。这是为了在计算机启动的最早期阶段(比如在BIOS或引导加载程序界面),操作系统还没有加载完整驱动时,能有一个绝对标准的、极简的键盘或鼠标可以使用。CircuitPython的HID设备可以被标记为支持这种模式。
import usb_hid # 创建一个支持Boot Protocol的键盘设备 boot_keyboard = usb_hid.Device( # ... 其他参数 ... usage_page=0x01, # Generic Desktop usage=0x06, # Keyboard # 关键:报告描述符必须符合Boot Protocol规范 # 通常可以直接使用库内置的,这里仅为示意 ) # 在enable时,这个设备会被特殊对待但是,请谨慎使用!根据社区反馈和原文提示,Boot设备在某些主机上(特别是macOS)可能存在兼容性问题,可能导致设备无法被正常识别。除非你的设备明确需要在BIOS环境下工作(比如一个KVM切换器),否则建议使用标准的HID协议。
5.2 Windows HID设备清理难题
在Windows上开发自定义HID设备是一场“持久战”。Windows系统会对连接过的HID设备缓存其报告描述符等信息。如果你在开发过程中修改了描述符(比如从16键手柄改成18键),Windows可能会固执地使用旧的缓存信息,导致你的新设备无法正常工作。
症状:修改了boot.py中的报告描述符并复位后,设备管理器里显示设备有叹号,或者你的应用程序读不到正确的数据。
解决方案:
- 拔掉你的CircuitPython设备。
- 下载并运行USB Device Cleanup Tool(由Uwe Sieber开发)。这是一个轻量级工具,可以列出并删除所有已断开连接的USB设备记录。
- 在工具列表中,找到与你设备相关的陈旧条目(可能显示为未知设备或带有错误描述),将其删除。
- 重新插上你的设备,让Windows重新安装驱动并缓存新的描述符。
这个过程在迭代开发自定义HID描述符时可能会重复很多次,养成每次大改描述符后都清理一下的习惯,能节省大量调试时间。
5.3 Linux下的特殊问题
原文提到了一个Linux特有的问题:如果你只启用一个自定义的游戏手柄设备(并且没有其他标准HID设备),在某些Linux发行版上可能会导致USB枚举失败,出现“USB busy”错误。
根本原因:Linux的HID驱动对某些单一功能的HID设备处理逻辑可能与Windows/macOS不同。
解决方案(Workaround):很简单,在启用你的自定义设备时,至少再附带一个标准的HID设备,比如鼠标。
# 在Linux上,不要只启用一个自定义游戏手柄 # usb_hid.enable((gamepad,)) # 这可能在某些Linux系统上失败 # 正确的做法:附带一个标准设备 usb_hid.enable((gamepad, usb_hid.Device.MOUSE)) # 启用游戏手柄和鼠标这样就能保证复合HID设备被正确识别。在你的应用代码中,你只需要使用游戏手柄部分,忽略鼠标即可。
5.4 错误排查与安全模式
当你的boot.py配置要求了超过硬件支持的端点数量时,CircuitPython会在启动时进入安全模式。
现象:板子上的LED可能呈现特定颜色闪烁模式(如黄色),CIRCUITPY盘符可能出现也可能不出现,code.py根本不会运行。
如何诊断:连接串口终端(REPL),你会看到类似这样的错误信息:
Auto-reload is on. Simply save files over USB to run them or enter REPL to disable. Code done running. Waiting for reload. Press any key to enter the REPL. Use CTRL-D to reload. Safe mode: USB devices need more endpoints than are available.这明确告诉你,USB设备需要的端点超过了可用数量。你需要返回boot.py,精简你的配置,例如禁用MIDI或CIRCUITPY,或者将两个CDC串口减少为一个。
另一个常见错误来源是boot.py本身的语法或运行时错误。这些错误不会显示在串口终端(因为USB还没初始化),而是被记录在CIRCUITPY根目录下的boot_out.txt文件中。每次硬复位后,这个文件都会被覆盖。所以,如果你的设备行为异常,第一件事就是检查这个文件。
6. 完整实战:构建一个“隐身”的宏键盘
让我们把所有知识串联起来,完成一个实战项目:一个基于CircuitPython的宏键盘,它在正常工作时完全“隐身”(不显示CIRCUITPY和REPL),只有按住一个特定按钮启动时,才会进入配置模式。
硬件:Adafruit KB2040(RP2040芯片,自带按键矩阵支持)或任何带有足够GPIO和按钮的板子。目标:
- 默认状态下,设备模拟一个标准键盘和消费控制设备。
CIRCUITPY和REPL串口默认禁用,使设备在电脑上只显示为一个键盘。- 通过按住BOOT按钮(或自定义按钮)上电,可以进入配置模式,此时
CIRCUITPY和REPL启用,方便更新脚本。 - 利用消费控制键实现多媒体功能。
boot.py配置:
import board import digitalio import storage import usb_cdc import usb_hid import usb_midi # --- 硬件初始化 --- # 使用板载的BOOT按钮(在KB2040上,通常是GP7,按下为低电平) config_button = digitalio.DigitalInOut(board.BOOT) # 请根据你的板子修改引脚 config_button.switch_to_input(pull=digitalio.Pull.UP) # 启用内部上拉电阻 # --- USB设备配置逻辑 --- # 检测按钮是否被按下 button_pressed = not config_button.value # 按下时,value为False if button_pressed: # **配置模式**:按钮按下,启用所有开发接口 print(“Boot: Configuration mode enabled. USB drive and REPL ON.”) # storage 和 usb_cdc 默认就是启用的,我们无需额外操作 # 但为了清晰,可以显式启用(虽然默认就是True) # storage.enable_usb_drive() # 默认已启用 # usb_cdc.enable(console=True, data=False) # 默认已启用console else: # **正常运行模式**:按钮未按下,隐藏开发接口,只保留HID print(“Boot: Normal operation mode. HID only.”) storage.disable_usb_drive() usb_cdc.disable() # 禁用REPL串口 # --- HID设备配置(两种模式都需要)--- # 我们始终启用键盘和消费控制设备,禁用鼠标(因为我们用不到) # 这样可以节省一点点资源,并避免某些电脑因检测到鼠标而禁用触摸板 usb_hid.enable( (usb_hid.Device.KEYBOARD, usb_hid.Device.CONSUMER_CONTROL,) # 注意:我们移除了 MOUSE ) # --- MIDI设备配置(我们不需要,禁用)--- usb_midi.disable() # 打印最终配置状态(信息会写入boot_out.txt) print(“Boot: HID devices configured.”) print(“Boot: Configuration button state:”, button_pressed)code.py主程序示例:
import time import board import keypad import usb_hid from adafruit_hid.keyboard import Keyboard from adafruit_hid.keycode import Keycode from adafruit_hid.consumer_control import ConsumerControl from adafruit_hid.consumer_control_code import ConsumerControlCode # 初始化键盘和消费控制对象 kbd = Keyboard(usb_hid.devices) cc = ConsumerControl(usb_hid.devices) # 假设我们连接了一个2x2的按键矩阵,引脚为ROW1, ROW2, COL1, COL2 # 请根据实际硬件连接修改 rows = [board.GP0, board.GP1] cols = [board.GP2, board.GP3] keys = keypad.KeyMatrix(rows, cols, value_when_pressed=False) # 定义每个按键的功能 (0,0), (0,1), (1,0), (1,1) key_actions = [ (Keycode.CONTROL, Keycode.C), # 复制 (Keycode.CONTROL, Keycode.V), # 粘贴 ConsumerControlCode.VOLUME_DECREMENT, # 音量减 ConsumerControlCode.PLAY_PAUSE, # 播放/暂停 ] print(“Macro Pad Started!”) # 这个print在正常模式下看不到,因为REPL被禁用了 while True: event = keys.events.get() if event: key_index = event.key_number if event.pressed: action = key_actions[key_index] if isinstance(action, tuple): # 键盘组合键 kbd.press(*action) else: # 消费控制键 cc.send(action) else: # 按键释放 action = key_actions[key_index] if isinstance(action, tuple): kbd.release(*action) # 消费控制键无需释放动作 time.sleep(0.01)项目总结与要点:
- 双重模式:通过
boot.py中的按钮检测,实现了产品模式与开发模式的无缝切换,这是产品化项目的必备设计。 - 资源优化:禁用了不需要的鼠标和MIDI设备,为未来可能的功能扩展留出了端点资源。
- 功能实现:在
code.py中,结合adafruit_hid库,轻松实现了键盘宏和多媒体控制功能。注意消费控制键的使用方式与普通键盘键不同。 - 调试:在正常模式下,所有
print()输出都不可见。调试时,需要按住按钮进入配置模式,或者通过额外的硬件(如LED)来指示状态。
通过这个完整的流程,你可以将一个通用的CircuitPython开发板,转变为一个行为可控、用户体验专业的USB外设。从基础的设备显隐控制,到复杂的自定义HID设备开发,再到跨平台的疑难排解,这套组合拳足以应对大多数基于USB的嵌入式Python项目。记住,关键始终在于理解USB资源的有限性,并在boot.py这个唯一的配置窗口内做好精细的规划和容错设计。