MicroPython machine.USBDevice 详解:用 Python 实现自定义 USB 设备
【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython
导读
machine.USBDevice是 MicroPython 提供的底层 USB 设备驱动 API,允许你在固件启动后直接用 Python 代码定义 USB 设备描述符、配置描述符与各类传输回调,从而实现自定义的 USB 设备功能(例如自研协议的回显设备、USB DFU 固件升级设备等)。本文以官方文档 docs/library/machine.USBDevice.rst 为骨架,结合仓库中 extmod/machine_usb_device.c 的源码实现与 examples/usb 下的完整示例,系统讲解该 API 的术语体系、生命周期管理、全部方法/属性/常量的语义与回调时序,帮助你掌握从描述符设计到回调编程的完整技能。
适用范围与定位
可用平台与前提
machine.USBDevice的可用性为ESP32、RP2、SAMD三个平台。文档特别强调:
- 必须支持原生 USB(Native USB),并非所有开发板都支持原生 USB,硬件不支持时该模块不可用。
- 这是一个底层 API,假定使用者对 USB 标准(描述符、控制传输、端点、STALL 等)有基本了解。
- 如果需要更简单、内置功能更丰富的方案,官方建议使用 micropython-lib 中提供的高层 usb 驱动模块(面向 CDC、MSC、HID 等常见用途);
machine.USBDevice则面向实现自定义 USB 协议的场景。
与 TinyUSB 的关系(源码佐证)
从源码看,该模块位于 extmod/machine_usb_device.c,其文件头部注释明确说明"此实现直接引用 TinyUSB"(#include "shared/tinyusb/mp_usbd.h")。它由MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE编译开关控制,Python 层的config()、submit_xfer()、stall()、active()等方法最终都会映射到 TinyUSB 的usbd_*系列 C API(如usbd_edpt_xfer、usbd_edpt_stall、usbd_edpt_claim等)。理解这一层关系有助于排查底层时序问题。
核心术语:Runtime 与 Built-in 两种 USB 设备
文档用两个术语区分 USB 设备接口的两种形态:
- Runtime(运行时)USB 设备接口/驱动:通过本 Python API 在 MicroPython 启动后动态定义,是本文的主角。
- Built-in(内置)USB 设备接口/驱动:编译进固件的、始终可用的设备接口,例如默认开启的 USB-CDC(串口)、某些端口可选的 USB-MSC(大容量存储)。
两者可以共存于同一个 USB 设备上,这也是生命周期管理复杂性的根源——详见下一节。
生命周期管理:软复位、boot.py 与调试配置
软复位会清除所有 Runtime 接口
管理 runtime USB 接口的难点在于:如果你正通过内置 USB-CDC 串口与 MicroPython 通信,而这个 CDC 与 runtime 设备属于同一个 USB 设备,那么:
- 一次 MicroPython软复位(soft reset)会清除所有 runtime USB 接口,导致整个 USB 设备从主机断开。
- 如果固件同时提供内置 USB-CDC 串口,软复位后该串口会重新出现。
文档特别给出一个实用结论:一些针对 USB-CDC 串口的工具(如mpremote run)在 runtime USB 接口激活时会立即失败,因为mpremote触发软复位导致端口消失;第二次尝试会成功,因为软复位后 runtime 接口已不存在。相关复位机制可参阅 docs/reference/reset_boot.rst。
每次开机自动配置:写入 boot.py
若希望每次上电都自动配置 runtime USB 设备,官方建议把配置代码放入设备 VFS 上的boot.py文件(文件系统说明见 docs/reference/filesystem.rst)。每次复位时,boot.py 在 USB 子系统初始化之前(也在 main.py 之前)执行,因此可以让开发板一上电就立即呈现配置好的 runtime USB 设备。
开发调试:禁用内置 USB-CDC,改用硬件串口 REPL
开发或调试自定义 USB 设备时,内置 USB-CDC 串口的存在会干扰实验。文档给出的方案是:
- 连接一个硬件串口 REPL;
- 完全禁用内置 USB-CDC 串口。
但并非所有端口都支持(文档明确:当前仅rp2支持)。自定义构建时需配置:
#define MICROPY_HW_USB_CDC (0) #define MICROPY_HW_ENABLE_UART_REPL (1)即关闭内置 USB CDC,同时启用 UART REPL。这也是 extmod/machine_usb_device.c 中HAS_BUILTIN_DRIVERS (MICROPY_HW_USB_CDC || MICROPY_HW_USB_MSC)宏所反映的编译期开关体系——是否定义BUILTIN_DEFAULT等常量完全取决于这些编译宏。
构造函数与单例模式
machine.USBDevice()构造一个 USBDevice 对象。注意这是一个单例(singleton):每次调用构造函数都返回同一个对象引用。源码 extmod/machine_usb_device.c 中usb_device_make_new的实现证实了这一点——对象存放在根指针MP_STATE_VM(usbd)中,首次调用时初始化各回调字段为None、builtin_driver指向BUILTIN_NONE、active为False,之后调用直接返回已有对象。典型的用法是:
usbd = machine.USBDevice()USBDevice.config():核心配置方法
USBDevice.config(desc_dev, desc_cfg, desc_strs=None, open_itf_cb=None, reset_cb=None, control_xfer_cb=None, xfer_cb=None)这是配置整个 runtime USB 设备状态与回调的总入口。从源码 extmod/machine_usb_device.c 看,desc_dev、desc_cfg、desc_strs三个参数在 C 层是必填(MP_ARG_REQUIRED),且做了类型校验:desc_dev与desc_cfg必须是支持 buffer 协议的对象(bytes/bytearray等),否则抛ValueError;desc_strs若提供则必须支持下标访问(subscr协议),否则同样抛ValueError。其余回调参数默认None。
desc_dev:USB 设备描述符
一个 bytes 类对象,包含新的 USB设备描述符(device descriptor)。它定义了 VID/PID、USB 版本、设备类、bMaxPacketSize、bNumConfigurations等信息。设备描述符格式由 USB 标准规定,必须手工按字节构造。
desc_cfg:USB 配置描述符
一个 bytes 类对象,包含新的 USB配置描述符(configuration descriptor),其内部通常串接配置描述符、接口描述符、端点描述符等。同样需手工按字节构造。
desc_strs:USB 字符串描述符(可选)
可选对象,持有字符串或 bytes 值,用于提供 USB 字符串描述符。可以是 list、dict,或任何支持用整数下标(字符串描述符索引)访问的对象。要点:
- 字符串是 USB 的可选特性;若描述符中没有引用任何字符串、或只想用内置字符串,此参数可省略(默认)。
- 除索引 0 外,所有字符串值应为纯 ASCII。
- 索引 0 是特殊的"语言(languages)"描述符,用 bytes 对象表示,格式由 USB 标准自定义;若索引 0 返回
None,则使用默认的"English"语言描述符。 - 希望某个索引回退到内置字符串值时,下标查询可以返回
None、抛KeyError或抛IndexError。
源码校验仅要求desc_strs支持subscr协议,具体行为(返回None、抛异常回退等)由 Python 侧的对象实现决定。
open_itf_cb:接口打开回调
在主机发出Set Configuration 请求(USB 设备对主机可用的最后阶段)时,对每个接口描述符或IAD(Interface Association Descriptor,接口关联描述符)调用一次。回调接收一个参数:被主机接受的接口/IAD 描述符的memoryview(包含所有关联描述符)。
关键约束:
- 该 memoryview 是之前传入的
desc_cfg对象的视图; - 仅在回调函数返回前有效,不要在回调之外保存使用。
reset_cb:总线复位回调
当 USB 主机执行总线复位(bus reset)时调用,无参数。语义要点:
- 任何进行中的传输永远不会完成;
- 主机随后很可能会重新枚举设备:调用描述符相关回调,然后调用
open_itf_cb()。
control_xfer_cb:控制传输回调
每个 USB 控制传输(设备端点 0)会调用该回调一次或多次,接收两个参数。
第一个参数是控制传输阶段(stage):
| 值 | 阶段 | 说明 |
|---|---|---|
1 | SETUP | 解析 8 字节标准/类请求 |
2 | DATA | 读写附加数据 |
3 | ACK | 主机确认传输完成 |
第二个参数是用于读取该阶段 USB 控制请求数据的 memoryview,同样仅在回调返回前有效。三个阶段的 memoryview 数据内容相同(同一次传输)。
一次成功的传输 = 回调按 1→2→3 顺序被依次调用。文档给出的经验法则是:如果设备想对某个控制请求做出响应,最好等到 ACK 阶段再动手,以确认主机控制器已按预期完成传输。
返回值语义:
- 返回
False:对端点执行STALL,拒绝该传输,不再进入剩余阶段; - 返回
True:继续传输到下一阶段; - 返回 buffer 对象(仅限 SETUP 阶段):当传输需要额外收发数据时使用——典型场景是请求中
wLength字段非零。OUT 方向传输应返回可写buffer,IN 方向传输应返回带数据的可读buffer。
xfer_cb:非控制传输完成回调
每当通过submit_xfer()提交的非控制传输完成时调用,接收三个参数:
- 端点号(完成的传输所在端点);
- 结果值:整数,
0(XFER_SUCCESS)表示成功;非零值XFER_FAILED或XFER_STALLED表示失败; - 成功传输的字节数:短传输(short transfer)时结果为
XFER_SUCCESS但xferred_bytes小于提交 buffer 的长度。
注意:若发生总线复位(见reset相关说明),对尚未完成的传输不会调用xfer_cb。
active():设备激活与去激活
USBDevice.active([value] /)- 无参数调用:返回当前 runtime USB 设备的激活状态(布尔值)。"激活"表示设备对主机可用,并不意味着主机真的在线。
- 传入真值:激活 USB 设备;
- 传入假值:去激活。去激活期间主机检测不到该设备。
模拟断开再重连:调用active(False)后跟active(True)。当 runtime 设备配置发生变化后,通常需要这样让主机看到新设备。
源码 extmod/machine_usb_device.c 揭示了内部机制:激活状态变更并非立即生效,而是设置trigger标志并调用mp_usbd_schedule_task()调度 TinyUSB 任务处理;首次激活时会调用mp_usbd_init()确保 TinyUSB 已初始化。此外,若既未启用内置驱动又未调用过config()设置描述符,直接激活会抛OSError(MP_EINVAL)——必须先 config 或启用内置驱动,再激活。
builtin_driver 属性:与内置驱动的协同
usbd.builtin_driver = usbd.BUILTIN_NONE- 该属性保存当前内置驱动配置,必须赋值为
USBDevice.BUILTIN_系列具名常量之一; - 默认值为
USBDevice.BUILTIN_NONE; - 设置该字段时 runtime 设备必须处于非激活状态——若已激活需先
active(False),设置后再active(True)。源码 extmod/machine_usb_device.c 的usb_device_attr中,激活状态下写该属性会直接抛OSError(MP_EINVAL)。
当设置为非BUILTIN_NONE值时,config()的参数受以下限制:
desc_cfg应以builtin_driver.desc_cfg提供的内置接口描述符数据开头;追加在内置配置描述符之后的描述符,其接口号、字符串号、端点号必须从itf_max、str_max、ep_max(内置驱动的最大值)开始往后排;- 若在
desc_cfg末尾追加了新接口,还需同步更新内置配置描述符中的bNumInterfaces字段; desc_strs应为None,或是一个 list/dict 且其中小于builtin_driver.str_max的索引缺失或值为None——这些索引预留给内置驱动;若在预留索引处放入其他字符串,则会覆盖内置驱动的对应字符串。
remote_wakeup():远程唤醒
USBDevice.remote_wakeup()若设备处于挂起(suspend)模式且主机已启用REMOTE_WAKEUP特性,则唤醒主机。前提是该特性需在 USB 属性中启用,且主机端也支持。返回True表示远程唤醒已启用且成功唤醒主机。源码实现 extmod/machine_usb_device.c 直接包装了 TinyUSB 的tud_remote_wakeup()。
submit_xfer():提交非控制传输
USBDevice.submit_xfer(ep, buffer /)在端点号ep上提交一次 USB 传输:
buffer必须是实现 buffer 接口的对象:IN 端点需要读访问,OUT 端点需要写访问;ep不能是控制端点 0——控制传输是通过control_xfer_cb的多次调用构建的;- 返回
True表示提交成功;返回False表示无法排队(设备未被主机配置,或该端点已有传输在排队); - 传输完成后调用
xfer_cb; - 若 USB 设备未激活,抛
OSError(原因MP_EINVAL)。
源码 extmod/machine_usb_device.c 进一步说明:端点地址按 TinyUSB 约定包含方向位(IN端点地址带0x80位),端点号 0 或超出CFG_TUD_ENDPPOINT_MAX会抛ValueError;端点被占用时抛OSError(MP_EBUSY);提交成功后,buffer 对象会被持有直到传输完成(防止被 GC 回收)。
stall():端点 STALL 状态
USBDevice.stall(ep, [stall] /)- 读取或设置设备端点的STALL状态;
ep为端点号;- 若提供可选参数
stall(布尔值),则设置 STALL 状态; - 返回值是调用前该端点的当前 STALL 状态;
- 处于 STALL 的端点可能保持该状态直到再次调用本函数,也可能被 USB 主机自动清除;
- 若 USB 设备未激活,抛
OSError(原因MP_EINVAL)。
源码 extmod/machine_usb_device.c 显示其内部调用 TinyUSB 的usbd_edpt_stalled/usbd_edpt_stall/usbd_edpt_clear_stall。
常量
BUILTIN_* 常量:内置驱动描述
USBDevice.BUILTIN_NONE USBDevice.BUILTIN_DEFAULT USBDevice.BUILTIN_CDC USBDevice.BUILTIN_MSC USBDevice.BUILTIN_CDC_MSC这些常量对象持有编译进固件的内置描述符数据:
BUILTIN_NONE与BUILTIN_DEFAULT始终存在;其余常量是否出现取决于固件构建配置与实际内置驱动。- 文档特别说明:当前
BUILTIN_CDC、BUILTIN_MSC、BUILTIN_CDC_MSC中至多一个被定义,且与BUILTIN_DEFAULT是同一个对象。这些常量存在的意义是运行时检测内置驱动;未来可能支持在多个内置驱动配置间切换。 - 这些值用于读写
builtin_driver属性。
每个常量对象包含以下只读字段:
| 字段 | 含义 |
|---|---|
itf_max | 内置配置描述符中使用的最高bInterfaceNumber值 + 1 |
ep_max | 内置配置描述符中使用的最高bEndpointAddress值 + 1(不含IN标志位0x80) |
str_max | 任何内置描述符使用过的最高字符串描述符索引 + 1 |
desc_dev | 内置 USB 设备描述符(bytes) |
desc_cfg | 完整的内置 USB 配置描述符(bytes) |
源码 extmod/machine_usb_device.c 中,BUILTIN_DEFAULT的字典表正是由USBD_ITF_BUILTIN_MAX、USBD_EP_BUILTIN_MAX、USBD_STR_BUILTIN_MAX等编译期宏和mp_usbd_builtin_desc_cfg数据构造;BUILTIN_NONE则固定为itf_max=0、ep_max=0、str_max=1、desc_cfg为空 bytes。
XFER_* 常量:传输结果
USBDevice.XFER_SUCCESS USBDevice.XFER_FAILED USBDevice.XFER_STALLED传给xfer_cb的传输结果值:
XFER_SUCCESS=0:传输成功;XFER_FAILED:因底层完整性错误而失败;XFER_STALLED:主机对该端点执行了 STALL。
所有失败值均为非零整数。源码注释 extmod/machine_usb_device.c 说明这些值取自 TinyUSB 的tusb_xfer_result_t子集,XFER_RESULT_TIMEOUT与XFER_RESULT_INVALID未暴露(前者仅出现在同步 API 子集及 samd 主机控制器的一种情况,后者只出现在主机控制器 API 中)。
实战示例一:Python 实现的 USB 回显设备
仓库 examples/usb/usb_simple_device.py 实现了一个非常简单的自定义 USB 设备:一个 OUT 端点 + 一个 IN 端点,接收主机发来的最多 64 字节数据并原样回显。这是理解整套 API 的最佳起点。
描述符与字符串
设备描述符用bytes手工构造(VID=0xF055、PID=0x9999,bDeviceClass=0xFF即厂商自定义类),配置描述符内含 1 个接口、2 个端点(IN 端点 0x81 为中断端点、OUT 端点 0x01 为批量端点),字符串描述符用字典按下标提供:
_desc_strs = { 0x11: b"iManufacturer", 0x12: b"iProduct", 0x13: b"iSerial", ... }回调逻辑
open_itf_cb在接口被主机打开时先提交一个 OUT 传输准备接收首个数据包;xfer_cb根据端点号分流:OUT 完成则打印收到的数据并通过 IN 端点回显(用memoryview(usbd_buf)[:xferred_bytes]精确截取实际收到的字节数),IN 完成则再次提交 OUT 传输等待下一包:
def _xfer_cb(ep_addr, result, xferred_bytes): if ep_addr == EP_OUT: print(usbd_buf) usbd.submit_xfer(EP_IN, memoryview(usbd_buf)[:xferred_bytes]) elif ep_addr == EP_IN: usbd.submit_xfer(EP_OUT, usbd_buf)装配与激活
usbd = machine.USBDevice() usbd.builtin_driver = usbd.BUILTIN_NONE usbd.config( desc_dev=_desc_dev, desc_cfg=_desc_cfg, desc_strs=_desc_strs, open_itf_cb=_open_itf_cb, xfer_cb=_xfer_cb, ) usbd.active(1)由于该示例会让设备切换成自定义 USB 模式(内置 CDC 串口消失),示例注释建议用mpremote运行且不等待响应:
$ mpremote run --no-follow usb_simple_device.py主机端程序(usb_simple_host_pyusb.py)需要pip install pyusb,且通常需要sudo访问自定义 USB 设备。运行结束后需复位或拔插 USB 以停止设备。mpremote工具本身位于仓库 tools/mpremote 目录。
实战示例二:Python 实现的 USB DFU 设备
examples/usb/usb_dfu_device.py 用 Python 完整实现了 USBDevice Firmware Update(DFU)协议,展示了control_xfer_cb处理类请求的典型模式。其描述符使用 ST 的 VID/PID(0x0483 / 0xDF11),配置描述符中包含 DFU 功能描述符(bDescriptorType=0x21,声明支持 detach、upload、download,wTransferSize=2048)。
核心的_control_xfer_cb用struct.unpack("<BBHHH", request)解析 8 字节的标准 USB 控制请求(bmRequestType, bRequest, wValue, wIndex, wLength),并根据阶段号处理:
- SETUP 阶段:根据
bmRequestType判断方向,OUT 方向返回可写 buffer 准备接收数据,IN 方向准备发送数据; - DATA / ACK 阶段:推进 DFU 状态机(如
DNLOAD、UPLOAD、GETSTATUS等 DFU 类请求的处理)。
运行方式与回显示例类似:
$ mpremote run --no-follow usb_dfu_device.py随后可用仓库中的 tools/pydfu.py 与 DFU 设备交互:
$ ../../tools/pydfu.py -l # 列出 DFU 设备 $ ../../tools/pydfu.py -u <file.dfu> # 下载固件到设备固件写入完成后,内置 USB-CDC 与 REPL 会重新出现。更多示例说明见 examples/usb/README.md。
实战要点与常见陷阱
综合文档与源码,编写 runtime USB 设备时需注意:
- 先配置、后激活:
config()未设置描述符且未启用内置驱动时直接active(True)会抛OSError;激活状态变更由调度任务异步处理,不要假设立即生效。 - 注意 memoryview 生命周期:
open_itf_cb与control_xfer_cb收到的 memoryview 仅在回调返回前有效,需要持久化数据必须自行拷贝。 - 控制传输三阶段:按 SETUP→DATA→ACK 顺序推进;需要响应控制请求时建议等 ACK 阶段确认;
wLength非零时在 SETUP 阶段返回 buffer 以承载附加数据。 - STALL 是拒绝控制请求的正确姿势:
control_xfer_cb返回False即对端点执行 STALL,拒绝不支持的请求。 - 总线复位后传输作废:
reset_cb被调用意味着进行中传输永不完成,也不会触发xfer_cb,需要重新枚举/重建传输状态。 - 与内置驱动共存:启用内置驱动时,新接口的编号要从
builtin_driver.itf_max/ep_max/str_max之后排起,并记得更新bNumInterfaces。 - 激活状态的修改:修改
builtin_driver前必须先active(False),否则抛OSError;配置变化后可用active(False)+active(True)模拟拔插让主机重新枚举。 - 软复位会清空一切 runtime 配置:若需开机自动生效,请把配置放在 boot.py;使用
mpremote run等触发软复位的工具时,第一次调用可能失败,第二次会成功。
通过本 API,你可以在不重新编译固件的情况下,用纯 Python 为 ESP32、RP2、SAMD 平台实现任意符合 USB 标准的自定义设备角色;对于更常规的 CDC/MSC/HID 需求,则优先考虑 micropython-lib 中的高层 usb 驱动模块。
【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考