
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三个平台。文档特别强调必须支持原生 USBNative 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_strsNone, open_itf_cbNone, reset_cbNone, control_xfer_cbNone, xfer_cbNone)这是配置整个 runtime USB 设备状态与回调的总入口。从源码 extmod/machine_usb_device.c 看desc_dev、desc_cfg、desc_strs三个参数在 C 层是必填MP_ARG_REQUIRED且做了类型校验desc_dev与desc_cfg必须是支持 buffer 协议的对象bytes/bytearray等否则抛ValueErrordesc_strs若提供则必须支持下标访问subscr协议否则同样抛ValueError。其余回调参数默认None。desc_devUSB 设备描述符一个 bytes 类对象包含新的 USB设备描述符device descriptor。它定义了 VID/PID、USB 版本、设备类、bMaxPacketSize、bNumConfigurations等信息。设备描述符格式由 USB 标准规定必须手工按字节构造。desc_cfgUSB 配置描述符一个 bytes 类对象包含新的 USB配置描述符configuration descriptor其内部通常串接配置描述符、接口描述符、端点描述符等。同样需手工按字节构造。desc_strsUSB 字符串描述符可选可选对象持有字符串或 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 设备对主机可用的最后阶段时对每个接口描述符或IADInterface 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 方向传输应返回可写bufferIN 方向传输应返回带数据的可读buffer。xfer_cb非控制传输完成回调每当通过submit_xfer()提交的非控制传输完成时调用接收三个参数端点号完成的传输所在端点结果值整数0XFER_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值 1ep_max内置配置描述符中使用的最高bEndpointAddress值 1不含IN标志位0x80str_max任何内置描述符使用过的最高字符串描述符索引 1desc_dev内置 USB 设备描述符bytesdesc_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_max0、ep_max0、str_max1、desc_cfg为空 bytes。XFER_* 常量传输结果USBDevice.XFER_SUCCESS USBDevice.XFER_FAILED USBDevice.XFER_STALLED传给xfer_cb的传输结果值XFER_SUCCESS0传输成功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手工构造VID0xF055、PID0x9999bDeviceClass0xFF即厂商自定义类配置描述符内含 1 个接口、2 个端点IN 端点 0x81 为中断端点、OUT 端点 0x01 为批量端点字符串描述符用字典按下标提供_desc_strs { 0x11: biManufacturer, 0x12: biProduct, 0x13: biSerial, ... }回调逻辑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 UpdateDFU协议展示了control_xfer_cb处理类请求的典型模式。其描述符使用 ST 的 VID/PID0x0483 / 0xDF11配置描述符中包含 DFU 功能描述符bDescriptorType0x21声明支持 detach、upload、downloadwTransferSize2048。核心的_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),仅供参考