USB 设备/配置/接口/端点/描述符 和 HID类请求详解

目录

前言

[一. 传输轮询配置](#一. 传输轮询配置)

[二. Logic 2 抓包示例](#二. Logic 2 抓包示例)

[2.1 抓包工具型号](#2.1 抓包工具型号)

[2.2 USB默认空闲传输](#2.2 USB默认空闲传输)

[2.2.1. 先拆解看到的时序(SOF 间隔 1ms)](#2.2.1. 先拆解看到的时序(SOF 间隔 1ms))

[2.2.2. 为什么不是严格 5ms?核心原因有 3 个](#2.2.2. 为什么不是严格 5ms?核心原因有 3 个)

(1)bInterval=5的本质:「最小轮询间隔」而非「精准周期」

[(2)IN 包必须跟随 SOF 帧,只能是「整数 ms」间隔](#(2)IN 包必须跟随 SOF 帧,只能是「整数 ms」间隔)

[(3)设备回复 NAK 会影响主机调度](#(3)设备回复 NAK 会影响主机调度)

[3. 补充:bInterval=5 的 "正确预期"](#3. 补充:bInterval=5 的 “正确预期”)

总结

[2.3 全速USB周期性SOF(上图SOF包 解释)](#2.3 全速USB周期性SOF(上图SOF包 解释))

[2.4 USB 全速模式下「主机轮询设备 IN 中断端点」的流程(上图SOF+IN+NAK包 解释)](#2.4 USB 全速模式下「主机轮询设备 IN 中断端点」的流程(上图SOF+IN+NAK包 解释))

[1. 第 1 包:SOF 帧同步包](#1. 第 1 包:SOF 帧同步包)

[2. 第 2 包:IN 令牌包](#2. 第 2 包:IN 令牌包)

[3. 第 3 包:NAK 响应包](#3. 第 3 包:NAK 响应包)

[整体流程 + 与bInterval=5的关系](#整体流程 + 与bInterval=5的关系)

关键说明

三.USB和自定义HID类配置详解

[3.1 设备描述符](#3.1 设备描述符)

设备描述符数组参考

[宏 1: USB_MAX_EP0_SIZE = 64U](#宏 1: USB_MAX_EP0_SIZE = 64U)

[宏 2: USBD_VID = 1151](#宏 2: USBD_VID = 1151)

[宏 3:USBD_PID_FS = 22351](#宏 3:USBD_PID_FS = 22351)

[宏 4/5/6:MFC_STR/PRODUCT_STR/SERIAL_STR](#宏 4/5/6:MFC_STR/PRODUCT_STR/SERIAL_STR)

[宏 7:USBD_MAX_NUM_CONFIGURATION = 1](#宏 7:USBD_MAX_NUM_CONFIGURATION = 1)

[3.2 配置描述符](#3.2 配置描述符)

[3.2.1 配置描述符数组参考](#3.2.1 配置描述符数组参考)

[3.2.2 关键缩写解释](#3.2.2 关键缩写解释)

[3.2.3 USB 描述符的通用规则](#3.2.3 USB 描述符的通用规则)

[第一部分:USB 标准配置描述符 (9 字节,数组第 0~8 位)](#第一部分:USB 标准配置描述符 (9 字节,数组第 0~8 位))

[第二部分:USB 标准接口描述符 (9 字节,数组第 9~17 位)](#第二部分:USB 标准接口描述符 (9 字节,数组第 9~17 位))

[第三部分:USB 自定义 HID 类专用描述符 (9 字节,数组第 18~26 位)](#第三部分:USB 自定义 HID 类专用描述符 (9 字节,数组第 18~26 位))

[第四部分:USB 标准端点描述符 (IN 端点,7 字节,数组第 27~33 位)](#第四部分:USB 标准端点描述符 (IN 端点,7 字节,数组第 27~33 位))

[第五部分:USB 标准端点描述符 (OUT 端点,7 字节,数组第 34~40 位)](#第五部分:USB 标准端点描述符 (OUT 端点,7 字节,数组第 34~40 位))

第六部分:配置描述符详解

[1. IN/OUT 端点的方向定义(USB 协议的要求)](#1. IN/OUT 端点的方向定义(USB 协议的要求))

[2. 端点传输类型 bmAttributes = 0x03(HID 设备的强制要求)](#2. 端点传输类型 bmAttributes = 0x03(HID 设备的强制要求))

[3. 其他端点参数解释](#3. 其他端点参数解释)

[对比2:接口描述符的 3 个关键字节全部修改(本质变化,决定设备类型)](#对比2:接口描述符的 3 个关键字节全部修改(本质变化,决定设备类型))

[3.3 自定义HID描述符和鼠标HID描述符对比](#3.3 自定义HID描述符和鼠标HID描述符对比)

[3.3.1 鼠标和自定义HID描述符对比](#3.3.1 鼠标和自定义HID描述符对比)

[STM32 官方库的「自定义 HID 标准模板」;](#STM32 官方库的「自定义 HID 标准模板」;)

[STM32 官方库的「HID鼠标模板」](#STM32 官方库的「HID鼠标模板」)

[3.3.2 HID自定义和HID鼠标对比](#3.3.2 HID自定义和HID鼠标对比)

[差异 1:USAGE_PAGE(用途页)](#差异 1:USAGE_PAGE(用途页))

[差异 2:【设备功能】功能是否被协议锁死](#差异 2:【设备功能】功能是否被协议锁死)

[差异 3:【通信能力】数据传输方向和通道](#差异 3:【通信能力】数据传输方向和通道)

[差异 4:【数据格式】数据包的长度和含义](#差异 4:【数据格式】数据包的长度和含义)

[差异 5:电脑识别和驱动](#差异 5:电脑识别和驱动)


前言

详解 :设备描述符,配置描述符,接口描述符,端点描述符,hid描述符

HID中断端点(接收/发送) 最大数据包:设置为64字节(最大)和2字节对比

全速模式下中断端点轮询间隔:5ms和1ms对比

USB协议中的:自定义HID和HID鼠标进行对比

USB全速中HID传输流程,对比设置的端点大小与轮询间隔对整个通信周期的影响;

一. 传输轮询配置

如下图,我仍然拿上篇文章:stm32 HAL配置usb全速 自定义HID类详解 进行对比

定义自定义 HID 设备接收「主机下发数据」的缓冲区长度为 64 字节。

#define USBD_CUSTOMHID_OUTREPORT_BUF_SIZE 64

指定自定义 HID 设备「报告描述符」的字节长度为 34 字节。

#define USBD_CUSTOM_HID_REPORT_DESC_SIZE 34

定义自定义 HID 设备「全速模式下中断端点」的轮询间隔为 5ms。

#define CUSTOM_HID_FS_BINTERVAL 5

CUSTOM_HID_EPIN_SIZE

CUSTOM_HID_EPOUT_SIZE 定义了这些端点可以处理的最大数据包大小(这里为2字节)

对比

usb全速设置:轮询间隔改为 1ms(HID类最快)

HID类设备的中断端点:改为64;目前为中断端点 发送/接收 处理的最大数据包

二. Logic 2 抓包示例

2.1 抓包工具型号

我这里使用Logic 8 逻辑分析仪抓的数据;(可以同时抓数字量和模拟量)

数字量最高 100MS/S

模拟量最高抓 10MS/S,这里不需要

数字量和模拟量可以同时抓,但我抓全速USB数字量设置为100MS时,模拟量最多设置2.5M,所以没配置了,直接看数字量的USB数据解析即可,如下文

逻辑分析仪连接:

GND ------ 逻辑分析仪GND

PA12(USB D+) --------逻辑分析仪通道0

PA11(USB D-) ---------逻辑分析仪通道1

Logic软件 选择全速连接

**当全速设备连接时:**D+为高电平,D-为低电平,主机通过检测这种状态来识别设备为全速设备。

对于设备断开的情况:USB 2.0协议规定,当主机端D+或D-的电压小于0.8V,并持续Tpps(最小值为2us)时间长度,主机就认为设备已经断开;USB全速模式下D+为高、D-为低的配置用于设备识别,而这种状态的持续时间主要取决于设备插入和断开的具体过程。

2.2 USB默认空闲传输

我前面已经配置中断间隔5ms(不影响包长) #define CUSTOM_HID_FS_BINTERVAL 5

为什么是(SOF + IN + NAK) (SOF) (SOF) (SOF) (SOF + IN + NAK);这里每4ms就一包IN;

不应该是(SOF + IN + NAK) (SOF) (SOF) (SOF) (SOF) (SOF + IN + NAK):每1ms 一包SOF包,每5ms一包IN;

先明确核心结论:bInterval=5是「最小轮询间隔」,而非「绝对精准的轮询周期」,USB 主机的调度规则 + SOF 帧的特性,导致实际轮询间隔出现 1ms 偏移(4ms)是完全正常的。

2.2.1. 先拆解看到的时序(SOF 间隔 1ms)

假设帧编号为连续的数字,你看到的抓包时序如下:

时间(ms) 帧编号 抓包内容 说明
0 n SOF + IN + NAK 第 1 次轮询 IN 端点
1 n+1 仅 SOF 无 IN 包(未到轮询窗口)
2 n+2 仅 SOF 无 IN 包
3 n+3 仅 SOF 无 IN 包
4 n+4 SOF + IN + NAK 第 2 次轮询 IN 端点(间隔 4ms)

2.2.2. 为什么不是严格 5ms?核心原因有 3 个

(1)bInterval=5的本质:「最小轮询间隔」而非「精准周期」

USB 规范对全速中断端点的bInterval定义是:主机「至少」每bInterval个帧(5 帧 = 5ms)轮询一次该端点,而非 "必须精准每 5ms 轮询一次"。

  • 主机的 USB 调度器需要兼顾所有连接的 USB 设备(比如鼠标、键盘、你的 HID 设备),会在 "5ms 窗口内" 找空闲时机发起轮询;
  • 若调度器在第 4ms(4 帧)时有空余带宽,就会提前发起 IN 轮询,表现为两次 IN 包间隔 4ms,这完全符合 USB 规范(只要不超过 5ms 即可)。
(2)IN 包必须跟随 SOF 帧,只能是「整数 ms」间隔

USB 全速的 SOF 是每 1ms 一个,且主机只能在 "SOF 帧开始后" 发起 IN/OUT 令牌包(不能在两个 SOF 之间发起)。

  • 你的轮询间隔只能是 1ms、2ms、3ms... 等整数;
  • 即使主机想精准 5ms 轮询,也可能因 SOF 帧的 "边界对齐",实际变成 4ms 或 5ms(±1ms 偏移)。
(3)设备回复 NAK 会影响主机调度

你看到的设备回复是NAK(表示 "当前无数据可上传"),主机收到 NAK 后,可能会微调下一次轮询的时机:

  • 若设备多次回复 NAK,主机可能 "提前一点" 轮询(比如从 5ms 缩短到 4ms),减少等待;
  • 若设备后续回复 DATA 包,主机又会回归到接近 5ms 的轮询周期。
3. 补充:bInterval=5 的 "正确预期"

你配置bInterval=5,应该预期的是:

  • 两次 IN 轮询的间隔 **≥5ms**(或接近 5ms,±1ms);
  • 主机不会超过 5ms 才轮询(否则违反规范),但提前 1ms(4ms)是允许的;
  • 若你让设备持续上传数据(回复 DATA 而非 NAK),轮询间隔会更接近 5ms(主机调度会更规律)。
总结
  1. bInterval=5是「最小轮询间隔(5 帧 / 5ms)」,主机调度允许 ±1ms 偏移,出现 4ms 间隔是正常的;
  2. IN 令牌包只能跟随 SOF 帧发起,因此间隔必为整数 ms;
  3. 设备回复 NAK 时,主机会微调轮询时机,进一步导致间隔偏离 5ms。

如果想让轮询间隔更接近 5ms,可以让设备持续向主机发送数据(回复 DATA 包而非 NAK),主机的调度会更规律。

我现在配置中断间隔1ms(不影响发送64字节的包长)

#define CUSTOM_HID_FS_BINTERVAL 1

每间隔1ms:空闲状态为 SOF + IN + NAK

每1ms间隔为:(SOF + IN + NAK )(SOF + IN + NAK )(SOF + IN + NAK )(SOF + IN + NAK)

我直接把轮询间隔设置为1:可以看到SOF包合PIDIN包已经合在一起了;

2.3 全速USB周期性SOF (上图SOF包 解释)

全速/低速:1 ms 一个 Frame 主机每 1 ms 打一次 SOF 包(PID=SOF):

里面带 11-bit 帧号。 总线在这 1 ms 里可以塞多个事务(不同设备/端点)。

SOF (Start-of-Frame)令牌包只在"全速/高速"总线上、由主机每 1 ms(全速)或每 125 µs(高速)无论总线忙闲周期性广播 ------不存在任何条件判断,就像心跳一样永远打下去。低速总线根本没有 SOF。

低速 USB 传输中主机每 1ms 会发送一次 SOF (Start-of-Frame) 包 ,但低速设备不处理 SOF 包,而是通过Keep Alive 信号 (End of Packet) 来维持同步和防止挂起。

下图为:STM32全速USB的SOF包结构,每过1ms传输一次

SOF 包放大如下图

SOF 帧同步包:含义如下

字段 含义 发送方
SYNC USB 包的同步字段(固定 8 位,用于时钟同步) 主机
PID SOF 包类型:帧起始包(Start Of Frame) 主机
Frame #0x4C8 当前帧的编号(USB 全速每 1ms 发 1 个 SOF,用于计时 / 同步) 主机
CRC OK 0x05 包的 CRC 校验正确 -
EOP 包结束标志(End Of Packet) 主机

2.4 USB 全速模式下「主机轮询设备 IN 中断端点」的流程(上图SOF+IN+NAK包 解释)

bInterval=5表示主机每 5ms 轮询一次设备的中断端点:

前面介绍的#define CUSTOM_HID_FS_BINTERVAL 5

(SOF + IN + NAK)

这是USB 全速模式下「主机轮询设备 IN 中断端点」的流程 ,3 个包是连续的传输交互,先拆分每个包的含义 + 发送方,再结合CUSTOM_HID_FS_BINTERVAL = 5解释逻辑:

1. 第 1 包:SOF 帧同步包

字段 含义 发送方
SYNC USB 包的同步字段(固定 8 位,用于时钟同步) 主机
PID SOF 包类型:帧起始包(Start Of Frame) 主机
Frame #0x4C8 当前帧的编号(USB 全速每 1ms 发 1 个 SOF,用于计时 / 同步) 主机
CRC OK 0x05 包的 CRC 校验正确 -
EOP 包结束标志(End Of Packet) 主机

2. 第 2 包:IN 令牌包

字段 含义 发送方
SYNC 同步字段 主机
PID IN 包类型:IN 令牌(主机请求从设备读取数据,传输方向:设备→主机) 主机
Address=0x0B Endpoint=0x01 目标设备地址(0x0B)+ 目标端点(0x01,即你配置的中断 IN 端点) 主机
CRC OK 0x12 CRC 校验正确 -
EOP 包结束标志 主机

3. 第 3 包:NAK 响应包

字段 含义 发送方
SYNC 同步字段 设备
PID NAK 包类型:否定确认(表示设备当前没有数据可以发给主机 设备
EOP 包结束标志 设备

整体流程 + 与bInterval=5的关系

配置的CUSTOM_HID_FS_BINTERVAL = 5,表示主机每 5ms(5 个 SOF 帧)轮询一次该中断 IN 端点

  1. 主机每 1ms 发 1 个 SOF(所以你会看到连续的 SOF 包);
  2. 当累计到 5 个 SOF(5ms)时,主机发送IN令牌包,请求从设备的 Endpoint 0x01 读取数据;
  3. 但此时设备没有待上传的数据(比如 HID 输入报告还没准备好),所以设备回复NAK响应包,告诉主机 "现在没数据,你稍后再问"。

关键说明

  • 这 3 个包是主机与设备的一次交互回合:主机先发 SOF 同步→再发 IN 请求读数据→设备回复 NAK 表示无数据;
  • 只有IN令牌包是 "主机请求读取数据" 的指令,NAK是设备的回应,SOF 是基础同步包;
  • 若设备后续准备好数据,当主机再次发 IN 令牌包时,设备会回复 DATA 包(而非 NAK)。

三.USB和自定义HID类配置详解

3.1 设备描述符

设备描述符数组参考

cpp 复制代码
__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{
  0x12,                       /* bLength:设备描述符的固定总长度 → 18字节 (0x12=18),USB协议强制,不可改 */
  USB_DESC_TYPE_DEVICE,       /* bDescriptorType:描述符类型 → 设备描述符,宏定义值=0x01,USB协议固定值 */
  0x00,                       /* bcdUSB[低字节]:USB协议版本号 → 0x0200 = USB 2.0 协议 */
  0x02,                       /* bcdUSB[高字节]:高字节是0x02,组合起来就是 USB 2.0 */
  0x00,                       /* bDeviceClass:设备的顶层类代码 → 0x00=延迟指定类 */
  0x00,                       /* bDeviceSubClass:设备的顶层子类代码 → 0x00=无子类 */
  0x00,                       /* bDeviceProtocol:设备的顶层协议代码 → 0x00=无协议 */
  USB_MAX_EP0_SIZE,           /* bMaxPacketSize0:EP0端点的最大包长 → 就是宏定义的64字节,全速USB固定64 */
  LOBYTE(USBD_VID),           /* idVendor[低字节]:厂商ID的低8位,拆分宏USBD_VID(1151)的低字节 */
  HIBYTE(USBD_VID),           /* idVendor[高字节]:厂商ID的高8位,拆分宏USBD_VID(1151)的高字节 */
  LOBYTE(USBD_PID_FS),        /* idProduct[低字节]:产品ID的低8位,拆分宏USBD_PID_FS(22351)的低字节 */
  HIBYTE(USBD_PID_FS),        /* idProduct[高字节]:产品ID的高8位,拆分宏USBD_PID_FS(22351)的高字节 */
  0x00,                       /* bcdDevice[低字节]:设备的硬件版本号 → 0x0200 = 版本2.00 */
  0x02,                       /* bcdDevice[高字节]:版本号高字节,自定义即可,无协议限制 */
  USBD_IDX_MFC_STR,           /* iManufacturer:厂商名称字符串的索引 → 宏定义的0x01 */
  USBD_IDX_PRODUCT_STR,       /* iProduct:产品名称字符串的索引 → 宏定义的0x02 */
  USBD_IDX_SERIAL_STR,        /* iSerialNumber:设备序列号字符串的索引 → 宏定义的0x03 */
  USBD_MAX_NUM_CONFIGURATION  /* bNumConfigurations:设备支持的配置数量 → 宏定义的1 */
};
cpp 复制代码
#define USB_MAX_EP0_SIZE                                64U
#define USBD_VID     1151
#define USBD_PID_FS     22351
#define USBD_IDX_MFC_STR                               0x01U
#define USBD_IDX_PRODUCT_STR                           0x02U
#define USBD_IDX_SERIAL_STR                            0x03U
#define USBD_MAX_NUM_CONFIGURATION     1
宏 1: USB_MAX_EP0_SIZE = 64U

含义:USB 设备的 0 号控制端点 (EP0) 的单次最大传输字节数;

数值说明:64是全速 USB (FS) 的强制固定值,不能改!USB 协议规定:全速 USB 的 EP0 最大包长就是 64 字节,高速 USB 是 512 字节;

核心知识点:EP0 是所有 USB 设备的「标配端点」,每一个 USB 设备都必须有 EP0,没有例外,EP0 是「控制端点」,负责USB 枚举的所有指令交互(电脑发指令、设备回传描述符),是双向端点,也是唯一不需要在配置描述符里定义的端点。

宏 2: USBD_VID = 1151

英文全称:Vendor ID → 厂商 ID;

含义:这是你的 USB 设备的「厂商编号」,是一个16 位的数值,用来标识「这个 USB 设备是哪个公司 / 厂商生产的」;

关键说明:正规的 VID 是需要向 USB-IF 组织 付费申请的,是全球唯一的;

你这里的1151是 STM32 官方库的默认测试 VID,非正规编号,用于开发调试完全没问题,电脑能正常识别;可以自己随便改这个数值(比如改成0x0483,这是 ST 官方的正规 VID),不影响设备工作。

宏 3:USBD_PID_FS = 22351

英文全称:Product ID → 产品 ID;

含义:这是你的 USB 设备的 产品型号编号,是一个16 位的数值,由厂商自己定义,用来标识「这个 VID 厂商下的哪一款产品」;

关键说明:

VID+PID 组合起来,是 USB 设备的唯一身份标识(比如:VID=1151,PID=22351 就是你这款 USB 鼠标设备的唯一编号);

电脑识别 USB 设备、加载对应驱动,核心就是靠 VID+PID;

这个数值也是 STM32 库的默认值,你可以随便改,开发调试无影响。

补充:VID 和 PID 的组合示例比如你电脑上的罗技鼠标,VID 是罗技的编号,PID 是这款鼠标的型号编号;你的 STM32 设备,VID=1151,PID=22351,电脑就会认为这是一款编号 22351 的 1151 厂商设备」。

**宏 4/5/6:**MFC_STR/PRODUCT_STR/SERIAL_STR

USBD_IDX_MFC_STR=0x01、

USBD_IDX_PRODUCT_STR=0x02、

USBD_IDX_SERIAL_STR=0x03

英文全称:Index → 索引值,这三个宏是「USB 字符串描述符的索引编号」;

含义拆解:

USBD_IDX_MFC_STR = 0x01 → 厂商名称字符串的索引是 1;

USBD_IDX_PRODUCT_STR = 0x02 → 产品名称字符串的索引是 2;

USBD_IDX_SERIAL_STR = 0x03 → 设备序列号字符串的索引是 3;

核心知识点:

USB 的「字符串描述符」是可选的,是给人看的文本信息(比如:厂商名 ="STMicroelectronics",产品名 ="STM32 USB Mouse",序列号 ="123456789");

这些索引值0x01/0x02/0x03,就是告诉电脑:「你要读厂商名,就去读索引 1 的字符串描述符;读产品名,读索引 2 的」;

USB 协议规定:索引 0 是预留的,不能用,所以所有字符串描述符的索引都从0x01开始。

宏 7:USBD_MAX_NUM_CONFIGURATION = 1

含义:这个 USB 设备支持的「配置描述符数量」;

数值说明:1表示你的设备只有 1 套配置参数(就是你之前问的那个鼠标配置描述符);

关键说明:USB 协议允许一个设备有多个配置(比如:一个 USB 声卡可以有「耳机模式」「麦克风模式」两个配置),但 STM32 的 USB 开发中,绝大多数场景都只需要 1 个配置,这个值固定写1即可,改大了反而会增加代码复杂度,无意义。

3.2 配置描述符

3.2.1 配置描述符数组参考

代码详细参考下文

STM32 USB 库 中「全速 (FS) USB 自定义 HID 设备」的核心配置描述符数组 ,是 USB 协议中最核心的代码之一,也是 USB 设备能被电脑(主机)成功识别、正常工作的基础。这是一个按 USB 协议标准、严格按字节顺序拼接的「二进制配置描述符块」,是一个uint8_t字节数组,所有数值都是十六进制的字节值。USB 设备插入电脑(主机)后,电脑会发起「USB 枚举流程」,第一步就是读取这个描述符数组;这个数组告诉电脑所有关键信息:设备是 USB 全速设备、是 HID 类型、有几个接口、有几个数据通道(端点)、每个通道的工作方式 / 传输大小 / 轮询频率等;这个数组的字节顺序、每个字节的含义都是 USB 协议强制规定的,错 1 个字节、少 1 个字节、顺序乱 1 个字节 → USB 枚举直接失败,电脑可能识别不到这个 USB 设备。

3.2.2 关键缩写解释

USB:通用串行总线协议
FS = Full Speed: 全速 USB,传输速率 12Mbps(STM32 最常用的 USB 速率)
HID = Human Interface Device: 人机接口设备(鼠标、键盘、游戏手柄、扫码枪都是标准 HID 设备)
CUSTOM_HID: 自定义 HID 设备(区别于标准鼠标 / 键盘),最灵活的 HID 类型,也是你这段代码的核心,自定义 HID 可以自己定义收发的数据格式,既能给电脑发数据、也能接收电脑的指令,是 STM32 做 USB 外设的首选方式。
__ALIGN_BEGIN/__ALIGN_END :STM32 的专用字节对齐宏,作用是让这个字节数组在内存中按 4 字节对齐存储,避免 USB 硬件读取描述符时出现「字节错位」的致命错误,必须加。
**static:**数组只在当前文件生效,节省内存 + 防止误修改。

3.2.3 USB 描述符的通用规则

所有 USB 描述符的前 2 个字节,是【固定格式】,没有例外!
第 1 个字节: bLength → 当前描述符的总字节长度(十六进制),比如配置描述符固定是0x09(9 字节)、端点描述符固定是0x07(7 字节);
**第 2 个字节:**bDescriptorType → 当前描述符的类型标识(USB 协议给的固定值),比如配置描述符 = 0x02、接口描述符 = 0x04、端点描述符 = 0x05、HID 类描述符 = 0x21。

这个数组,是5 段描述符「顺序拼接」而成 ,USB 协议强制要求这个拼接顺序,不能打乱:

标准配置描述符 (9 字节) → 标准接口描述符 (9 字节) → 自定义 HID 类描述符 (9 字节) → IN 端点描述符 (7 字节) → OUT 端点描述符 (7 字节)

总长度:9+9+9+7+7 = 41 字节(对应代码最后的注释/* 41 */)

注意:所有0xXX都是单字节的十六进制值,USB 协议中「双字节数值」都是小端序(低字节在前,高字节在后),这是 USB 协议的强制要求,

cpp 复制代码
/* USB CUSTOM_HID device FS Configuration Descriptor */
__ALIGN_BEGIN static uint8_t USBD_CUSTOM_HID_CfgFSDesc[USB_CUSTOM_HID_CONFIG_DESC_SIZ] __ALIGN_END =
{
  0x09, /* bLength: Configuration Descriptor size */
  USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType: Configuration */
  USB_CUSTOM_HID_CONFIG_DESC_SIZ,
  /* wTotalLength: Bytes returned */
  0x00,
  0x01,         /*bNumInterfaces: 1 interface*/
  0x01,         /*bConfigurationValue: Configuration value*/
  0x00,         /*iConfiguration: Index of string descriptor describing
  the configuration*/
  0xC0,         /*bmAttributes: bus powered */
  0x32,         /*MaxPower 100 mA: this current is used for detecting Vbus*/

  /************** Descriptor of CUSTOM HID interface ****************/
  /* 09 */
  0x09,         /*bLength: Interface Descriptor size*/
  USB_DESC_TYPE_INTERFACE,/*bDescriptorType: Interface descriptor type*/
  0x00,         /*bInterfaceNumber: Number of Interface*/
  0x00,         /*bAlternateSetting: Alternate setting*/
  0x02,         /*bNumEndpoints*/
  0x03,         /*bInterfaceClass: CUSTOM_HID*/
  0x00,         /*bInterfaceSubClass : 1=BOOT, 0=no boot*/
  0x00,         /*nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse*/
  0,            /*iInterface: Index of string descriptor*/
  /******************** Descriptor of CUSTOM_HID *************************/
  /* 18 */
  0x09,         /*bLength: CUSTOM_HID Descriptor size*/
  CUSTOM_HID_DESCRIPTOR_TYPE, /*bDescriptorType: CUSTOM_HID*/
  0x11,         /*bCUSTOM_HIDUSTOM_HID: CUSTOM_HID Class Spec release number*/
  0x01,
  0x00,         /*bCountryCode: Hardware target country*/
  0x01,         /*bNumDescriptors: Number of CUSTOM_HID class descriptors to follow*/
  0x22,         /*bDescriptorType*/
  USBD_CUSTOM_HID_REPORT_DESC_SIZE,/*wItemLength: Total length of Report descriptor*/
  0x00,
  /******************** Descriptor of Custom HID endpoints ********************/
  /* 27 */
  0x07,          /*bLength: Endpoint Descriptor size*/
  USB_DESC_TYPE_ENDPOINT, /*bDescriptorType:*/

  CUSTOM_HID_EPIN_ADDR,     /*bEndpointAddress: Endpoint Address (IN)*/
  0x03,          /*bmAttributes: Interrupt endpoint*/
  CUSTOM_HID_EPIN_SIZE, /*wMaxPacketSize: 2 Byte max */
  0x00,
  CUSTOM_HID_FS_BINTERVAL,          /*bInterval: Polling Interval */
  /* 34 */

  0x07,          /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_ENDPOINT, /* bDescriptorType: */
  CUSTOM_HID_EPOUT_ADDR,  /*bEndpointAddress: Endpoint Address (OUT)*/
  0x03, /* bmAttributes: Interrupt endpoint */
  CUSTOM_HID_EPOUT_SIZE,  /* wMaxPacketSize: 2 Bytes max  */
  0x00,
  CUSTOM_HID_FS_BINTERVAL,  /* bInterval: Polling Interval */
  /* 41 */
};
第一部分:USB 标准配置描述符 (9 字节,数组第 0~8 位)

USB 标准配置描述符 (9 字节,数组第 0~8 位)

cpp 复制代码
0x09,        // bLength: 配置描述符的固定长度 → 9字节(USB协议强制)
USB_DESC_TYPE_CONFIGURATION, // bDescriptorType: 类型=配置描述符(宏定义值=0x02)
USB_CUSTOM_HID_CONFIG_DESC_SIZ, // wTotalLength[低字节]: 本配置描述符的总字节数(41)
0x00,        // wTotalLength[高字节]: 总长度高字节,41是单字节,所以填0
0x01,        // bNumInterfaces: 本设备只有【1个USB接口】(HID设备固定1个)
0x01,        // bConfigurationValue: 配置编号=1(主机枚举时会选中这个配置)
0x00,        // iConfiguration: 无配置相关的字符串描述符(填0即可)
0xC0,        // bmAttributes: 设备供电属性 + 特性(重中之重!)
0x32,        // MaxPower: 设备最大功耗(单位:2mA/1个单位)

bmAttributes = 0xC0:二进制是1100 0000,USB 协议中这个字节的位定义:

第 7 位(最高位):必须为1 → 表示总线供电(设备的供电来自 USB 线的 5V,STM32 USB 设备基本上都是总线供电);

第 6 位:1= 自供电,0= 无自供电 → 这里是 0;

第 5 位:1= 支持远程唤醒,0= 不支持 → 这里是 0;

低 5 位:固定填 0;→ 总结:0xC0 = 总线供电 + 不自供电 + 不支持远程唤醒(STM32 HID 设备的标准配置)。

MaxPower = 0x32:0x32=50,USB 协议规定这个值的单位是 2mA → 50 × 2mA = 100mA。→ 含义:告诉主机,这个 USB 设备最多从 USB 总线取 100mA 电流,主机据此分配供电,这个值是 STM32 USB 的「标配值」,不用改。

第二部分:USB 标准接口描述符 (9 字节,数组第 9~17 位)

紧跟配置描述符,告诉主机:当前 USB 接口的类型、功能、使用的端点数量,HID 设备的核心接口定义都在这里,注释标注/* 09 */

cpp 复制代码
0x09,        // bLength: 接口描述符固定长度 →9字节
USB_DESC_TYPE_INTERFACE, // bDescriptorType: 类型=接口描述符(宏定义值=0x04)
0x00,        // bInterfaceNumber: 接口编号=0(只有1个接口,编号从0开始)
0x00,        // bAlternateSetting: 无备用接口(填0即可)
0x02,        // bNumEndpoints: 该接口使用【2个端点】(IN+OUT,核心值!)
0x03,        // bInterfaceClass: 接口类型=HID类(USB协议固定值=0x03,重中之重!)
0x00,        // bInterfaceSubClass: 子类=0 → 自定义HID(1=标准HID/键鼠/鼠标)
0x00,        // bInterfaceProtocol: 协议=0 → 无标准协议(1=键盘,2=鼠标)
0,           // iInterface: 无接口相关字符串描述符(填0即可)

本部分关键字节解释(重中之重,一个都不能错)

bNumEndpoints = 0x02:当前接口用2 个端点,正好对应后面的「IN 端点 + OUT 端点」,数量必须和后面的端点描述符数量一致!

bInterfaceClass = 0x03:USB 协议的绝对固定值!只要是 HID 设备(不管标准 / 自定义),这个值必须是0x03,改了这个值,电脑就不会把设备识别为 HID,枚举直接失败!

bInterfaceSubClass=0x00 + bInterfaceProtocol=0x00:组合起来表示「自定义 HID 设备」,没有遵循键鼠 / 鼠标的标准协议,这也是你代码里CUSTOM_HID的核心体现。如果是标准键盘,这里会是0x01+0x01,标准鼠标是0x01+0x02。

第三部分:USB 自定义 HID 类专用描述符 (9 字节,数组第 18~26 位)

这是HID 设备独有的描述符 ,告诉主机:遵循的 HID 协议版本、HID 的核心配置、报告描述符的信息,注释标注/* 18 */

cpp 复制代码
0x09,        // bLength: HID类描述符固定长度 →9字节
CUSTOM_HID_DESCRIPTOR_TYPE, // bDescriptorType: 类型=HID类描述符(宏定义值=0x21)
0x11,        // bHID[低字节]: HID协议版本号 → 0x0111 = HID 1.11规范
0x01,        // bHID[高字节]: 协议版本高字节
0x00,        // bCountryCode: 硬件目标国家码 → 0=无地域限制(通用)
0x01,        // bNumDescriptors: 跟随的HID类描述符数量 →1个(固定值)
0x22,        // bDescriptorType: 跟随的描述符类型=报告描述符(USB协议固定值=0x22)
USBD_CUSTOM_HID_REPORT_DESC_SIZE, // wItemLength[低字节]: HID报告描述符的总长度
0x00,        // wItemLength[高字节]: 报告描述符长度高字节(一般长度<256,填0)

本部分关键字节解释(核心)

**0x11 0x01:**组合是0x0111 → 表示遵循 USB HID 1.11 协议规范,这是目前最通用的 HID 协议版本,不用改。

**0x22:**USB 协议固定值,代表(接下来的长度描述的是【HID 报告描述符】的大小)。

HID 报告描述符:这是自定义 HID 的(灵魂),你的这段代码只是配置描述符,报告描述符是另一个独立的数组,它的作用是(定义 HID 设备的收发数据格式):比如一次发多少字节、每个字节的含义、是输入还是输出数据等。USBD_CUSTOM_HID_REPORT_DESC_SIZE就是这个报告描述符的字节长度。

第四部分:USB 标准端点描述符 (IN 端点,7 字节,数组第 27~33 位)

端点(Endpoint)是 USB 设备的数据通道,是 USB 通信的「最小单位」,所有 USB 数据收发都必须通过端点完成。

USB 的端点是「单向的」,所以需要分IN和OUT两个端点,注释标注/* 27 */,端点描述符的固定长度是7 字节(0x07),所有端点都遵循这个格式。

cpp 复制代码
0x07,        // bLength: 端点描述符固定长度 →7字节
USB_DESC_TYPE_ENDPOINT, // bDescriptorType: 类型=端点描述符(宏定义值=0x05)
CUSTOM_HID_EPIN_ADDR,   // bEndpointAddress: IN端点地址(核心!)
0x03,        // bmAttributes: 端点的传输类型(USB协议固定值,重中之重!)
CUSTOM_HID_EPIN_SIZE,   // wMaxPacketSize[低字节]: IN端点一次最大传输字节数
0x00,        // wMaxPacketSize[高字节]: 最大传输字节数高字节(填0)
CUSTOM_HID_FS_BINTERVAL,// bInterval: 端点轮询间隔(单位:ms,全速USB专属)
第五部分:USB 标准端点描述符 (OUT 端点,7 字节,数组第 34~40 位)

和 IN 端点描述符格式完全一致,只是「端点地址」不同,注释标注/* 34 */

cpp 复制代码
0x07,        // bLength: 端点描述符固定长度 →7字节
USB_DESC_TYPE_ENDPOINT, // bDescriptorType: 类型=端点描述符(0x05)
CUSTOM_HID_EPOUT_ADDR,  // bEndpointAddress: OUT端点地址(核心!)
0x03,        // bmAttributes: 端点的传输类型(和IN端点一致=0x03)
CUSTOM_HID_EPOUT_SIZE,  // wMaxPacketSize[低字节]: OUT端点一次最大传输字节数
0x00,        // wMaxPacketSize[高字节]: 填0
CUSTOM_HID_FS_BINTERVAL,// bInterval: 轮询间隔(和IN端点一致)
第六部分:配置描述符详解
1. IN/OUT 端点的方向定义(USB 协议的要求)

USB 的方向,永远是**相对于电脑(主机)**而言的,不是相对于 STM32!

CUSTOM_HID_EPIN_ADDR:IN 端点 → 数据流向:STM32(设备) → 电脑(主机)作用:STM32 主动给电脑发数据(比如:采集的传感器数据、按键状态、串口数据转发等),这是「上报数据」。宏定义值一般是0x81(地址 1,IN 方向),USB 中 IN 端点的地址最高位固定是 1(二进制)。

CUSTOM_HID_EPOUT_ADDR:OUT 端点 → 数据流向:电脑(主机) → STM32(设备)作用:电脑主动给 STM32 发指令(比如:控制 LED 亮灭、设置参数、控制电机等),这是「接收指令」。宏定义值一般是0x01(地址 1,OUT 方向),USB 中 OUT 端点的地址最高位固定是 0(二进制)。

2. 端点传输类型 bmAttributes = 0x03(HID 设备的强制要求)

这个字节的低 2 位定义传输类型,0x03 = 二进制0000 0011 → 代表:中断传输 (Interrupt Transfer)。核心规则:所有 HID 设备(标准 / 自定义)的端点,只能使用【中断传输】,不能用其他传输方式!

为什么用中断传输?中断传输的特点是「固定频率轮询、低延迟、可靠」,适合传输少量的、实时性要求高的数据(比如按键、鼠标移动、传感器值),完美匹配 HID 设备的需求;

USB 的其他传输方式:批量传输(U 盘)、同步传输(声卡)、控制传输(枚举用),都不能用在 HID 端点上,改了就枚举失败。

3. 其他端点参数解释

CUSTOM_HID_EPIN_SIZE / CUSTOM_HID_EPOUT_SIZE:代码里是0x02 → 代表每个端点一次最多传输 2 字节数据。这个值可以改(全速 USB 最大 64 字节),按需调整即可。

CUSTOM_HID_FS_BINTERVAL:轮询间隔,全速 USB 下单位是毫秒 (ms),常用值是0x0A(10ms) → 含义:电脑会每隔 10ms 主动查询一次这个端点,看有没有数据要收发。这个值越小,实时性越高,一般填 10ms 足够。这里使用1ms 和 5ms 进行测试;

如果我这样子修改

对比1:参考自定义hid 和 鼠标类的对比( 新增「远程唤醒」功能**)**

bmAttributes = 0xC0 变为 0XE0

二进制:0xE0 = 1110 0000,USB 协议这个字节的位规则不变;

变化仅在第 5 位:从0→1,代表该 USB 设备 新增「远程唤醒」功能;

其他不变:第 7 位还是1(总线供电,靠 USB 5V 供电),第 6 位还是0(无自供电);

通俗讲:这个鼠标设备可以主动唤醒休眠的电脑(USB 标准功能,不影响核心使用)。

补充:0x32(MaxPower 100mA) 没改,功耗不变。

对比2:接口描述符的 3 个关键字节全部修改(本质变化,决定设备类型)
cpp 复制代码
// 原配置(自定义HID):
0x02,         // bNumEndpoints: 2个端点(IN+OUT双向)
0x00,         // bInterfaceSubClass : 0=自定义HID
0x00,         // nInterfaceProtocol : 0=无标准协议

// 新对比配置(鼠标):
0x01,         /*bNumEndpoints: 1个端点 【改1】*/
0x03,         /*bInterfaceClass: HID 【不变】*/
0x01,         /*bInterfaceSubClass : 1=BOOT子类 【改2】*/
0x02,         /*nInterfaceProtocol : 2=标准鼠标协议 【改3】*/

bNumEndpoints = 0x01:端点数量从2 个 → 1 个

原自定义 HID:2个端点(IN+OUT),双向通信 → 既能 STM32 发数据给电脑,也能电脑发指令给 STM32;

新标准鼠标:1个端点(仅IN),单向通信 → 只有 STM32 → 电脑 这一个方向,电脑不会给这个鼠标发任何数据;

原因:USB 标准鼠标的逻辑就是「只上报鼠标状态(移动、按键)给电脑,不需要接收电脑的指令」,所以 OUT 端点完全不需要,直接删掉。

bInterfaceSubClass = 0x01:子类从0(自定义) → 1(BOOT)

USB 协议定义:0x00 = 自定义 HID 设备(无标准规范,自己定义数据格式);

0x01 = BOOT 类 HID 设备(标准启动类设备,仅键鼠);

关键特性:BOOT 类鼠标 / 键盘,电脑的 BIOS 阶段就能识别,开机进系统前就能用,优先级远高于自定义 HID,这是标准键鼠的专属标识。

bInterfaceProtocol = 0x02:协议类型从0(无) → 2(标准鼠标)

USB 协议固定值:0x00= 无协议、0x01= 标准键盘、0x02= 标准鼠标;

改完这个值,电脑会直接调用系统自带的鼠标驱动,免装任何驱动,即插即用,这是标准鼠标的核心特征!

结论:

把原来的自定义 HID 设备 改成了 USB 协议标准的BOOT 类鼠标设备带摇杆功能的标准鼠标,当然鼠标后续的报告描述符和端点长度这些也需要需要修改,后续有机会介绍

3.3 自定义HID描述符和鼠标HID描述符对比

3.3.1 鼠标和自定义HID描述符对比

前面的设备描述符和配置描述符没有太大的差别,HID设备最重要的是HID描述符以下是官方的自定义HID描述符和鼠标HID描述符;

STM32 官方库的「自定义 HID 标准模板」;

描述符是 「对称双向 64 字节」,输入和输出的格式完全一致,这是自定义 HID 最常用的写法,兼容性最好,开发也最方便。

cpp 复制代码
0x06, 0x00, 0xff,  // 声明:厂商自定义设备,脱离标准类
0x09, 0x01,        // 声明:自定义用途ID=1
0xa1, 0x01,        // 开始:应用级集合(HID逻辑的总入口)

// ↓↓↓ 设备→电脑:64字节输入报告 ↓↓↓
0x09, 0x01,        // 关联自定义用途ID
0x15, 0x00,        // 数据最小值:0
0x26, 0xff, 0x00,  // 数据最大值:255(单字节无符号数)
0x75, 0x08,        // 每个数据项8bit=1字节
0x95, 0x40,        // 共64个数据项 → 64字节
0x81, 0x02,        // 输入属性:可读写的有效数据,绝对值

// ↓↓↓ 电脑→设备:64字节输出报告 ↓↓↓
0x09, 0x01,        // 关联自定义用途ID
0x15, 0x00,        // 数据最小值:0
0x26, 0xff, 0x00,  // 数据最大值:255
0x75, 0x08,        // 每个数据项8bit=1字节
0x95, 0x40,        // 共64个数据项 → 64字节
0x91, 0x02,        // 输出属性:可读写的有效数据,绝对值

0xC0               // 结束:应用级集合(闭合逻辑)
STM32 官方库的「HID鼠标模板」
cpp 复制代码
#define HID_MOUSE_REPORT_DESC_SIZE    74U
// 标准USB HID鼠标报告描述符 (74字节),支持左键/右键/中键 + X轴/Y轴移动 + 滚轮滚动 + 预留扩展功能位
// 遵循USB HID 1.11协议规范,电脑免驱识别为标准USB鼠标,BIOS级兼容,配套4字节鼠标数据包
__ALIGN_BEGIN static uint8_t HID_MOUSE_ReportDesc[HID_MOUSE_REPORT_DESC_SIZE]  __ALIGN_END =
{
    0x05, 0x01,        // USAGE_PAGE (Generic Desktop)        定义用途页:通用桌面设备(协议固定值)
    0x09, 0x02,        // USAGE (Mouse)                       定义设备类型:鼠标(协议固定值)
    0xA1, 0x01,        // COLLECTION (Application)            开始定义应用级集合,总逻辑体开始,与最后0xC0配对
    0x09, 0x01,        // USAGE (Pointer)                     定义子设备:指针(鼠标的核心指针功能)

    0xA1, 0x00,        // COLLECTION (Physical)               开始定义物理级集合,鼠标硬件功能体开始,与第45行0xC0配对
    0x05, 0x09,        // USAGE_PAGE (Button)                 定义用途页:按键功能
    0x19, 0x01,        // USAGE_MINIMUM (Button 1)            定义最小按键编号:左键(按键1)
    0x29, 0x03,        // USAGE_MAXIMUM (Button 3)            定义最大按键编号:中键(按键3)

    0x15, 0x00,        // LOGICAL_MINIMUM (0)                 按键逻辑最小值:0(按键松开状态)
    0x25, 0x01,        // LOGICAL_MAXIMUM (1)                 按键逻辑最大值:1(按键按下状态)
    0x95, 0x03,        // REPORT_COUNT (3)                    报告数据个数:3个(对应左/右/中键)
    0x75, 0x01,        // REPORT_SIZE (1)                     每个报告数据位宽:1bit (1个按键占1bit)

    0x81, 0x02,        // INPUT (Data,Var,Abs)                输入报告属性:数据位、可变值、绝对值 | 3个按键共占3bit
    0x95, 0x01,        // REPORT_COUNT (1)                    报告数据个数:1组
    0x75, 0x05,        // REPORT_SIZE (5)                     每个报告数据位宽:5bit
    0x81, 0x01,        // INPUT (Cnst,Arr,Abs)                输入报告属性:常量、数组、绝对值 | 5bit填充位(凑满1字节,无实际意义)

    0x05, 0x01,        // USAGE_PAGE (Generic Desktop)        重新定义用途页:通用桌面设备
    0x09, 0x30,        // USAGE (X)                           定义X轴移动 (协议固定值:0x30=X轴)
    0x09, 0x31,        // USAGE (Y)                           定义Y轴移动 (协议固定值:0x31=Y轴)
    0x09, 0x38,        // USAGE (Wheel)                       定义滚轮滚动 (协议固定值:0x38=滚轮)

    0x15, 0x81,        // LOGICAL_MINIMUM (-127)              逻辑最小值:0x81=-127 (左移/上移/滚轮下滚)
    0x25, 0x7F,        // LOGICAL_MAXIMUM (127)               逻辑最大值:0x7F=+127 (右移/下移/滚轮上滚)
    0x75, 0x08,        // REPORT_SIZE (8)                     每个轴/滚轮数据位宽:8bit=1字节
    0x95, 0x03,        // REPORT_COUNT (3)                    报告数据个数:3个(X轴+Y轴+滚轮)

    0x81, 0x06,        // INPUT (Data,Var,Rel)                输入报告属性:数据位、可变值、相对值 | 三轴数据各占1字节,共3字节
    0xC0,              // END_COLLECTION                      结束物理级集合,与第10行0xA1 0x00配对
    0x09, 0x3C,        // USAGE (System Control)              定义用途:系统控制功能(预留扩展)
    0x05, 0xFF,        // USAGE_PAGE (Vendor Defined)         定义用途页:厂商自定义区域(0xFF00)

    0x09, 0x01,        // USAGE (Vendor Usage 1)              定义厂商自定义用途ID:1
    0x15, 0x00,        // LOGICAL_MINIMUM (0)                 自定义数据最小值:0
    0x25, 0x01,        // LOGICAL_MAXIMUM (1)                 自定义数据最大值:1
    0x75, 0x01,        // REPORT_SIZE (1)                     自定义数据位宽:1bit

    0x95, 0x02,        // REPORT_COUNT (2)                    自定义数据个数:2bit (预留2个功能位)
    0xB1, 0x22,        // FEATURE (Data,Var,Abs,NVol,NPref)   特征报告属性:自定义功能位为可读写数据
    0x75, 0x06,        // REPORT_SIZE (6)                     填充位宽:6bit
    0x95, 0x01,        // REPORT_COUNT (1)                    填充位个数:1组

    0xB1, 0x01,        // FEATURE (Cnst,Arr,Abs,NVol,NPref)   特征报告属性:6bit填充位,无实际意义
    0xC0               // END_COLLECTION                      结束应用级集合,与第6行0xA1 0x01配对,描述符结束
};

3.3.2 HID自定义和HID鼠标对比

**差异 1:**USAGE_PAGE(用途页)

差异 1:【协议根基】核心指令 USAGE_PAGE(用途页) 完全不同(绝对核心!0 容错)

这是第一个字节就体现的差异,也是 USB 协议里「区分标准 / 自定义」的铁律,改这个字节,设备身份立刻变!

标准鼠标描述符 → 第一行指令:0x05, 0x01

**含义:**告诉电脑「我属于 USB 标准化的『通用桌面设备』家族」,这个家族里只有鼠标、键盘、游戏手柄、摇杆等常见外设,所有设备的指令都是标准化的;

**取值:**0x01 是 USB 协议强制固定值,所有标准键鼠都必须写这个值。

自定义 HID 描述符 → 第一行指令:0x06, 0x00, 0xFF

**含义:**告诉电脑「我不属于任何 USB 标准化的设备家族,我是厂商自己定义的私有设备」,0xFF00 是 USB 协议为厂商预留的私有用途页地址,全世界所有自定义 HID 设备都用这个值;

**取值:**0xFF00 是 USB 协议的固定预留值,所有自定义 HID 都必须这么写,这是「自定义」的官方入口。

**补充铁律:**USAGE_PAGE 是 HID 描述符的「根指令」,它决定了后续所有指令的「归属和含义」,标准鼠标的指令在0x01页里有效,自定义的指令在0xFF00页里有效,二者完全互斥。

差异 2:【设备功能】功能是否被协议锁死

差异 2:【设备功能】功能是否被协议锁死 → 标准鼠标「固定死」,自定义「无限自由」

标准鼠标描述符 → 功能是 USB 协议强制固定的,你只能用、不能改、不能加、不能减

能实现的功能:永远只有「左键 / 右键 / 中键 + X 轴移动 + Y 轴移动 + 滚轮」,这是鼠标的全部功能,协议里写死了;

数据的含义:每个字节、每个 bit 的含义都是固定的,比如 Byte0 的 bit0 一定是左键,Byte1 一定是 X 轴,改了电脑就识别不了;

**举个例子:**你想让鼠标的 Byte1 变成「控制 LED 亮灭」,不行!电脑依然会把它当成 X 轴移动,结果就是「LED 没反应,鼠标乱飞」。

自定义 HID 描述符 → 功能是 你说了算,无限自由,无任何限制

能实现的功能: 想定义什么功能,就定义什么功能,比如: 上报传感器数据(温湿度、加速度、电压电流); 上报按键 / 摇杆 / 旋钮的状态; 接收电脑下发的指令(控制 LED、蜂鸣器、电机、舵机);

数据的含义:每个字节、每个 bit 的含义,由你自己定义,电脑不会解析,只会原封不动的收发,数据的含义只有你自己知道;

**举个例子:**你定义 Byte0 的 bit0 是「LED1 亮灭」,bit1 是「LED2 亮灭」,Byte1 是「电机转速」,完全没问题,USB 协议不会有任何意见。

差异 3:【通信能力】数据传输方向和通道

差异 3:【通信能力】数据传输方向 & 通道 → 鼠标是「单向残废」,自定义是「双向满血」

这是开发中最实用的差异,也是改配置描述符时「端点数量变化」的根本原因;

标准鼠标描述符 → 仅支持【单向通信】:设备 → 主机 (只有 IN 端点)

**通信逻辑:**鼠标的本质是「只上报自己的状态给电脑」,电脑只需要「接收鼠标数据」,不需要「给鼠标发指令」;

**描述符特征:**描述符里只有 INPUT 指令,没有任何 OUTPUT 指令;

配置描述符匹配:只需要1 个 IN 中断端点,前面鼠标配置描述符里 bNumEndpoints=0x01;

**核心限制:**你永远只能给电脑发数据,电脑永远不能给你发任何指令,想做「电脑控制 STM32 的 LED / 电机」,用标准鼠标描述符完全做不到。

自定义 HID 描述符 → 支持【双向全双工通信】:设备 ↔ 主机 (IN+OUT 双端点)

**通信逻辑:**自定义 HID 的设计初衷就是「自由通信」,既可以让 STM32 主动给电脑发数据(上报),也可以让电脑主动给 STM32 发数据(下发指令),二者互不干扰,同时进行;

**描述符特征:**前面自定义HID代码里同时有 INPUT 和 OUTPUT 两段完整的指令,是「输入报告 + 输出报告」的完整组合;

0x81,0x02 → INPUT:设备→主机,STM32 给电脑发 64 字节自定义数据;

0x91,0x02 → OUTPUT:主机→设备,电脑给 STM32 发 64 字节自定义数据;

**配置描述符匹配:**必须用2 个中断端点(IN+OUT),所以你最开始的配置描述符里 bNumEndpoints=0x02;

**核心优势:**既能上报数据,又能接收指令,这是自定义 HID 最核心的价值,也是绝大多数 STM32 USB 项目选择自定义 HID 的原因。

差异 4:【数据格式】数据包的长度和含义

差异 4:【数据格式】数据包的长度含义 → 鼠标「固定死」,自定义「自己定」

这是开发中最直接的差异,也是写代码时「最直观的感受」,和前面的配置描述符里 HID_EPIN_SIZE/CUSTOM_HID_EPIN_SIZE 强绑定!

标准鼠标描述符 → 数据包格式是 USB 协议强制固定,字节数不可改

精简版 50 字节描述符(拿描述符生成器DT.exe生成的) → 固定3 字节数据包:按键位 (1) + X 轴 (1) + Y 轴 (1);

完整版 74 字节描述符(stm32cubemx生成,参考这个) → 固定4 字节数据包:按键位 (1) + X 轴 (1) + Y 轴 (1) + 滚轮 (1);

**数据含义:**每个字节的含义是全世界统一的,比如 Byte0 的 bit0 = 左键,Byte1=X 轴,改一个字节就会出问题;

**字节数限制:**鼠标的数据包永远只有 3/4 字节,想多发一个字节都不行,协议不允许。

自定义 HID 描述符 → 数据包格式是 你自己定义,字节数按需设置

代码里定义的是 双向 64 字节数据包(已经设置为最大了),这个数值是你自己定的,核心指令解析:

cpp 复制代码
0x75,0x08  // REPORT_SIZE (8) → 每个数据项占8bit=1字节
0x95,0x40  // REPORT_COUNT (64) → 一共64个数据项
// 数据包总长度 = 8 × 64 = 64字节

**灵活性:**想改成「双向 1 字节、双向 8 字节、双向 32 字节」都可以,只需要改 0x95 的值即可,无任何协议限制;

**数据含义:**这 64 个字节里,第 1 字节存什么、第 2 字节存什么,完全由自己决定,比如: 第 1 字节:按键状态(bit0 = 按键 1,bit1 = 按键 2);第 2-5 字节:温湿度传感器数据;第 6-9 字节:摇杆的 X/Y 轴数值;剩下的字节:预留备用;

**核心特点:**数据是「裸数据」,电脑不会解析,只会原封不动的传给你的上位机软件,由你自己写代码解析。

差异 5:电脑识别和驱动

差异 5:【电脑识别 & 驱动】→ 鼠标「免驱即用」,自定义「无驱动,需写上位机」

这是使用层面最核心的差异,也是「标准 HID」和「自定义 HID」最直观的体验区别,没有之一!

标准鼠标描述符 → 【极致友好】电脑自动识别 + 自动加载驱动

**电脑识别:**插入 USB 后,电脑的设备管理器里会显示 「鼠标和其他指针设备」→「USB 鼠标」;

**驱动:**完全不需要你写任何驱动,Windows/Linux/Mac 都自带鼠标驱动,即插即用,无需任何额外操作;

**数据解析:**电脑自动解析鼠标数据包,直接转换成「鼠标移动、按键点击」的系统事件,你不需要写任何上位机软件。

自定义 HID 描述符 → 【需要开发】电脑仅识别为 HID 设备,无驱动,需自己写上位机

**电脑识别:**插入 USB 后,电脑的设备管理器里会显示 「人体学输入设备」→「USB 输入设备」,名字是通用的,没有具体设备名;

驱动:Windows 会自动加载一个「通用 HID 驱动」,保证数据能收发,但这个驱动不会解析任何数据,只是一个「数据通道」;

**数据解析:**我这里使用免费的PortHelper1.8.0软件收发数据 或者 自己写上位机软件(比如用 C#/Python/Qt/ 串口助手),通过 HID 通信库读取 / 写入这 64 字节的裸数据,然后在软件里解析数据含义、实现功能(比如显示传感器数据、发送控制指令)。

**补充:**这不是缺点,而是自定义 HID 的「特性」------ 正因为没有系统驱动的限制,你才能实现完全自定义的功能,这是标准 HID 做不到的。

相关推荐
小何code16 小时前
STM32入门教程,第10课(下),Keil调试模式
stm32·单片机·嵌入式硬件
Y1rong19 小时前
STM32之中断
stm32·单片机·嵌入式硬件
先知后行。19 小时前
STM32F103的启动过程
stm32·单片机·嵌入式硬件
idcardwang20 小时前
xl9555-IO拓展芯片
stm32·单片机·嵌入式硬件
Y1rong20 小时前
STM32之EXTI
stm32·单片机·嵌入式硬件
兆龙电子单片机设计20 小时前
【STM32项目开源】STM32单片机智能语音家居控制系统
stm32·单片机·嵌入式硬件·物联网·开源·自动化
意法半导体STM3221 小时前
【官方原创】SAU对NSC分区的影响 LAT1578
stm32·单片机·嵌入式硬件·mcu·信息安全·trustzone·stm32开发
SmartRadio21 小时前
MK8000(UWB射频芯片)与DW1000的协议适配
c语言·开发语言·stm32·单片机·嵌入式硬件·物联网·dw1000
LDR00621 小时前
芯片电路的引脚标识代表什么?
stm32·单片机·嵌入式硬件