☰
STM32F103实现USB复合设备:HID键盘+鼠标描述符与端点设计详解
2026/10/22 12:04:11 网站建设 项目流程

简介:面向嵌入式开发者的 STM32 RBT6 USB 复合设备工程资源,实现了单个芯片同时枚举为 HID 鼠标和 HID 键盘两个接口,解决了多输入设备 USB 功能整合的问题,适合学习 USB 协议与 STM32 固件库的入门及进阶开发者。资源共包含 48 个文件,压缩包约 110KB,以 C 与 H 源码为主,其中含有 23 个头文件和 15 个 C 文件,同时附带 Keil 工程文件、HID 报告描述符配置、USB 设备描述符、hex 固件以及链接脚本等;目录按 SYSTEM、USER、USB 等模块划分,工程结构一目了然。目前已有 866 人学习下载,具备一定参考热度。压缩包内是完整可编译的 Keil 工程,烧录后能被电脑直接识别为鼠标加键盘的复合设备;源码中对 USB 端点、描述符及中断处理做了详细标注,可对照理解两个 Interface 的枚举过程与 HID 数据上报机制,既适合协议学习,也可作为后续复合设备开发的直接模板。 前面调试HID设备的时候经常遇到一个问题:网上的键盘例程和鼠标例程分开看都是正常的,但想要用同一块板子既当键盘又当鼠标,直接把两套代码拼到一起,插上电脑却只能识别出其中一个功能。原因很直接——USB设备在主机眼里不是"代码工程",而是"描述符",当配置描述符里只有一个HID接口时,主机就只能把它识别成键盘或者鼠标中的一种。要打破这个限制,就得在USB协议层面把设备定义成"复合设备",在同一个配置描述符下面挂两个Interface,每个Interface独立上报HID事件。

这篇文章我会以STM32F103RBT6为核心,完整梳理USB复合设备(HID键盘 + HID鼠标)从描述符设计、报告描述符编写、端点数据上报到枚举排错的全过程。RBT6内置全速USB Device控制器,PA11/PA12直接引出USB差分信号,不需要外挂USB转串口芯片,非常适合做这种轻量级的HID方案。文章里给出的所有代码和配置都围绕"两个Interface、两个HID设备"这个目标展开,不管是想做自定义按键矩阵、自动化测试工装,还是游戏外设的模拟器,都能直接用这套框架去改。

1. 先想明白:为什么"键盘+鼠标"必须做成复合设备

1.1 单个HID接口的瓶颈在哪

USB HID设备的核心特征是:设备通过中断端点向主机周期性上报数据,主机解析这些数据并把它映射成键盘输入、鼠标移动、游戏手柄等操作。键盘例程能跑通,是因为设备描述符里声明了一个"键盘类"接口;鼠标例程能跑通,是因为声明了一个"鼠标类"接口。但USB协议里,一个设备地址下可以存在多个接口(Interface),主机枚举设备时会逐个读取配置描述符里的接口信息,按照每个接口自己的HID报告描述符去解析数据。

这就是复合设备存在的意义——整根USB线只有一个物理连接、一个设备地址,但逻辑上可以拆成多个独立的HID设备。比如这里要做的双接口方案,就是让主机同时看到"HID Keyboard Device"和"HID-compliant mouse"两个节点,它们各自占用一个Interface,各自拥有一个独立的IN端点。对应用层来说,这就像是把两个USB设备并联到了同一个MCU上,互不干扰。

1.2 复合设备的实现门槛是什么

复合设备并不需要写两套USB协议栈,也不需要做USB Hub。关键的差异只在描述符的组织方式上:

  • 设备描述符中,bDeviceClass必须设为0x00,表示"设备类由各个接口自己决定";
  • 配置描述符中,bNumInterfaces必须设为2;
  • 每个接口描述符后面,都要跟一套独立的HID描述符和端点描述符;
  • 键盘和鼠标的HID报告描述符是两份不同的数据,枚举时主机分别索取。

这个思路一旦理清,剩下的工作就是往描述符数组里正确填充字节。最麻烦的不是协议本身,而是描述符长度和报告描述符长度一旦计算错误,主机就会拒绝枚举或者只识别出部分接口。后面的章节我会把两个接口的完整描述符代码贴出来,并逐段说明每个字节的含义,照着抄也能避开大部分坑。

2. RBT6的USB硬件准备与时钟配置

2.1 引脚分配和原理图要点

