news 2026/9/3 19:12:51

基于STM32F103的USB HID复合设备开发:鼠标键盘双接口设计全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于STM32F103的USB HID复合设备开发:鼠标键盘双接口设计全攻略

简介:STM32 RBT6 USB复合设备工程,面向嵌入式开发者和USB协议学习者,基于STM32F103RBT6实现单个USB设备同时模拟HID鼠标与HID键盘两个接口,解决多外设接入时USB端口占用与功能集成问题。工程包含可编译的完整源码,共48个文件,以23个.h头文件和15个.c源文件为主,覆盖USB控制器配置、设备/配置/接口/端点描述符、鼠标与键盘报告描述符、中断事务处理以及双接口软件架构;另附Keil工程文件、烧录固件和初始化脚本,压缩包仅110KB,目录层级清晰,便于直接导入开发环境编译验证。已有866人学习,适合希望深入理解USB复合设备枚举流程、HID报告描述符编写和STM32 USB固件库应用的开发者,能在较短时间内掌握从原理到实际调试的关键路径。 最近在调一颗 STM32F103RBT6 的 USB 复合设备,目标很直接:一根线进电脑,枚举出一个 HID 鼠标和一个 HID 键盘,两个 Interface 挂在同一个 USB 设备下面,系统端完全免驱。这个玩法在宏键盘、自定义控制台、工业触控板、实验室键鼠备份工具里都很常见,做起来却很能检验一个人对 USB 描述符体系的理解程度。

我最初以为这个项目就是把 HID 例程里鼠标的报告描述符换成键盘,最多再开个端点。真动手才发现,复合设备的核心根本不是“上报函数”,而是“配置描述符怎么组织”。鼠标接口占一个 Interface,键盘接口占另一个 Interface,主机识别驱动靠的是接口描述符里的类代码和协议号,任何一个字段写错,轻则只出一个设备,重则直接报代码 43。这篇博文就从硬件准备、描述符设计、代码实现到排查经验完整过一遍,给准备做多接口 HID 的朋友一个可以直接落地的参考。

1. 项目整体设计与思路拆解

1.1 USB 复合设备到底是怎么回事

先理清概念。一个 USB 设备有一个设备描述符,设备描述符下面有配置描述符,配置描述符里可以包含多个接口描述符,每个接口描述符下面还有端点描述符。所谓“复合设备”,就是同一个 USB 物理设备里挂载了多个接口,每个接口可以承载一个独立功能。HID 类设备尤其适合这种做法,因为键鼠是操作系统内置驱动,不需要额外装驱动。

鼠标和键盘虽然都属于 HID 类,但它们是两个独立的 Interface:接口 0 是鼠标,接口 1 是键盘。每个接口各有自己的 HID 描述符和端点描述符,上报数据互不干扰。主机端看到一个 USB 设备,枚举出两个 HID 接口后,会自动加载鼠标、键盘两套驱动,用户感知上等于插了一个同时具备鼠标和键盘功能的“二合一”设备。

理解这个结构的关键在于:设备描述符里的 bDeviceClass 必须设为 0x00,表示设备类由各个接口自行声明。如果把设备描述符直接改成 HID 类,有些系统会把整个设备当成单一的 HID 设备来解析,后面的多接口设计就失效了。这一点是最容易踩的坑。

1.2 为什么选 STM32F103RBT6 而不是其他方案

选型的时候其实也很纠结。STM32F103RBT6 是 Cortex-M3 内核,主频 72MHz,Flash 128KB,SRAM 20KB,内部自带 USB 2.0 Full Speed Device 外设,还带一块专用的 PMA(Packet Memory Area),用于 USB 数据包缓冲。相比之下,C8T6 的 Flash 只有 64KB,做纯粹的键鼠足够了,但 RBT6 同封装、同价格带里能多出一倍 Flash,后续想加字库、加协议栈或者多做一个自定义 HID 控制接口,空间会从容很多。

也有人会问,为什么不用 CH552 或者 ATtiny 这类方案。CH552 内置 USB,支持 HID 也很方便,价格更低,但它的开发资料和 USB 库成熟度相比 ST 要差一些,而且调试手段有限。STM32F103 的 USB 库和例程非常丰富,网上几乎能找到所有踩坑记录,对新手极其友好。RBT6 唯一的短板是没有硬件 OTG,只支持 Device 模式,但这个项目恰恰只需要 Device,浪费不了任何资源。

2. 硬件准备与最小系统设计

2.1 最小电路:别把 USB 引脚接错

