MicroPython pyb.USB_HID 类全解析:在 STM32 上模拟 USB 鼠标与键盘
2026/9/20 18:38:40 网站建设 项目流程
  • 嵌入式
  • 语言运行时
  • 编程语言
  • 解释器
  • 编译器
  • 物联网
  • 系统编程

【免费下载链接】micropython

MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems

项目地址:https://gitcode.com/gh_mirrors/mi/micropython
点击查看免费下载

本篇技术指南以 MicroPython 官方文档 docs/library/pyb.USB_HID.rst 为主体,系统讲解pyb.USB_HID类的使用方法:如何通过pyb.usb_mode()使能 HID 接口、如何用send()recv()收发 HID 报告,并深入 STM32 移植层源码(ports/stm32/usb.c、ports/stm32/usbd_hid_interface.c)剖析其底层实现。读完本文,你将能够独立编写"板卡伪装成鼠标/键盘"的实战程序,理解 HID 报告描述符、报文格式与超时机制,并能基于仓库内的预置常量(pyb.hid_mousepyb.hid_keyboard)或自定义报告描述符完成任意 HID 设备的模拟。

一、pyb.USB_HID 是什么

USB_HID类是 MicroPython pyboard(STM32 移植版)提供的 USB Human Interface Device(人机接口设备,HID)类。它允许创建一个表示板卡 USB HID 接口的对象,用于将开发板模拟为鼠标、键盘等标准外设,从而向连接的主机(如 PC)发送输入事件或接收主机下发的报告。

import pyb hid = pyb.USB_HID() # 创建 USB_HID 对象

该类的核心特征可以概括为两点:

  • 它是"接口"而非"实体":按 ports/stm32/usb.c 中的设计哲学注释,USB 本身不是一个实体,真正可访问的是各个接口(VCP、MSC、HID)。pyb.USB_HID()只是取回该接口对应的对象。
  • 使用前必须先用pyb.usb_mode()打开 HID 接口:在 pyb.USB_HID.rst 中明确说明"Before you can use this class, you need to usepyb.usb_mode()to set the USB mode to include the HID interface"。在 usb.c 的make_new中甚至留有一行TODO raise exception if USB is not configured for HID注释,意味着当前版本在未启用 HID 时创建对象并不会主动报错,但收发操作将无法正常工作。

从源码看,pyb_usb_hid_obj是一个单例对象,其结构仅包含对象基类与指向usb_device_t的指针:

typedef struct _pyb_usb_hid_obj_t { mp_obj_base_t base; usb_device_t *usb_dev; } pyb_usb_hid_obj_t;

构造器pyb_usb_hid_make_new不接受任何参数(mp_arg_check_num(n_args, n_kw, 0, 0, false)),直接返回该单例。

二、前置条件:用 pyb.usb_mode() 使能 HID 接口

在使用USB_HID之前,必须先通过pyb.usb_mode()将板卡 USB 模式配置为包含 HID 的复合模式。该函数的完整签名与说明见 docs/library/pyb.rst:

pyb.usb_mode(modestr, port=-1, vid=0xf055, pid=-1, msc=(), hid=pyb.hid_mouse, high_speed=False)

2.1 modestr 可选模式

modestr含义
None禁用 USB
'VCP'仅 VCP(虚拟串口)接口
'MSC'仅 MSC(大容量存储设备)接口
'VCP+MSC'VCP 与 MSC
'VCP+HID'VCP 与 HID(人机接口设备)
'VCP+MSC+HID'VCP、MSC 与 HID(仅 PYBD 系列板卡可用)

为向后兼容,'CDC'等价于'VCP'(同理'CDC+MSC''CDC+HID')。该兼容表在 usb.c 的pyb_usb_mode_table中有完整实现,其中还包含MICROPY_HW_USB_CDC_NUM >= 2/3时支持的多 VCP 组合模式(如'2xVCP''3xVCP+MSC+HID'等,取决于具体板卡配置)。