STM32F103RBT6的USB Device控制器在芯片内部,外部需要引出的信号只有两条:USB_DP(PA12)和USB_DM(PA11)。和用CH340这类USB转串口芯片不同,F103的USB是直接挂到USB总线上的,所以信号质量直接决定能不能被主机正确枚举。

画原理板时这几个点最容易出问题:

  • PA11、PA12上不要挂LED、按键或者其他负载,这两根线应该走差分对,尽量短、尽量等长;
  • DP(PA12)上需要1.5kΩ上拉电阻到3.3V,这相当于告诉主机"这是一个全速设备"。F103内部虽然也有上拉控制逻辑,但很多官方评估板仍然会在外部放一个1.5k电阻,调试阶段建议先保留;
  • USB的VBUS最好加一个分压电阻采样到MCU,用来检测USB线是否插入,这样MCU可以在未插入时进入低功耗模式,也方便做热插拔初始化。

RBT6本身是LQFP64封装,IO充足,但USB_DP和USB_DM这两个引脚没有第二功能复用,硬件上只需要确认它们没有被其他外设占用即可。

2.2 48MHz USB时钟是枚举成功的前提

USB全速设备的位时钟是12MHz,但F103的USB外设内部需要48MHz的时钟输入。这个48MHz只能从PLL获得,典型配置是:

  • 8MHz HSE
  • PLL倍频9倍,得到72MHz系统时钟
  • USB预分频器选择1.5分频,得到48MHz

用标准外设库配置时,SetSysClock()函数中要特别注意:

RCC_CFGR_USBPRE = RCC_CFGR_USBPRE_PLLCLK_Div1_5;

如果用HAL库,则是在SystemClock_Config()里设置PLLM=1, PLLN=9, PLLP=RCC_PLLP_DIV2,同时使能RCC_ClkInitStruct.USBClock = RCC_USBCLKSOURCE_PLLSA1_DIV1之类的选项(具体取决于HAL版本)。

有一类很隐蔽的坑:板子上明明有8MHz晶振,但VBAT没接、或者晶振负载电容没焊,HSE起振失败。此时系统会切到HSI(8MHz)运行,PLL照样能配出72MHz,但PLL的参考源变成了HSI,算出来的USB时钟就不是精确的48MHz,轻则枚举不稳定,重则完全识别不到设备。排查时可以先看运行指示灯是否正常,再用示波器量OSC_IN/OSC_OUT是否有8MHz波形,没有就直接查晶振和负载电容。

2.3 USB Device库怎么选

F103平台常见的USB实现路径有三条:

  • 标准外设库 + ST官方的STM32_USB-FS-Device_Lib(老项目最常见)
  • STM32CubeMX + HAL库 + USB Device中间件(新项目主流)
  • 开源协议栈如TinyUSB(需要自己适配F103)

如果是修改现有例程,我建议直接用官方例程改起。STM32_USB-FS-Device_Lib里的"Custom HID"例程已经把枚举状态机、描述符请求、端点中断都封装好了,你要做的只是替换描述符数组和填充发送缓冲区。TinyUSB虽然代码更现代,但对F103这种老芯片的适配资料相对少,排查成本高。

我这里下面的代码示例以标准外设库版本的USB-FS-Device_Lib为参考,HAL版本的核心差异只在API名称上,描述符部分完全通用。

3. 描述符结构拆解:两个Interface怎么嵌进一个配置描述符

3.1 USB描述符的层级关系

USB设备通过描述符向主机"自我介绍",这相当于一份简历,主机靠它决定加载什么驱动。描述符的层级可以类比成一个公司的组织架构:

  • 设备描述符:公司名称和营业执照,一个USB设备只有一个;
  • 配置描述符:一份部门总览,说明这个设备有哪些部门、需要多少电流;
  • 接口描述符:一个具体的部门,比如键盘部门、鼠标部门;
  • 端点描述符:部门里的通信窗口,主机通过窗口收发数据。

对于复合设备,一个配置描述符下面可以挂多个接口描述符。每个接口描述符后面跟HID描述符(说明本接口用的是HID类),HID描述符里再说明HID报告描述符的长度和位置。主机通过GET_DESCRIPTOR请求,先把配置描述符和接口、端点全部读一遍,然后再针对每个HID接口单独请求HID报告描述符。

3.2 完整的配置描述符代码

下面这段是双接口复合设备的核心,我直接贴出来并标好注释:

const uint8_t Composite_ConfigDescriptor[] = { // ---------- Configuration Descriptor (9 bytes) ---------- 0x09, // bLength USB_CONFIGURATION_DESCRIPTOR_TYPE, // bDescriptorType = 0x02 0x3B, 0x00, // wTotalLength = 59 bytes 0x02, // bNumInterfaces = 2 0x01, // bConfigurationValue 0x00, // iConfiguration (no string) 0x80, // bmAttributes = Bus Powered 0x32, // bMaxPower = 100mA // ---------- Interface 0: Keyboard (9 bytes) ---------- 0x09, // bLength USB_INTERFACE_DESCRIPTOR_TYPE, // bDescriptorType = 0x04 0x00, // bInterfaceNumber = 0 0x00, // bAlternateSetting 0x01, // bNumEndpoints = 1 0x03, // bInterfaceClass = HID 0x01, // bInterfaceSubClass = Boot 0x01, // bInterfaceProtocol = Keyboard 0x00, // iInterface (no string) // ---------- HID Descriptor for Keyboard (9 bytes) ---------- 0x09, // bLength 0x21, // bDescriptorType = HID 0x01, 0x01, // bcdHID = 1.01 0x00, // bCountryCode 0x01, // bNumDescriptors 0x22, // bDescriptorType = Report 0x41, 0x00, // wDescriptorLength = 65 // ---------- Endpoint 1 IN (7 bytes) ---------- 0x07, // bLength USB_ENDPOINT_DESCRIPTOR_TYPE, // bDescriptorType = 0x05 0x81, // bEndpointAddress = EP1 IN 0x03, // bmAttributes = Interrupt 0x08, 0x00, // wMaxPacketSize = 8 0x0A, // bInterval = 10ms // ---------- Interface 1: Mouse (9 bytes) ---------- 0x09, // bLength USB_INTERFACE_DESCRIPTOR_TYPE, // bDescriptorType = 0x04 0x01, // bInterfaceNumber = 1 0x00, // bAlternateSetting 0x01, // bNumEndpoints = 1 0x03, // bInterfaceClass = HID 0x01, // bInterfaceSubClass = Boot 0x02, // bInterfaceProtocol = Mouse 0x00, // iInterface (no string) // ---------- HID Descriptor for Mouse (9 bytes) ---------- 0x09, // bLength 0x21, // bDescriptorType = HID 0x01, 0x01, // bcdHID = 1.01 0x00, // bCountryCode 0x01, // bNumDescriptors 0x22, // bDescriptorType = Report 0x34, 0x00, // wDescriptorLength = 52 // ---------- Endpoint 2 IN (7 bytes) ---------- 0x07, // bLength USB_ENDPOINT_DESCRIPTOR_TYPE, // bDescriptorType = 0x05 0x82, // bEndpointAddress = EP2 IN 0x03, // bmAttributes = Interrupt 0x08, 0x00, // wMaxPacketSize = 8 0x01 // bInterval = 1ms };

这份数组里每个字节都对号入座。我特别提醒两点:

  • wTotalLength要计算整个数组的总长度。这里配置描述符9字节 + 键盘接口9字节 + 键盘HID描述符9字节 + 键盘端点7字节 + 鼠标接口9字节 + 鼠标HID描述符9字节 + 鼠标端点7字节 = 59字节(0x3B)。算错这个值,主机只会读到你数组的一部分,设备节点会莫名其妙少一个接口;
  • 两个HID描述符里的wDescriptorLength要和实际的HID报告描述符数组长度保持一致。键盘报告描述符长65字节,鼠标报告描述符长52字节,这是严格对应的,后面写报告描述符时不能随意增删条目。

3.3 HID报告描述符:键盘和鼠标各自怎么上报数据

配置描述符只是告诉主机"我有两个HID接口",真正的"数据格式"由HID报告描述符定义。键盘的标准Boot Protocol报告长8字节:

const uint8_t KEYBOARD_ReportDescriptor[] = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x05, 0x07, // Usage Page (Keyboard/Keypad) 0x19, 0xE0, // Usage Minimum (224) 0x29, 0xE7, // Usage Maximum (231) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x81, 0x02, // Input (Data, Variable, Absolute) - modifier byte 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x01, // Input (Constant) - reserved byte 0x95, 0x05, // Report Count (5) 0x75, 0x01, // Report Size (1) 0x05, 0x08, // Usage Page (LEDs) 0x19, 0x01, // Usage Minimum (1) 0x29, 0x05, // Usage Maximum (5) 0x91, 0x02, // Output (Data, Variable, Absolute) - LED report 0x95, 0x01, // Report Count (1) 0x75, 0x03, // Report Size (3) 0x91, 0x01, // Output (Constant) - padding 0x95, 0x06, // Report Count (6) 0x75, 0x08, // Report Size (8) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x00, // Logical Maximum (255) 0x05, 0x07, // Usage Page (Keyboard/Keypad) 0x19, 0x00, // Usage Minimum (0) 0x29, 0xFF, // Usage Maximum (255) 0x81, 0x00, // Input (Data, Array) - key codes 0xC0 // End Collection };

这个报告描述符定义的数据结构是:第0字节是修饰键位(Ctrl、Shift、Alt、GUI),第1字节保留,第2到第7字节是6个同时按下的按键值。发送时就是填充这8个字节,通过EP1 IN端点传到主机。

鼠标的报告描述符定义4字节数据:

const uint8_t MOUSE_ReportDescriptor[] = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x02, // Usage (Mouse) 0xA1, 0x01, // Collection (Application) 0x09, 0x01, // Usage (Pointer) 0xA1, 0x00, // Collection (Physical) 0x05, 0x09, // Usage Page (Buttons) 0x19, 0x01, // Usage Minimum (1) 0x29, 0x03, // Usage Maximum (3) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x95, 0x03, // Report Count (3) 0x75, 0x01, // Report Size (1) 0x81, 0x02, // Input (Data, Variable, Absolute) - buttons 0x95, 0x01, // Report Count (1) 0x75, 0x05, // Report Size (5) 0x81, 0x01, // Input (Constant) - padding 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x09, 0x38, // Usage (Wheel) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) 0x75, 0x08, // Report Size (8) 0x95, 0x03, // Report Count (3) 0x81, 0x06, // Input (Data, Variable, Relative) - X, Y, Wheel 0xC0, // End Collection 0xC0 // End Collection };

鼠标报告的4字节含义是:第0字节是按键状态(bit0左键、bit1右键、bit2中键),第1字节是X轴位移(-127到127),第2字节是Y轴位移,第3字节是滚轮位移。它是相对坐标,每次上报的值代表从上次位置移动了多少。

4. 端点收发与数据上报逻辑

4.1 两个IN端点如何分工

配置描述符里已经定义了EP1 IN(键盘)和EP2 IN(鼠标)。在USB协议栈里,端点0用于枚举控制传输,EP1、EP2用于数据传输。F103的USB内置Packet Memory(PMA),每个端点都需要分配自己的缓冲区,两个端点不能共用同一块PMA地址。

在STM32_USB-FS-Device_Lib中,端点初始化通常在USB_Config()或者Joystick_Config()这类函数里完成。以标准库的USB_SIL_Write为例:

// 上报键盘数据到 EP1 IN uint8_t keyboard_report[8] = {0}; void HID_Keyboard_Send(uint8_t modifier, uint8_t keycode) { keyboard_report[0] = modifier; keyboard_report[1] = 0x00; keyboard_report[2] = keycode; keyboard_report[3] = 0x00; keyboard_report[4] = 0x00; keyboard_report[5] = 0x00; keyboard_report[6] = 0x00; keyboard_report[7] = 0x00; USB_SIL_Write(EP1_IN, (uint8_t*)keyboard_report, 8); SetEPTxValid(ENDP1); }

鼠标发送函数类似,只是数据是4字节、端点是EP2:

uint8_t mouse_report[4] = {0}; void HID_Mouse_Send(int8_t dx, int8_t dy, int8_t wheel, uint8_t buttons) { mouse_report[0] = buttons; mouse_report[1] = dx; mouse_report[2] = dy; mouse_report[3] = wheel; USB_SIL_Write(EP2_IN, (uint8_t*)mouse_report, 4); SetEPTxValid(ENDP2); }

USB_SIL_Write负责把数据从用户缓冲区拷贝到PMA,SetEPTxValid则是把端点状态切换到"发送有效",USB外设会在下一个IN令牌时把数据发给主机。如果发送太频繁,上一个包还没发完就再次调用SetEPTxValid,会造成端点状态覆盖,所以实际项目中通常会加一个Busy标志:

volatile uint8_t ep1_busy = 0; void HID_Keyboard_Send(...) { if (ep1_busy) return; USB_SIL_Write(EP1_IN, ...); SetEPTxValid(ENDP1); ep1_busy = 1; }

在端点发送完成中断EP1_IN_Callback()里清除:

void EP1_IN_Callback(void) { ep1_busy = 0; }

4.2 数据上报的节奏和多按键处理

HID键盘支持6键无冲突(6KRO),也就是说同一时刻最多可以发6个按键值,这对大多数输入场景都够用了。实现组合键也很简单,比如Ctrl+Alt+Delete:

uint8_t modifier = 0x07; // Ctrl(0x01) | Alt(0x04) | GUI(0x08)?

需要说明的是,注册表项的修饰键编码是按位来算的:左Ctrl是0x01,左Shift是0x02,左Alt是0x04,左GUI(Win键)是0x08。Ctrl+Alt+Delete其实是左Ctrl(0x01)+ 左Alt(0x04),等于0x05,按Delete键的键值是0x4C。但我实际测试时发现,在Windows登录界面,这个组合键会被系统特殊拦截,不一定能通过普通HID键盘模拟成功,这是Windows安全机制的一部分。想验证组合键功能,用Ctrl+C、Alt+Tab这类普通组合键更稳妥。

鼠标的移动是相对位移,主机拿到数据后按当前光标位置累加,所以连续发送快一点,光标就移动得快一点。调试时可以做一个简单的正弦波移动,让光标自动画圈:

int16_t angle = 0; void Mouse_Demo_Task(void) { int8_t dx = (int8_t)(10 * arm_sin_f32(angle * 0.017f)); int8_t dy = (int8_t)(10 * arm_cos_f32(angle * 0.017f)); HID_Mouse_Send(dx, dy, 0, 0); angle += 10; if (angle >= 360) angle = 0; }

调用频率控制在50Hz左右,也就是每20ms发一次,光标轨迹就是平滑的圆。发太快会占满USB带宽,10ms一次对鼠标已经足够。

4.3 USB Suspend/Resume 和低功耗事项

最后提一个容易被忽略的状态:当主机进入睡眠或者USB总线空闲超过3ms时,USB外设会进入Suspend状态。F103在Suspend时如果没做处理,会保持唤醒状态,白白耗电。标准库的USB_EventBusReset、USB_EventSuspend等回调里可以加入自己的处理逻辑,比如关闭其他外设、进入STOP模式。但如果你的设备是USB供电且需要一直在线,最好直接忽略Suspend事件,只处理Resume后重新初始化端点状态。

5. 枚举失败和代码12这类问题的完整排查链路

5.1 现象:设备管理器出现"该设备找不到足够资源可以使用(代码 12)"

这个错误在Windows下很常见,尤其是当电脑同时接了摄像头、无线接收器、多块声卡等高带宽设备时。它并不是说固件描述符写错了,而是USB主控制器在带宽分配阶段发现,已经没法给新的中断端点或等时端点安排足够的总线时间。

我遇到过这样一个场景:把双HID复合设备插到一台只带一个USB控制器的紧凑型主机上,系统里已经挂了一个UVC摄像头(等时传输,占用带宽很大)和两个无线键鼠接收器,此时复合设备插上去就报代码12。拔掉摄像头后,设备立刻枚举成功。

解决思路按优先级排序:

  • 检查USB带宽占用:Windows的设备管理器里,在"通用串行总线控制器"下找到对应根Hub,查看属性里的"高级"标签,能看到带宽占用率,把占用大的设备暂时拔掉测试;
  • 调整键盘端点的bInterval。键盘本质不需要1ms轮询,Windows对键盘轮询间隔也没有苛刻要求,把bInterval改成10(也就是10ms),能显著降低总线占用;
  • 把wMaxPacketSize从64改成8。HID键盘和鼠标的单次上报数据量远小于8字节,64字节的MaxPacketSize纯粹是浪费带宽;
  • 如果鼠标也允许牺牲一点延迟,可以适当调大鼠标的bInterval,但我不建议,鼠标的实时性直接影响体验。

USB全速总线的帧周期是1ms,帧内要分配所有中断端点和等时端点的传输时间。两个8字节的HID中断端点理论上占用非常小,但一旦总线上有大流量等时设备,再加上这种聚合的HID设备,控制器的带宽分配策略就会开始"挑肥拣瘦"。所以遇到代码12时,不要第一时间怀疑自己的描述符写错了,先排查总线上的其他设备。

5.2 现象:设备枚举失败或者反复重连

这个现象和代码12完全不同,往往是硬件或者描述符本身的问题。排查链路我建议按下面的顺序走:

  1. 确认VBUS电压:用万用表量MCU供电引脚,确保3.3V正常,VBUS经过分压后给MCU的电压不能异常偏低;
  2. 确认8MHz晶振起振:示波器量OSC_IN,能看到8MHz波形再查下一步;
  3. 确认DP上拉:PA12到USB连接器的DP线中间应该有1.5kΩ上拉,如果没有,主机完全看不到设备插入;
  4. 用总线分析仪或者逻辑分析仪抓枚举过程:开源的USBPcap + Wireshark组合能抓到USB枚举的URB包,可以看到主机在哪个请求上失败。最常见的是GET_DESCRIPTOR(Configuration)返回的wTotalLength和实际接收到的数据长度不一致,主机就会直接放弃枚举;
  5. 确认报告描述符长度:配置描述符里两个HID描述符的wDescriptorLength必须和对应报告描述符数组的元素个数一致,多一字节、少一字节都会让HID解析失败。

有一次我调试时,改键盘报告描述符时多加了一个Usage Page项,但忘了同步更新HID描述符里的长度,结果鼠标接口能识别,键盘接口变成未知设备。这种问题用肉眼很难发现,但只要有意识地核对描述符长度,几分钟就能定位。

6. 实测效果与扩展思路

6.1 插上之后系统里应该看到什么

把编译好的固件烧进RBT6,插上USB线,Windows的设备管理器里会出现两个节点:一个"HID Keyboard Device",一个"HID-compliant mouse"。这两个节点挂在同一个复合设备下面,但系统会为它们分别加载驱动,互不影响。

我在实测时验证过几个场景:

  • 键盘和鼠标同时工作:一边用键盘发字符串,一边发送X/Y位移,光标移动和字符输入同时发生,没有相互干扰;
  • 键盘的修饰键:按住左Shift的同时发字母键,主机能正确得到大写字母;
  • 鼠标滚轮:通过修改第4字节发送滚轮事件,页面正常上下滚动。

Linux下也可以用lsusb -v查看描述符树,能看到两个HID接口各带一个中断IN端点,用evtest能分别抓取键盘和鼠标事件。

6.2 从双接口扩展到更多功能

这套描述符框架的扩展性很强。想加多媒体按键,可以在键盘报告描述符里增加Consumer Page(Usage Page 0x0C),把音量加减、播放暂停映射到单独的Usage ID上,然后通过一个额外的接口或者直接在键盘报告里扩展报告长度实现。想模拟触摸屏,则要把鼠标报告从相对坐标改成绝对坐标(Logical Minimum/Maximum映射到屏幕分辨率),系统就能把设备识别成HID触摸屏。

更常见的一种扩展是"键盘 + 鼠标 + 厂商自定义接口"三合一。在配置描述符里继续增加第三个接口,Bulk传输,用于和上位机做双向数据通道。这样一根USB线既能当键鼠工具,又能承担数据透传功能,很多自动化测试设备的雏形就是这么来的。

RBT6的Flash是128KB,RAM是20KB,跑一个双HID复合设备绰绰有余,甚至还能同时跑一个Bootloader。但要注意,USB端点缓冲区(PMA)是独立的1KB SRAM,如果以后增加接口,每个端点的PMA分配需要在usb_conf.h里预先规划好,避免端点之间缓冲区重叠。

6.3 实际项目里的一些细节沉淀

最后分享几个我在这个项目里踩过的坑:

  • 描述符改动后,一定要重新插拔USB线,而不是只复位MCU。有时候主机端的枚举信息缓存还保留着旧描述符,复位MCU不会触发重新枚举;
  • 端点发送完成中断里,不要做太重的处理。F103的USB中断优先级建议设为中等偏低,避免和ADC、定时器中断抢优先级导致数据丢失;
  • 如果你用的是HAL库,注意中间件版本差异。STM32CubeMX生成的USB Device中间件在HAL库升级后,描述符结构体定义可能有变化,不要直接把标准库的代码原样拷贝进去;
  • 测试的时候可以用记事本当靶子,但别用它测组合键——记事本对Ctrl+C这类快捷键反应不直观。用一个带输入框的调试工具或者串口助手界面,能看到按键事件是否真的到达应用层。

这套双HID复合设备的方案,核心价值在于把"USB描述符"这件看起来像玄学的事情讲透。只要理解了配置描述符、接口描述符、HID报告描述符之间的关系,往后不管是改报告格式、加第三接口,还是换到其他芯片平台,思路都是一样的——先理清描述符,再谈收发逻辑。F103RBT6在2025年的今天依然有大量低成本工装项目在用它跑HID方案,正是因为它稳定、资料多、改起来快。希望这篇能帮你少走几步弯路,把时间花在真正有创造性的功能实现上。

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

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

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

立即咨询