硬件电路其实很简单,但有几个细节不能省。STM32F103RBT6 的 USB 引脚是固定的:PA11 对应 USB_DM,PA12 对应 USB_DP,这是硬件设计上写死的,软件改不了。建议在 D+ 和 D- 线上各串联一个 22Ω 的电阻,用于阻抗匹配,D+ 线上还要接一个 1.5kΩ 的上拉电阻到 3.3V,这是 USB 全速设备识别的关键,漏接或接错到 5V,电脑基本认不出设备。

晶振方面,我用的 8MHz 外部晶振,配合 PLL 倍频到 72MHz,再把 APB1 的 USB 时钟分频到 48MHz。USB 全速协议要求 48MHz 时钟,这个不能含糊,选 12MHz 或 25MHz 外部晶振也是可以的,但分频配置要重新算清楚。另外,很多最小系统板把 USB 的 D+ 上拉直接接到 3.3V,不做断开控制,这在调试阶段能用,但后面要实现在线重枚举会麻烦一些。更规范的做法是留一个 GPIO 控制上拉电阻,比如用 PA9 去控制一个三极管或 MOSFET,拉低一段时间再释放,就能让主机重新枚举设备,相当于软件里的“热拔插”。

2.2 供电、走线和常见布局问题

供电方面,USB 枚举阶段主机最多提供 100mA,正常工作时可以到 500mA。鼠标键盘这种低功耗外设,电流需求很小,但为了稳定,还是建议用 AMS1117-3.3 把 USB 的 5V 降到 3.3V,并在电源输入端放一个 10μF 钽电容加一个 100nF 陶瓷电容。不要直接把 5V 接到芯片 VDD,不然芯片大概率烧掉。

PCB 布线时,USB 的 D+ 和 D- 要尽量等长,并且不要穿过大电流路径。如果用的是洞洞板飞线,也尽量把这两根线缩短、靠地线隔离。我踩过一次坑:飞线板的 D+/D- 绕了半块板子,结果设备时而能识别、时而掉线,最后把线剪短重新飞线才稳定。这类“软故障”在 USB 上非常典型,优先级甚至比代码问题还高。

3. 核心细节解析:描述符与报告描述符

3.1 配置描述符是多接口设计的重头戏

USB 枚举过程中,主机会向设备请求各种描述符,而复合设备的区别集中体现在配置描述符的组成上。配置描述符里必须把 bNumInterfaces 改成 2,并且依次放入:接口 0 描述符、接口 0 的 HID 描述符、接口 0 的端点描述符、接口 1 描述符、接口 1 的 HID 描述符、接口 1 的端点描述符。

HID 鼠标和 HID 键盘的接口描述符差别不大,类代码都是 0x03,子类代码建议用 0x01(Boot Interface),这样在 BIOS 和某些无驱动环境下也能正常使用。关键在于协议号:鼠标是 0x02,键盘是 0x01,别写反。端点方面,我习惯给鼠标分配端点 1 IN(0x81),键盘分配端点 2 IN(0x82),最大包长分别是 4 字节和 8 字节,轮询间隔选 1ms 或 10ms 都可以,键鼠对这种延迟不敏感,我常用 1ms 保证响应更跟手。

配置项鼠标接口(Interface 0)键盘接口(Interface 1)
bInterfaceClass0x030x03
bInterfaceSubClass0x010x01
bInterfaceProtocol0x020x01
端点数1 个 IN1 个 IN
端点地址0x810x82
最大包长48
查询间隔1ms1ms

注意每个接口后面的 HID 描述符必须独立存在,因为 HID 描述符里包含了各自的报告描述符长度。主机按照“接口描述符 -> HID 描述符 -> 端点描述符”的顺序解析,任何一个 HID 描述符漏写,后面接口的解析就会错位。

3.2 鼠标和键盘的报告描述符怎么设计

HID 报告描述符是另一块核心内容。鼠标的报告描述符定义了 4 字节报文:第一字节低 3 位是左键、右键、中键状态,第二字节是 X 轴相对位移,第三字节是 Y 轴相对位移,第四字节是滚轮。键盘的报告描述符定义了 8 字节报文:第一字节是修饰键(Ctrl、Shift、Alt、Win),第二字节保留,后面 6 字节是普通按键的按键码。

两个接口使用各自的报告描述符,并且通过各自的 HID 描述符告诉主机报告描述符的地址和长度,因此这里不需要使用 Report ID。只有在同一个接口里混用多种报告类型时,Report ID 才是必须的。比如有些宏键盘,一个接口既上报普通按键又上报多媒体键,就会给两种报告各分配一个 ID,这个项目里用不上。

