附录02 USB HID协议
约 3418 字大约 11 分钟
2026-03-20
1. USB 枚举过程

我们将一个USB设备插入Windows时,Windows识别这个设备的过程,称为USB枚举。
简单来说,当一个USB设备插入Host时,Host会先后获取设备的设备描述符和配置描述符。有了这两者,Host就能知道如何给一个USB设备供电,如何与其交换数据。
设备描述符
你可以把它理解为USB设备的“身份证”,里面记录了设备的基本信息,比如生产厂商是谁(VID)、产品型号(PID)、支持哪种USB版本、设备属于哪一类(比如是U盘还是摄像头)。主机一拿到这个描述符,就知道“插进来的是个什么东西”。
windows系统中设备描述符的结构体定义如下:
struct _DEVICE_DESCRIPTOR_STRUCT
{
BYTE bLength; //设备描述符的字节数大小,为0x12
BYTE bDescriptorType; //描述符类型编号,为0x01
WORD bcdUSB; //USB版本号
BYTE bDeviceClass; //USB分配的设备类代码,0x01~0xfe为标准设备类,0xff为厂商自定义类型
//0x00不是在设备描述符中定义的,如HID
BYTE bDeviceSubClass; //usb分配的子类代码,同上,值由USB规定和分配的
BYTE bDeviceProtocol; //USB分配的设备协议代码,同上
BYTE bMaxPacketSize0; //端点0的最大包的大小
WORD idVendor; //厂商编号
WORD idProduct; //产品编号
WORD bcdDevice; //设备出厂编号
BYTE iManufacturer; //描述厂商字符串的索引
BYTE iProduct; //描述产品字符串的索引
BYTE iSerialNumber; //描述设备序列号字符串的索引
BYTE bNumConfiguration; //可能的配置数量
}配置描述符
它描述的是设备的一种“工作模式”。一个USB设备可以有好几种配置(比如一个设备既可以当键盘用,也可以当鼠标用),但同一时间只能激活其中一种。
配置描述符会说明激活这个模式需要多少电流、有多少个接口,可以理解为“设备准备用这种状态来工作”。
配置描述符中一般会包含接口描述符和接口关联描述符(可选)

windows系统中配置描述符的结构体定义如下:
struct _CONFIGURATION_DESCRIPTOR_STRUCT
{
BYTE bLength; //配置描述符的字节数大小,固定为9字节
BYTE bDescriptorType; //描述符类型编号,为0x02
WORD wTotalLength; //配置所返回的所有数量的大小
BYTE bNumInterface; //此配置所支持的接口数量
BYTE bConfigurationVale; //Set_Configuration命令需要的参数值
BYTE iConfiguration; //描述该配置的字符串的索引值
BYTE bmAttribute; //供电模式的选择
BYTE MaxPower; //设备从总线提取的最大电流
}CONFIGURATION_DESCRIPTOR_STRUCT接口描述符
它代表USB设备里的一个“功能单元”。比如一个带麦克风的耳机,控制音量的功能和传输声音的功能就是两个不同的接口,每个接口描述符会说明这个功能属于什么类型(音频、键盘等)、需要用到几个端点(数据通道),相当于告诉主机“设备提供了这个独立的功能”。
接口描述符中会包含端点描述符。
windows系统中接口描述符的结构体定义如下:
struct _INTERFACE_DESCRIPTOR_STRUCT
{
BYTE bLength; //设备描述符的字节数大小,为0x09
BYTE bDescriptorType; //描述符类型编号,为0x04
BYTE bInterfaceNumber; //接口的编号
BYTE bAlternateSetting; //备用的接口描述符编号
BYTE bNumEndpoints; //该接口使用端点数,不包括端点0
BYTE bInterfaceClass; //接口类型
BYTE bInterfaceSubClass;//接口子类型
BYTE bInterfaceProtocol;//接口所遵循的协议
BYTE iInterface; //描述该接口的字符串索引值
}INTERFACE_DESCRIPTOR_STRUCT接口关联描述符
当多个接口要一起配合才能实现一个完整功能时,就用这个描述符把它们“打包”成一个整体。比如耳机的控制接口和音频流接口其实是同一个设备功能的两部分,接口关联描述符就负责把这两个接口绑定在一起,避免主机把它们当作两个互不相关的设备来分别驱动。
接口关联描述符与其它描述符的关系图:

