news 2026/9/20 9:58:14

MicroPython machine.USBDevice 详解:用 Python 实现自定义 USB 设备

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MicroPython machine.USBDevice 详解:用 Python 实现自定义 USB 设备

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_xferusbd_edpt_stallusbd_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)中,首次调用时初始化各回调字段为Nonebuiltin_driver指向BUILTIN_NONEactiveFalse,之后调用直接返回已有对象。典型的用法是:

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_devdesc_cfgdesc_strs三个参数在 C 层是必填MP_ARG_REQUIRED),且做了类型校验:desc_devdesc_cfg必须是支持 buffer 协议的对象(bytes/bytearray等),否则抛ValueErrordesc_strs若提供则必须支持下标访问(subscr协议),否则同样抛ValueError。其余回调参数默认None

desc_dev:USB 设备描述符

一个 bytes 类对象,包含新的 USB设备描述符(device descriptor)。它定义了 VID/PID、USB 版本、设备类、bMaxPacketSizebNumConfigurations等信息。设备描述符格式由 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)

阶段说明
1SETUP解析 8 字节标准/类请求
2DATA读写附加数据
3ACK主机确认传输完成

第二个参数是用于读取该阶段 USB 控制请求数据的 memoryview,同样仅在回调返回前有效。三个阶段的 memoryview 数据内容相同(同一次传输)。

一次成功的传输 = 回调按 1→2→3 顺序被依次调用。文档给出的经验法则是:如果设备想对某个控制请求做出响应,最好等到 ACK 阶段再动手,以确认主机控制器已按预期完成传输。

返回值语义

  • 返回False:对端点执行STALL,拒绝该传输,不再进入剩余阶段;
  • 返回True:继续传输到下一阶段;
  • 返回 buffer 对象(仅限 SETUP 阶段):当传输需要额外收发数据时使用——典型场景是请求中wLength字段非零。OUT 方向传输应返回可写buffer,IN 方向传输应返回带数据的可读buffer。

xfer_cb:非控制传输完成回调

每当通过submit_xfer()提交的非控制传输完成时调用,接收三个参数:

  1. 端点号(完成的传输所在端点);
  2. 结果值:整数,0XFER_SUCCESS)表示成功;非零值XFER_FAILEDXFER_STALLED表示失败;
  3. 成功传输的字节数:短传输(short transfer)时结果为XFER_SUCCESSxferred_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_maxstr_maxep_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_NONEBUILTIN_DEFAULT始终存在;其余常量是否出现取决于固件构建配置与实际内置驱动。
  • 文档特别说明:当前BUILTIN_CDCBUILTIN_MSCBUILTIN_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_MAXUSBD_EP_BUILTIN_MAXUSBD_STR_BUILTIN_MAX等编译期宏和mp_usbd_builtin_desc_cfg数据构造;BUILTIN_NONE则固定为itf_max=0ep_max=0str_max=1desc_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_TIMEOUTXFER_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_cbstruct.unpack("<BBHHH", request)解析 8 字节的标准 USB 控制请求(bmRequestType, bRequest, wValue, wIndex, wLength),并根据阶段号处理:

  • SETUP 阶段:根据bmRequestType判断方向,OUT 方向返回可写 buffer 准备接收数据,IN 方向准备发送数据;
  • DATA / ACK 阶段:推进 DFU 状态机(如DNLOADUPLOADGETSTATUS等 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 设备时需注意:

  1. 先配置、后激活config()未设置描述符且未启用内置驱动时直接active(True)会抛OSError;激活状态变更由调度任务异步处理,不要假设立即生效。
  2. 注意 memoryview 生命周期open_itf_cbcontrol_xfer_cb收到的 memoryview 仅在回调返回前有效,需要持久化数据必须自行拷贝。
  3. 控制传输三阶段:按 SETUP→DATA→ACK 顺序推进;需要响应控制请求时建议等 ACK 阶段确认;wLength非零时在 SETUP 阶段返回 buffer 以承载附加数据。
  4. STALL 是拒绝控制请求的正确姿势control_xfer_cb返回False即对端点执行 STALL,拒绝不支持的请求。
  5. 总线复位后传输作废reset_cb被调用意味着进行中传输永不完成,也不会触发xfer_cb,需要重新枚举/重建传输状态。
  6. 与内置驱动共存:启用内置驱动时,新接口的编号要从builtin_driver.itf_max/ep_max/str_max之后排起,并记得更新bNumInterfaces
  7. 激活状态的修改:修改builtin_driver前必须先active(False),否则抛OSError;配置变化后可用active(False)+active(True)模拟拔插让主机重新枚举。
  8. 软复位会清空一切 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 9:56:20

Qwen3 进了 Artificial Analysis 收录页:用 TaoToken 复现同款模型 ID

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 9:56:10

零基础学ESP32:SD卡读写与数据存储完全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 9:52:26

2026压测工具测评盘点:13款主流开源与商业工具选型指南

2026年了&#xff0c;性能测试这个活儿在测试工程师的日常里占的权重越来越高&#xff0c;但每次聊到压测工具&#xff0c;总有人问“到底用哪款好”。市面上的压测工具少说有几十款&#xff0c;能经得住生产环境检验、团队愿意长期用的其实就那么十来个。这篇盘点不打算把13款…

作者头像 李华
网站建设 2026/9/20 9:52:19

B站弹幕抓取与词云生成全链路解析:从cid获取到TF-IDF加权词云

简介&#xff1a;本资源是一套面向高校计算机专业学生的Python课程设计实践项目&#xff0c;聚焦B站视频弹幕与评论数据的采集、清洗及可视化分析&#xff0c;解决短视频平台用户行为文本挖掘的教学与实践需求。压缩包共12个文件&#xff0c;含4个核心Python脚本&#xff08;如…

作者头像 李华
网站建设 2026/9/20 9:49:50

从零吃透多层感知机MLP:原理、PyTorch实战与训练避坑指南

1. 为什么我劝你别跳过MLP直接上手Transformer这两年聊神经网络有个很有趣的现象&#xff1a;新人入门第一件事是跑通一个Transformer&#xff0c;好像不聊注意力机制就不算"搞深度学习的"。上一次出现这种风气是CNN火的时候&#xff0c;大家觉得会调几个卷积层就很厉…

作者头像 李华
网站建设 2026/9/20 9:48:59

从免费CRM到私有化部署:DeskcommCRM落地实践与避坑指南

做销售管理这几年&#xff0c;我印象最深的一个词是“客户不在系统里&#xff0c;就在抽屉里”。我们团队从七八个人扩张到二十多人&#xff0c;客户信息却还停留在Excel、微信群和个人通讯录里。每周五大家交周报&#xff0c;我经常看到同一个客户被两个人跟进&#xff0c;报价…

作者头像 李华