写报告描述符时,最容易错的是 Logical Minimum 和 Logical Maximum。鼠标的 X/Y 位移是相对量,逻辑范围要设置成 -127 到 127,对应十六进制就是 0x81 到 0x7F。键盘按键是绝对量,逻辑范围是 0 到 101,对应键盘键码表的上限。如果这两个值声明得不对,主机对报文的解析也会出问题。

4. 实操过程与核心代码实现

4.1 工程搭建的两种路线

我分别用 ST 标准外设库和 CubeMX/HAL 库做过这个项目,两条路线都可以,但改动点略有差别。

如果用标准外设库加 ST 官方的 USB-FS-Device Development Kit,主要改四个文件:usb_desc.c 里面定义设备描述符和配置描述符,usb_prop.c 里处理标准请求和厂商请求,usb_endp.c 里做端点数据收发,usb_conf.c 或 usb_conf.h 里配置端点数量和缓冲区。这个库看着老,但结构透明,适合深入理解 USB 枚举流程。

如果用 CubeMX 生成工程,默认只生成单 HID 设备,需要在 usbd_hid.c 里把报告描述符换掉,在 usbd_desc.c 里改设备描述符的 VID/PID,并在 usbd_conf.c 里确认端点配置足够。CubeMX 的好处是初始化代码自动生成,但复合设备描述符需要手动维护,没有标准库来得直观。

4.2 修改描述符实现双 Interface

以标准外设库为例,我直接把配置描述符数组改成两段结构,整体长度 59 字节。数组核心部分长这样:

/* USB 配置描述符 */ uint8_t ConfigDescriptor[] = { /* 配置描述符 */ 0x09, /* bLength */ 0x02, /* bDescriptorType */ 0x3B, 0x00, /* wTotalLength: 59 */ 0x02, /* bNumInterfaces: 2 */ 0x01, /* bConfigurationValue */ 0x00, /* iConfiguration */ 0xC0, /* bmAttributes: Self Powered */ 0x32, /* bMaxPower: 100mA */ /* Interface 0 描述符(鼠标) */ 0x09, /* bLength */ 0x04, /* bDescriptorType */ 0x00, /* bInterfaceNumber: 0 */ 0x00, /* bAlternateSetting */ 0x01, /* bNumEndpoints: 1 */ 0x03, /* bInterfaceClass: HID */ 0x01, /* bInterfaceSubClass: Boot */ 0x02, /* bInterfaceProtocol: Mouse */ 0x00, /* iInterface */ /* HID 描述符(鼠标) */ 0x09, 0x21, 0x10, 0x01, 0x00, 0x01, 0x22, 0x32, 0x00, /* 鼠标端点 IN */ 0x07, 0x05, 0x81, 0x03, 0x04, 0x00, 0x01, /* Interface 1 描述符(键盘) */ 0x09, /* bLength */ 0x04, /* bDescriptorType */ 0x01, /* bInterfaceNumber: 1 */ 0x00, /* bAlternateSetting */ 0x01, /* bNumEndpoints: 1 */ 0x03, /* bInterfaceClass: HID */ 0x01, /* bInterfaceSubClass: Boot */ 0x01, /* bInterfaceProtocol: Keyboard */ 0x00, /* iInterface */ /* HID 描述符(键盘) */ 0x09, 0x21, 0x10, 0x01, 0x00, 0x01, 0x22, 0x3A, 0x00, /* 键盘端点 IN */ 0x07, 0x05, 0x82, 0x03, 0x08, 0x00, 0x01, };

注意 HID 描述符中最后两个字节是报告描述符的长度,鼠标是 0x0032(50 字节),键盘是 0x003A(58 字节),必须和你实际定义的报告描述符数组长度严格一致。配置描述符的 wTotalLength 写了 0x003B,也就是 59 字节,主机就是靠这个长度遍历整个配置描述符集合的,长度算错,后面接口基本解析不出来。

4.3 鼠标和键盘的收发函数怎么写

描述符改完之后,收发逻辑就相对套路化了。鼠标上报在端点 1 IN,每次发 4 字节;键盘上报在端点 2 IN,每次发 8 字节。下面是我常用的两个发送函数:

/* 鼠标上报:buttons, dx, dy, wheel */ void HID_Mouse_Send(uint8_t buttons, int8_t dx, int8_t dy, int8_t wheel) { uint8_t report[4] = { buttons, (uint8_t)dx, (uint8_t)dy, (uint8_t)wheel }; UserToPMABufferCopy((uint8_t *)report, ENDP1_TXADDR, 4); SetEPTxCount(ENDP1, 4); SetEPTxValid(ENDP1); } /* 键盘上报:modifier + keycode[6] */ void HID_Keyboard_Send(uint8_t modifier, uint8_t *keycodes) { uint8_t report[8] = { modifier, 0x00, keycodes[0], keycodes[1], keycodes[2], keycodes[3], keycodes[4], keycodes[5] }; UserToPMABufferCopy((uint8_t *)report, ENDP2_TXADDR, 8); SetEPTxCount(ENDP2, 8); SetEPTxValid(ENDP2); }

这段代码的核心是UserToPMABufferCopy,它把数据写到指定的端点缓冲区地址,然后通过SetEPTxCount设置发送字节数,最后用SetEPTxValid把端点状态切到有效,让 USB 外设在下一个帧周期把数据发出去。有时候漏了 SetEPTxCount,或者端点地址写错,主机会一直收不到数据,现象就是设备正常枚举,但鼠标键盘都没反应。

4.4 PMA 缓冲区分配

STM32F103 的 USB 外设使用一块独立的 PMA 区域,用来存放各个端点的数据。默认的 usb_conf.h 里会给 EP0 分配收发缓冲区,我们要在此基础上追加 EP1 和 EP2 的缓冲区。比如我这样分配:

/* PMA 地址分配示例 */ #define ENDP0_RXADDR (0x40) #define ENDP0_TXADDR (0x80) #define ENDP1_TXADDR (0x88) #define ENDP2_TXADDR (0x90)

注意分配规则是地址必须按字对齐,并且各个端点的缓冲区不能重叠。EP0 收发各用了 64 字节,EP1 占 4 字节,EP2 占 8 字节,这样分配刚好不冲突。如果后面再增加第三个接口,需要继续往后推,比如 0x98 开始给第三个端点分配空间。缓冲区重叠的典型症状是鼠标和键盘互相干扰,发一组数据,另一个设备也会动一下,排查起来很痛苦。

5. 常见问题与排查技巧实录

5.1 枚举失败:代码 43 和代码 12

最常见的故障就是插上电脑没反应,设备管理器里出现黄色感叹号。代码 43 通常是枚举中途失败,设备描述符或配置描述符返回有问题,比如 wTotalLength 算错、HID 描述符长度字段错误、端点描述符的轮询间隔为 0 等。代码 12 表示“该设备找不到足够资源可以使用”,在 HID 复合设备上不常出现,一旦出现,先换一个 USB 口,再拔掉其他 USB 设备,因为 PC 的 USB 控制器资源被占满时也会报这个错。

排查这类问题,我强烈建议用 USB 抓包工具。Windows 下可以用 Wireshark 加 USBPcap,或者直接用 USBlyzer,看主机请求到哪个描述符时设备没有正确响应。枚举过程是“主机发请求 -> 设备响应”,只要响应包里数据异常,基本就能定位到是描述符数组的问题,而不是硬件问题。

5.2 只枚举出鼠标,没有键盘

这个问题十有八九是 bNumInterfaces 没有改成 2,或者配置描述符里第二个接口描述符的位置不对。主机遍历配置描述符时,会按固定格式解析,如果第一个接口的 HID 描述符长度写错,主机解析到一半就停止,第二个接口自然看不到。

还有一种情况是,主机识别出了两个 HID 接口,但都是按鼠标加载的。检查一下接口描述符里的 bInterfaceProtocol,鼠标 0x02、键盘 0x01,如果两个接口都写成 0x02,系统会当成双鼠标处理,键盘接口的功能就无法正常工作。

5.3 枚举正常但按键无响应

设备管理器里两个 HID 设备都正常,但按按键没反应,这种问题集中在发送逻辑。先确认键盘上报用的是端点 2 IN,如果误发到端点 1,数据会被鼠标接口接收,主机自然不会处理。再确认键盘报文是否包含了“释放”状态:普通键盘按下某个键时,按键码放进 keycode 数组,松开时必须发送一个全 0 的 8 字节报文。如果一直重复发送按下状态,操作系统只会识别一次按键,表现为按键不连续、像失灵。

鼠标的位移也要注意,X 和 Y 是相对偏移值,转动多少发多少,不要一直发固定值。滚轮状态同样需要在使用后清零,否则系统会认为滚轮一直在滚动。