windows系统中接口关联描述符的结构体定义如下:
typedef struct _USB_INTERFACE_ASSOCIATION_DESCRIPTOR {
UCHAR bLength; //长度为8
UCHAR bDescriptorType; //USB_INTERFACE_ASSOCIATION_DESCRIPTOR_TYPE,值为0x0b
UCHAR bFirstInterface; //第一个接口编号
UCHAR bInterfaceCount; //接口总数量
UCHAR bFunctionClass; //视频接口类代码CC_VIDEO,值0x0E
UCHAR bFunctionSubClass; //视频子类接口代码 SC_VIDEO_INTERFACE_COLLECTION,值为0x03
UCHAR bFunctionProtocol; //未用,必须为PC_PROTOCOL_UNDEFINED,值为0x00
UCHAR iFunction; //字符串描述符索引
} USB_INTERFACE_ASSOCIATION_DESCRIPTOR, *PUSB_INTERFACE_ASSOCIATION_DESCRIPTOR;端点描述符
它是USB设备里真正用来传输数据的“通道”。每个接口下面可以包含若干个端点,每个端点描述符会说明这个端点的传输方向(是数据输入还是输出)、传输方式(是实时传输音频、还是可靠传输文件)、以及每次能传多少数据。可以把它理解为设备给主机留出的一个个“收发室”,主机通过这些指定的端点来和设备的某个具体功能交换数据。
windows系统中端点描述符的结构体定义如下:
struct _ENDPOIN_DESCRIPTOR_STRUCT
{
BYTE bLength; //设备描述符的字节数大小,为0x7
BYTE bDescriptorType; //描述符类型编号,为0x05
BYTE bEndpointAddress; //端点地址及输入输出属性
BYTE bmAttribute; //端点的传输类型属性
WORD wMaxPacketSize; //端点收、发的最大包的大小
BYTE bInterval; //主机查询端点的时间间隔
} ENDPOIN_DESCRIPTOR_STRUCT ;2. USB HID协议
在上面的接口描述符中,如果这个接口是一个HID设备(bInterfaceClass为0x03)。那么它就会被识别为一个HID(Human Input Device)设备。
HID设备除了支持USB设备的5种标准描述符之外,还支持HID设备特有的3种描述符。这些描述符是:
USB标准描述符:设备描述符、配置描述符、接口描述符、端点描述符和字符串描述符。 HID特有的描述符:HID描述符、报表描述符(Report)和实体描述符(Physical)
一个HID设备的接口描述符后会紧跟一个HID描述符,这个描述符内部一般包含一个HID报表描述符,描述了这个HID设备的类型(鼠标,键盘,游戏手柄等)以及它的汇报格式。
https://www.usbzh.com/article/detail-48.html
HID 描述符
HID 描述符的主要作用是用来识别 HID 通信所使用的额外描述符。下表是HID 描述符结构。
| 偏移量 | 字段 | 字节数 | 数值类型 | 说明 |
|---|---|---|---|---|
| 0 | bLength | 1 | Numeric | 描述符字节数 |
| 1 | bDescriptorType | 1 | Constant | 0x21 = HID描述符 |
| 2 | bcdHID | 2 | Numeric | HID规范版本号(BCD) |
| 4 | bCountryCode | 1 | Numeric | 硬件设备所在国家的国家代码 |
| 5 | bNumDescriptors | 1 | Numeric | 类别描述符数目(至少有一个报表描述符) |
| 6 | 【bDescriptorType】 | 1 | Constant | 该类别描述符的类型 |
| 7 | 【wDescriptorLength】 | 2 | Numeric | 该类别描述符的总长度 |
bcdHID:设备与其描述符所遵循的HID规范的版本号码,此数值是4个16进位的BCD格式字符。例如版本1.1的bcdHID是0110h。
bCountryCode:国家的识别码。如果不说明,该字段为0。
bDescriptorType:HID描述符附属的描述符的类型(报表或实体)。每一个 HID都必须至少支持一个报表描述符。一个接口可以支持多个报表描述符,以及一个或多个实体描述符。
HID描述符的偏移量为6和7的bDescriptorType和wDescriptorLength可以重复存在多个。
HID报表描述符(HID Report Map)
下面是一段标准的USB HID键盘的报表描述符
05 01 09 06 A1 01 05 07 19 E0 29 E7 15 00 25 01
75 01 95 08 81 02 75 08 95 01 81 03 75 01 95 05
05 08 19 01 29 05 91 02 75 03 95 01 91 03 75 08
95 06 15 00 25 65 05 07 19 00 29 65 81 00 C0以下是这一端数据的解析:
| Hex | 含义 | 解析 |
|---|---|---|
| 05 01 | Usage Page (0x05) = Generic Desktop (0x01) | 选择 Generic Desktop 页,设备类型属于桌面控制 |
| 09 06 | Usage (0x09) = Keyboard (0x06) | Usage = Keyboard |
| A1 01 | Collection (0xA1) = Application (0x01) | Application Collection 开始 |
| 05 07 | Usage Page (0x05) = Keyboard (0x07) | 切换到 Keyboard Usage Page |
| 19 E0 | Usage Minimum (0x19) = 0xE0 | modifier 起始:LeftCtrl |
| 29 E7 | Usage Maximum (0x29) = 0xE7 | modifier 结束:Right GUI |
| 15 00 | Logical Minimum (0x15) = 0x00 | 逻辑最小值 0 |
| 25 01 | Logical Maximum (0x25) = 0x01 | 逻辑最大值 1 |
| 75 01 | Report Size (0x75) = 0x01 | 每个 modifier 1 bit |
| 95 08 | Report Count (0x95) = 0x08 | 8 个 modifier |
| 81 02 | Input (0x81) = 0x02 | Data(0x00) Var(0x02) Abs(0x00) → modifier 输入 |
| 75 08 | Report Size (0x75) = 0x08 | 8 bit |
| 95 01 | Report Count (0x95) = 0x01 | 1 字节 |
| 81 03 | Input (0x81) = 0x03 | Const(0x01) Var(0x02) Abs(0x00) → reserved |
| 75 01 | Report Size (0x75) = 0x01 | LED 1 bit |
| 95 05 | Report Count (0x95) = 0x05 | 5 个 LED |
| 05 08 | Usage Page (0x05) = LED (0x08) | LED Usage Page |
| 19 01 | Usage Minimum (0x19) = 0x01 | Num Lock |
| 29 05 | Usage Maximum (0x29) = 0x05 | Kana |
| 91 02 | Output (0x91) = 0x02 | Data Var Abs → LED 输出 |
| 75 03 | Report Size (0x75) = 0x03 | padding 3 bit |
| 95 01 | Report Count (0x95) = 0x01 | 1 |
| 91 03 | Output (0x91) = 0x03 | Const → padding |
| 75 08 | Report Size (0x75) = 0x08 | keycode 8 bit |
| 95 06 | Report Count (0x95) = 0x06 | 6 键 |
| 15 00 | Logical Minimum (0x15) = 0x00 | 最小键值 |
| 25 65 | Logical Maximum (0x25) = 0x65 | 最大键值 101 |
| 05 07 | Usage Page (0x05) = Keyboard (0x07) | Keyboard page |
| 19 00 | Usage Minimum (0x19) = 0x00 | 起始键值 |
| 29 65 | Usage Maximum (0x29) = 0x65 | 结束键值 |
| 81 00 | Input (0x81) = 0x00 | Data Array Abs → 6 键数组 |
| C0 | End Collection (0xC0) | Collection 结束 |
该段Report Map定义了一个输入Report一个输出Report
输入Report,共8字节
| Byte | 位 | 名称 | 说明 |
|---|---|---|---|
| 0 | 0 | Left Ctrl | modifier |
| 0 | 1 | Left Shift | modifier |
| 0 | 2 | Left Alt | modifier |
| 0 | 3 | Left GUI | modifier |
| 0 | 4 | Right Ctrl | modifier |
| 0 | 5 | Right Shift | modifier |
| 0 | 6 | Right Alt | modifier |
| 0 | 7 | Right GUI | modifier |
| 1 | 0-7 | Reserved | 保留 |
| 2 | 0-7 | Keycode 1 | 按键 |
| 3 | 0-7 | Keycode 2 | 按键 |
| 4 | 0-7 | Keycode 3 | 按键 |
| 5 | 0-7 | Keycode 4 | 按键 |
| 6 | 0-7 | Keycode 5 | 按键 |
| 7 | 0-7 | Keycode 6 | 按键 |
输出Report,共1字节
| Byte | 位 | 名称 |
|---|---|---|
| 0 | 0 | Num Lock |
| 0 | 1 | Caps Lock |
| 0 | 2 | Scroll Lock |
| 0 | 3 | Compose |
| 0 | 4 | Kana |
| 0 | 5-7 | padding |
HID 类特定请求
HID 类特定请求,是 USB HID 设备在标准控制传输基础上,定义的一套类专属控制请求,主机通过这些请求来获取 HID 报告、设置协议、控制 idle 等。它们都走控制端点 0,属于 USB 控制传输里的类特定请求。
HID Report
以上面的键盘为例,当Host获取了所有描述符后,能够识别该设备为一个键盘后,即可通过之前获取的端点与设备交互。一般HID设备的端点为中断端点,上面的键盘需要一个输入端点和一个输出端点。
假如我们在键盘上按下Ctrl + A,那么输入端点就会向主机汇报如下字节:
01 00 04 00 00 00 00 00- 01 表示左Ctrl被按下
- 04 表示A被按下
假如我们按下了Capslock,改变了主机大小写锁定状态(例如锁定大写),主机会通过输出端点发送以下字节:
02表示Capslock灯的状态为1,应该点亮,其他灯为0,应该熄灭
3. 实现步骤分析
枚举部分的实现步骤
实现步骤:
- 初始化 USB PHY、时钟、中断和 USB device controller。
- 准备设备描述符,填写 VID、PID、USB 版本、端点 0 最大包长和字符串索引。
- 准备配置描述符集合,包含配置描述符、接口描述符、HID 描述符和端点描述符。
- 准备 HID Report Descriptor,定义键盘输入报告格式。
- 准备字符串描述符,包括语言 ID、厂商名、产品名和序列号。
- 在端点 0 的 setup 请求处理函数中响应
GET_DESCRIPTOR。 - 正确处理
SET_ADDRESS,通常在 status stage 完成后再切换地址。 - 正确处理
SET_CONFIGURATION,进入 configured 状态后打开 HID 中断 IN 端点。
HID 通信的实现步骤
枚举完成后,键盘通信主要围绕 Input Report 和 Output Report。
实现步骤:
- 周期性扫描键盘矩阵,例如 1 ms 一次。
- 对每个按键做消抖,得到稳定的按下/释放状态。
- 根据 keymap 把物理键位映射成 HID keycode。
- 把 Ctrl、Shift、Alt、GUI 等修饰键填入
modifier。 - 把普通按键填入
keycode[6]。 - 如果当前报告与上一次不同,调用 HID 发送接口。
- 按键释放时必须发送空报告,否则主机会认为按键一直按下。
- 在发送完成回调里清除 busy 标志,避免上一个 IN 包还没完成就重复写端点。