2.2 其它参数

  • port:整数(0, 1, ...),选择板卡的多 USB 端口中的哪一个;-1表示使用默认或自动选择的端口。
  • vid / pid:自定义 VID(厂商 ID)与 PID(产品 ID);pid=-1时会根据modestr自动选择内置的 PID(源码中对应MICROPY_HW_USB_PID_*系列宏,如MICROPY_HW_USB_PID_CDC_HID)。
  • msc:仅 MSC 模式有效,指定要暴露的 SCSI LUN 列表,如msc=(pyb.Flash(), pyb.SDCard())
  • hid:仅 HID 模式有效,指定 HID 细节,为一个五元组(subclass, protocol, max packet length, polling interval, report descriptor)。默认值为适合 USB 鼠标的参数;仓库还预置了适合键盘的pyb.hid_keyboard常量。
  • high_speed:设为True时,若硬件支持则启用 USB HS(高速)模式。

2.3 预置 HID 常量:pyb.hid_mouse 与 pyb.hid_keyboard

在 usb.c 中,这两个常量被定义为由 5 个元素组成的 ROM 元组:

// 鼠标:(subclass, protocol, max_packet, polling_interval, report_desc) { MP_ROM_INT(1), // subclass: boot MP_ROM_INT(2), // protocol: mouse MP_ROM_INT(USBD_HID_MOUSE_MAX_PACKET), MP_ROM_INT(8), // polling interval: 8ms ... } // 键盘:(subclass, protocol, max_packet, polling_interval, report_desc) { MP_ROM_INT(1), // subclass: boot MP_ROM_INT(1), // protocol: keyboard MP_ROM_INT(USBD_HID_KEYBOARD_MAX_PACKET), MP_ROM_INT(8), // polling interval: 8ms ... }

两者均采用Boot subclass(子类 1)8ms 轮询间隔,区别仅在于协议号(鼠标 2、键盘 1)与各自的报告描述符、最大包长。hid_mousehid_keyboard在 modpyb.c 中被注册为pyb模块的属性。

2.4 实战:在 boot.py 中按需切换 USB 模式

仓库自带示例 examples/SDdatalogger/boot.py 演示了在boot.py中根据按键状态动态选择 USB 模式的典型写法:

if switch_value: pyb.usb_mode("VCP+MSC") pyb.main("cardreader.py") # 按下开关:读卡器模式 else: pyb.usb_mode("VCP+HID") pyb.main("datalogger.py") # 未按开关:HID 模式(配合 USB_HID 收发)

三、构造器

USB_HID()

创建一个新的 USB_HID 对象,无任何参数。如前所述,源码中它返回的是全局单例,与创建的次数无关;因此也可以安全地在多个地方重复调用。

import pyb hid = pyb.USB_HID() # 获得 HID 接口对象

四、发送数据:USB_HID.send(data)

USB_HID.send(data)

通过 USB HID 接口发送一个 HID 报告(report)。data可以是:

  • 整数构成的 tuple / list;
  • 或者bytearray(字节缓冲)。

4.1 源码中的参数处理与限制

在 usb.c 的pyb_usb_hid_send中,参数按以下规则处理:

  1. 首先尝试将参数当作可读缓冲(mp_get_buffer)处理——即bytearray等直接可用;
  2. 若失败,则按 tuple/list 解析(mp_obj_get_array),逐元素取整数值填入临时缓冲;
  3. 限制:tuple/list 形式的报告长度超过 8 字节时会抛出ValueError: tuple/list too large for HID report; use bytearray instead——因为临时缓冲只有byte temp_buf[8]。要发送更长的报告(如键盘的 8 字节报告以外的自定义报告),应改用bytearray
  4. 底层调用USBD_HID_SendReport完成发送,成功返回发送字节数,失败返回0

发送方法返回发送的字节数(失败时为 0),不会抛出异常。

4.2 鼠标报告格式(pyb.hid_mouse)

pyb.hid_mouse的完整报告描述符定义在 usbd_cdc_msc_hid.c。综合描述符可以确定鼠标报告为4 字节

字节含义
0按键位:bit0 左键、bit1 右键、bit2 中键(高 5 位为填充)
1X 轴相对位移(-127 ~ 127)
2Y 轴相对位移(-127 ~ 127)
3滚轮相对位移(-127 ~ 127)

对应描述符关键片段:Report Count(3), Report Size(1)的 3 个按钮位、Report Size(5)的填充位,以及Logical Minimum(-127) / Logical Maximum(127)Report Size(8), Report Count(3)的 X/Y/Wheel 三个相对位移字节。