5.4 排查思路总结

现象可能原因处理办法
设备管理器代码 43描述符长度/内容错误USB 抓包,对照标准描述符规范修改
设备管理器代码 12USB 控制器资源不足换 USB 口,移除其他 USB 设备,更新驱动
只枚举出一个 HID 接口bNumInterfaces 未修改或描述符顺序错检查配置描述符集合结构
两个接口都变成鼠标接口协议号写错鼠标 0x02,键盘 0x01
枚举正常但键鼠无反应端点地址错、未发送释放报文核对端点地址,确保发送空报告
键鼠互相干扰PMA 缓冲区重叠重新分配端点缓冲区地址

我在实际项目里调试这个复合设备,大概花了一天半,其中半天都在跟“只出鼠标不出键盘”较劲,最后发现问题是我在配置描述符里把 bNumInterfaces 写成 1 了,这种低级错误用抓包工具一眼就能看出来。USB 调试没有太多玄学,描述符就是设备向主机自报家门的“身份表”,跟着抓包数据逐字节核对才是最快路径。

这个项目的价值在于,把两个 HID 接口的套路吃透之后,后面再加第三个接口、第四个接口,甚至混合 CDC 串口,思路都是一样的:描述符里加一个接口块,中断处理里加一个端点,收发逻辑复制一份。RBT6 的 USB 资源足够支持 4 个以上的端点,性能完全不是瓶颈。最后再分享一个小技巧,把鼠标和键盘的报告描述符分别放在独立数组里,并在 HID 描述符里用宏方式引用,后面想改键位、增加自定义控制功能,只动数组即可,不必碰配置描述符的布局。

本文还有配套的精品资源,点击获取

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

双人文化关系应用如何管理两套资料:输入校验、响应模式与使用边界

双人文化关系应用如何管理两套资料,关键不是完成一次调用,而是让输入口径、处理状态和结果证据可以复核。本文围绕“如何管理双方出生资料、重点方向、同步异步响应和文化娱乐使用边界”给出一套面向真实业务流程的实现方式。 问题与结果 双方资料独立校…

作者头像 李华
网站建设 2026/9/3 19:11:29

Linux sudo黑屏响“咚”?一文掌握排查与修复方案

1. 这到底是个什么“BUG” 如果你最近在 Linux 社区、技术群或者某些段子手聚集的论坛里逛过,大概率看到过这么一条消息: 修复了 Linux 在 sudo 时不会黑屏和“咚!”的 BUG 初看像是某个发行版的更新日志,仔细一想又不太对劲。因…

作者头像 李华
网站建设 2026/9/3 19:08:19

STM32H743基础例程实战:从时钟配置到OV2640摄像头开发

简介:一份面向嵌入式开发者的 STM32H743 基础例程集合,覆盖 GPIO、看门狗、定时器、PWM、PWM 捕获、LCD 与 SRAM 等关键模块,适合刚接触该芯片或需要快速上手外设配置的开发者。压缩包共 2000 个文件,约 382MB,包含大量…

作者头像 李华
网站建设 2026/9/3 19:07:21

Code::Blocks 17.12便携版全配置:MinGW编译器、深色主题与LVGL模拟器

简介:Code::Blocks 17.12 是一款面向 C/C 开发者的开源跨平台集成开发环境,这份 zip 压缩包即其完整发行版,适合需要在 Windows、Linux 或 macOS 上搭建轻量级编程环境的初学者、学生与日常开发者。包内共 2000 个文件,以 C/C 头文…

作者头像 李华
网站建设 2026/9/3 19:06:56

Qt跨平台U盘热插拔监测:从系统通知到信号封装

简介:面向Linux平台Qt开发者的一份实用参考资源,聚焦如何利用Qt框架实时监测U盘等USB设备的热插拔事件,适合需要为文件管理器、备份工具或系统监控应用增加外部存储感知能力的C程序员。资源打包为gz格式,共2个文件,包含…

作者头像 李华
网站建设 2026/9/3 19:03:06

当进入欧洲市场,如何选择专业雇主PEO还是名义雇主EOR?

在企业进入欧洲市场时、了解专业雇主PEO与名义雇主EOR的区别重要。PEO主要提供全方位的人力资源管理支持、帮助企业处理员工薪资、税务等法律合规事务,使公司能更专注于核心业务。另外,PEO适合需要长期管理员工关系的企业,为其提供灵活的用工…

作者头像 李华