import pyb hid = pyb.USB_HID() # 移动鼠标:向右 50、向下 30,无按键 hid.send((0, 50, 30, 0)) # 单击左键:按下再松开 hid.send((1, 0, 0, 0)) hid.send((0, 0, 0, 0)) # 滚动滚轮向上 hid.send((0, 0, 0, 1))

4.3 键盘报告格式(pyb.hid_keyboard)

键盘报告描述符见 usbd_cdc_msc_hid.c,报告为标准的8 字节Boot Keyboard 格式:

字节含义
0修饰键位:bit0 Ctrl、bit1 Shift、bit2 Alt、bit3 GUI(Win/Command)……
1保留字节(恒为 0)
2 ~ 76 个同时按下的按键键码(0 ~ 101,见描述符Logical Maximum(101)
import pyb, time hid = pyb.USB_HID() keyboard = pyb.hid_keyboard # 先用键盘常量配置 USB 模式(见下) # 按下并松开字母 'a'(键码 0x04) hid.send((0, 0, 0x04, 0, 0, 0, 0, 0)) time.sleep_ms(50) hid.send((0, 0, 0, 0, 0, 0, 0, 0)) # 按下 Ctrl+C(修饰键 bit0 + 键码 0x06) hid.send((0x01, 0, 0x06, 0, 0, 0, 0, 0)) time.sleep_ms(50) hid.send((0, 0, 0, 0, 0, 0, 0, 0))

注意:若要用键盘协议,需在usb_mode中传入hid=pyb.hid_keyboard,否则板卡上报的是鼠标协议,主机端会按鼠标报告解析这些字节。

五、接收数据:USB_HID.recv(data, *, timeout=5000)

USB_HID.recv(data, *, timeout=5000)

在总线上接收数据,data的两种形式对应两种行为:

  • data 为整数:表示要接收的字节数,函数会分配一个新的字节缓冲返回,内含实际接收到的字节;
  • data 为可变缓冲(如bytearray):接收到的字节被填入该缓冲,函数返回实际读入的字节数。

timeout是等待接收的超时时间,单位为毫秒,默认 5000ms。超时行为在源码中有明确定义:usbd_hid_rx在超时后返回-MP_ETIMEDOUT,而pyb_usb_hid_recv捕获到负返回值时不抛出异常,而是将结果置为0(usb.c)——即超时时整数形式返回空字节串、缓冲形式返回 0。

5.1 底层接收机制

接收路径的核心实现在 usbd_hid_interface.c:

  • usbd_hid_init:初始化report_in_len = USBD_HID_REPORT_INVALID(即(size_t)-1,表示"无待读报告"),并返回内部缓冲report_in_buf的地址,供底层 USB 驱动存放首个传入报告;
  • usbd_hid_receive:当中断端点收到主机下发的 IN 报告时,仅记录长度(report_in_len = len),立即安排下一次接收,直到用户读取当前报告;
  • usbd_hid_rx:在超时时间(timeout_ms)内轮询等待report_in_len变为有效值;一旦有数据,拷贝min(len, report_in_len)字节到用户缓冲,随后调用USBD_HID_ReceivePacket调度下一个报告的接收,并返回实际拷贝字节数。

这一设计意味着recv()一次调用对应读取一个完整 HID 报告(报告长度由最大包长HID_DATA_FS_MAX_PACKET_SIZE决定,见 usbd_hid_interface.h)。

5.2 接收示例

接收方向通常用于"板卡接收主机下发"的场景,例如键盘的 LED 输出报告、自定义 HID 设备的命令下发:

import pyb hid = pyb.USB_HID() # 方式一:按字节数接收,返回新缓冲 data = hid.recv(8, timeout=1000) # 方式二:接收进预分配缓冲,返回字节数 buf = bytearray(8) n = hid.recv(buf, timeout=1000) print(n, buf)

5.3 与 select/poll 配合

USB_HID对象实现了流式接口的ioctl(usb.c),支持MP_STREAM_POLL_RD(可读:有报告待接收)与MP_STREAM_POLL_WR(可写:底层允许发送报告)。因此可以配合select.select()在多个流对象间轮询,避免阻塞:

import pyb, select hid = pyb.USB_HID() vcp = pyb.USB_VCP() while True: r, w, x = select.select([hid, vcp], [], []) if hid in r: data = hid.recv(8, timeout=0) # 处理 HID 输入

六、完整实战:将板卡变成组合输入设备

综合以上内容,一个把 STM32 板卡同时伪装成鼠标与键盘(通过VCP+HID保持串口调试能力)的完整程序如下:

import pyb, time # 1. 在 boot.py 中先行配置,或在此调用(推荐放在 boot.py) pyb.usb_mode('VCP+HID', hid=pyb.hid_mouse) hid = pyb.USB_HID() # 模拟鼠标移动轨迹 for i in range(10): hid.send((0, 5, 0, 0)) # 每次向右移动 5 time.sleep_ms(20) # 发送一次左键点击 hid.send((1, 0, 0, 0)) time.sleep_ms(20) hid.send((0, 0, 0, 0))

若需在鼠标与键盘之间切换,需要在切换后重新上电或重新执行pyb.usb_mode,因为源码注释明确 USB 设备"只在设备的通电生命周期内初始化一次"(only init USB once in the device's power-lifetime,见 usb.c),运行中反复切换模式不会生效。

七、自定义 HID 设备(进阶)

usb_modehid参数接受任意(subclass, protocol, max packet length, polling interval, report descriptor)五元组,因此理论上可以模拟任意符合 HID 规范的设备,例如游戏手柄、多媒体控制键等。报告描述符需按 USB HID 规范手工构造为字节序列:

import pyb # 自定义报告描述符(此处仅为示意,需按目标设备规范编写) custom_desc = bytes([ 0x05, 0x01, # Usage Page (Generic Desktop) 0x09, 0x05, # Usage (Game Pad) 0xA1, 0x01, # Collection (Application) ... 0xC0, # End Collection ]) pyb.usb_mode('VCP+HID', hid=(1, 0, 8, 8, custom_desc)) # subclass/协议/最大包长/轮询间隔/描述符 hid = pyb.USB_HID() hid.send(bytearray([...])) # 按自定义描述符定义的长度发送报告

从 usb.c 可看到hid关键字参数默认值为pyb.hid_mouse,即不传时按鼠标协议工作。

八、注意事项与限制小结

  1. 必须先行使能 HID 模式:在调用pyb.USB_HID()收发之前,确保boot.py中已调用pyb.usb_mode('VCP+HID', ...)(或'VCP+MSC+HID',后者仅 PYBD 板卡支持)。
  2. USB 配置只在通电生命周期内生效一次usb_mode必须在boot.py阶段调用,程序运行中途切换不会重新枚举设备。
  3. tuple/list 发送上限 8 字节:更长的报告请使用bytearray,否则抛ValueError
  4. 超时返回 0 / 空数据而非异常recv超时不抛异常,编程时需自行判断返回值。
  5. 旧的pyb.hid()函数已弃用:docs/library/pyb.rst 中说明pyb.hid((buttons, x, y, z))已弃用,应改用pyb.USB_HID.send();对应 C 侧实现pyb_hid_send_report也标注为 deprecated(usb.c)。
  6. 平台相关性pyb.USB_HID属 STM32 移植层专属接口(modpyb.c 注册于pyb模块),其它移植(如 ESP32、rp2)没有该模块,跨平台项目需使用各自移植的 HID API(如machine.USBDevice)。

九、进一步阅读

  • 类文档原文:docs/library/pyb.USB_HID.rst
  • pyb.usb_mode()/pyb.hid_mouse/pyb.hid_keyboard完整说明:docs/library/pyb.rst
  • Python 层绑定实现(send/recv/ioctl/构造器):ports/stm32/usb.c
  • 底层报告接收引擎:ports/stm32/usbd_hid_interface.c 与 usbd_hid_interface.h
  • 鼠标/键盘报告描述符与 USB 描述符:usbd_cdc_msc_hid.c
  • 真实使用示例:examples/SDdatalogger/boot.py
  • 嵌入式
  • 语言运行时
  • 编程语言
  • 解释器
  • 编译器
  • 物联网
  • 系统编程

【免费下载链接】micropython

MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems

项目地址:https://gitcode.com/gh_mirrors/mi/micropython
点击查看免费下载

相关推荐

上一篇:Learn Prompting学习路径:从新手到专家的提示工程成长计划
下一篇:把文献阅读时间砍掉一半:Awesome Claude Skills文献分析与总结工具完整上手指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询