USB2.0枚举流程(以鼠标为例)——从零开始学习USB2.0协议(四)
1 总线枚举流程介绍
USB 2.0枚举的重要性在于它实现了即插即用功能,让主机自动识别、配置和管理设备,确保设备无需用户干预就能正常工作。
以下是 USB 2.0 枚举过程的概括步骤:

当一个设备连接到一个供电的端口时,将会有下列过程:
- 设备连接与电源稳定
- 设备连接到供电的USB端口,HUB检测到端口电压变化,通过其中断端点向主机报告连接事件。
- 主机发送Get_Port_Status请求给HUB以获取更多连接细节。
- HUB通过检测总线空闲时差分线的电压来判断设备速度(低速、全速或高速)。此检测在复位操作之前完成。
- 端口复位与设备速度确认
- 主机等待至少100ms,确保连接稳定和设备电源稳定。
- 主机发送Set_Port_Feature请求,要求HUB对设备端口进行复位。HUB将数据线(D+和D-)驱动为低电平,持续至少10ms。
- 复位期间,HUB不会影响其他端口。
- 对于高速设备,初始以全速运行。如果HUB支持高速,会进行高速检测(通过检查设备是否在复位过程中发出Chirp信号)。如果设备支持高速,则切换到高速模式;否则保持全速。
- 设备默认状态与通信建立
- 复位完成后,设备进入默认状态,使用默认地址0,端点0进行通信。此时设备可从总线获取最大100mA电流。
- 主机通过反复发送Get_Port_Status请求,确认复位完成。
- 初始设备描述符获取
- 主机向默认地址0发送Get_Descriptor请求,获取设备描述符。设备描述符中包含了端点0的最大包长度(位于第8字节)等重要信息。
- 首次控制传输完成后,主机会要求HUB再次对设备进行复位,使设备进入确定状态。
- 地址分配
- 主机发送Set_Address请求,为设备分配一个唯一的地址。设备使用新地址,进入地址状态。
- 详细设备信息获取
- 主机使用新地址再次发送Get_Descriptor请求,获取设备描述符(包括设备类型、VID、PID、配置个数等)。
- 主机获取配置描述符(通常为9字节),然后根据配置描述符中的配置集合总长度,获取完整的配置集合(包括配置描述符、接口描述符、端点描述符等)。
- 根据需要,主机可能还会获取字符串描述符。
- 驱动匹配与加载
- 主机解析描述符,根据设备信息选择合适的驱动程序。对于复合设备,通常需要匹配接口。
- 主机将设备添加到USB总线设备列表,USB总线遍历驱动列表,调用match函数进行匹配。匹配成功后,绑定驱动。
- 设备配置
- 设备驱动根据设备信息,发送Set_Configuration请求,选择设备的某个配置作为工作配置。
- 设备使能所选配置的接口,进入配置状态。
2 模拟鼠标的枚举实现
2.1 鼠标外设程序
程序是基于公司开发的USB IP接口,在FPGA开发板上实现了模拟鼠标的功能,通过:
1、向串口中发送指令字符
2、控制USB向windows主机发送命令,移动鼠标光标。
2.1.1 任务分析
基于对前面对USB2.0协议的理解,可以来做一个鼠标的demo,下面可以来简单规划下整个任务。
- 目标
模拟鼠标上下左右移动和左右键,用向uart输入wsad字符来代替光标移动,输入c和空格代替左右按键。
- 实现
为了实现主要在于:
1、构建描述符,解析主机设备请求,按照流程完成枚举
2、hid鼠标事件上报
2.1.2 描述符构建
鼠标本身属于HID设备的子类,和大多数的USB设备一样,HID设备也有USB设备的一些标准描述符,如设备描述符、配置描述符、接口描述符、端点找述符。
但HID设备也有一些特殊描述符,如HID描述符、报告描述符和物理描述符。
这里简单说明下,后面可能会针对HID设备进行详细介绍。

说明:
- HID设备的设备类型不是在设备类型中定义,而是在接口描述符中定义。设备描述符中的bDeviceClass和bDeviceSubClass字段不用于标识属于HID类的设备。而是在接口描述符中使用bInterfaceClass和bInterfaceSubClass字段。
- 接口描述符中的bInterfaceSubClass仅用于区分是否支持boot启动,bInterfaceProtocol也只在bInterfaceSubClass有效时用于区分boot起动的设备类型。
- HID设备在非boot模式下,设备的类型是由报告描述符来定义的。一个报告描述符可以包含多个应用(调备)类型。
- 物理描述符physical descriptor是可选的
- 报告描述符report descriptor是必须的
- 报告描述符的个数和各个报告描述符的长度在HID描述符中定义。
- 报告描述符的获取是通过发向接口的标准请求实现的。
就上面的描述来看,要实现对USB2.0鼠标的注册,就要完成设备、配置、接口、HID、报告等描述符的。
我们可以根据HID对描述符的要求来构建下。
- 设备描述符
const unsigned char DevDes[18]=
{0x12, /* bLength */0x01, /* bDescriptorType : device_descriptor */0x10,0x01, /* bcdUSB usb2.0 = 0x0200 usb1.1 = 0x0110 usb3.11 = 0x0311 */0x00, /* bDeviceClass */0x00, /* bDeviceSubClass */0x00, /* bDeviceProtocol */0x40, /* bMaxPacketSize */0x66,0x66, /* idVendor : 2 Bytes */0x88,0x88, /* idProduct : 2 Bytes */0x01,0x00, /* bcdDevice rel. 1.00 */0x01, /* Index of manufacturer string */0x02, /* Index of product string */0x00, /* Index of serial number string */0x01 /* bNumConfigurations */
};
- 配置描述符
这里将配置描述符、接口描述符、类特殊描述符、端点描述符一同放到配置描述符。
是因为正常的主机获取配置描述符过程:
1、第一次先获取9字节长度的配置描述符,然后根据配置描述符中配置集合的长度,再次获取配置描述符集合(不同主机程序可能有些差异)。
2、第二次获取的时候,设备会将配置描述符、接口描述符、类特殊描述符、端点描述符等一并返回。
后面抓包会看到该流程。
const unsigned char ConDes[9 + 9 + 9 + 7]=
{// Configuation Descriptor0x09, // bLength0x02, // bDescpriptorTypesizeof(ConDes) & 0xFF,(sizeof(ConDes)) >> 8 & 0xFF, // wTotalLength0x01, // bNumInterfaces0x01, // bConfigurationValue0x00, // iConfiguration0x80, // bmAttributes0x32, // bMaxPower : 100mA// Interface Descriptor0x09, // bLength0x04, // bDescriptorType0x00, // bInterfaceNumber0x00, // bAlternateSetting0x01, // bNumEndpoints(not include Ep0)0x03, // bInterfaceClass (Hid)0x01, // bInterfaceSubClass0x02, // bInterfaceProtocol : mouse0x00, // iInterface// HID descriptor0x09, // bLength0x21, // bDescriptorType0x11,0x01, // bcdHID : 0x01110x00, // bContryCode0x01, // bNumDescriptor0x22, // bDescriptorTypesizeof(ReportDescriptor) & 0xFF,(sizeof(ReportDescriptor) >> 8) & 0xFF, // wDescriptorLength// Endpoint Descriptor0x07, // bLength0x05, // bDescriptorType0x81, // Ep1 : In0x03, // bmAttributes : Interrupt0x04,0x00, // wMaxPackeSize : 0x00040xA // bInterval : 10ms
};
- 字符描述符
/* langDes */
const unsigned char LangDes[]=
{0x04, // bLength0x03, // bDescriptorType0x09,0x04 // wLANGID(0x0409:english)
};/* Manuf */
const unsigned char Manuf_Des[]=
{0x12,0x03,0x68, 0x00, //hezaizai0x65, 0x00,0x7A, 0x00,0x61, 0x00,0x69, 0x00,0x7A, 0x00,0x61, 0x00,0x69, 0x00,
};
/* product Des */
const unsigned char Prod_Des[]=
{0x32,0x03,0x44, 0x00, // Demo USB2.0 optical Mouse0x65, 0x00,0x6D, 0x00,0x6F, 0x00,0x20, 0x00,0x55, 0x00,0x53, 0x00,0x42, 0x00,0x32, 0x00,0x2E, 0x00,0x30, 0x00,0x20, 0x00,0x6F, 0x00,0x70, 0x00,0x74, 0x00,0x69, 0x00,0x63, 0x00,0x61, 0x00,0x6C, 0x00,0x20, 0x00,0x4D, 0x00,0x6F, 0x00,0x75, 0x00,0x73, 0x00,0x65, 0x00,
};
/* product ser */
const unsigned char SerDes[18] =
{0x12,0x03,0x32, 0x00, // 202510240x30, 0x00,0x32, 0x00,0x35, 0x00,0x31, 0x00,0x30, 0x00,0x32, 0x00,0x34, 0x00,
};
2.1.3 设备请求解析
- 设备请求格式封装
按照设备请求格式,代码封装如下:
typedef union _REQUEST_PACK {unsigned char buffer[8];struct {unsigned char bmReuestType;unsigned char bRequest;uint16_t wValue;uint16_t wIndx;uint16_t wLength;} r;
} mREQUEST_PACKET, *mpREQUEST_PACKET;
2、根据设备请求格式解析
void handle_usb_request(mREQUEST_PACKET USB_request)
{setUpStage = 1;if (USB_request.r.bmReuestType & 0x80) {printf("dir: GET\n");} else {printf("dir: SET\n");}switch ((request.r.bmReuestType >> 5) & 0x3) {case 0:printf("std request->");handle_std_request(USB_request); //handle standard requestbreak;case 1:printf("class request->");break;case 2:printf("manufacture request->");break;case 3:printf("reserve req!\r\n");break;}
}// only handle GET_DESCRIPTOR and SET_ADDRESS req
void handle_std_request(mREQUEST_PACKET USB_request)
{switch (USB_request.r.bRequest) {case GET_STATUS:printf("GET_STATUS->");break;case CLEAR_FEATURE:printf("CLEAR_FEATURE->");break;case SET_FEATURE:printf("SET_FEATURE->");break;case SET_ADDRESS:printf("SET_ADDRESS:\r\n");USB_SetDevAddress(USB_request.r.wValue);break;case GET_DESCRIPTOR:printf("GET_DESCRIPTOR->");handle_get_desc_request(USB_request);break;case SET_DESCRIPTOR:printf("SET_DESCRIPTOR->");break;case GET_CONFIGURATION:printf("GET_CONFIGURATION->");break;case SET_CONFIGURATION:printf("SET_CONFIGURATION->");break;case GET_INTERFACE:printf("GET_INTERFACE->");break;case SET_INTERFACE:printf("SET_INTERFACE->");break;case SYNCH_FRAME:printf("SYNCH_FRAME->");break;}
}
- 根据设备请求类型处理描述符
void handle_get_desc_request(mREQUEST_PACKET USB_request)
{int len;unsigned char req_type = USB_request.r.wValue >> 8;unsigned char req_index = USB_request.r.wValue & 0xF;switch (req_type) {case 1:printf("device desc id(%d)\n",req_index);VarSetupDescr = DevDes;mVarSetupLength = MIN(sizeof(DevDes), SetupMaxLength);break;case 2:printf("cfg desc id(%d)\n",req_index);VarSetupDescr = ConDes;mVarSetupLength = MIN(sizeof(ConDes), SetupMaxLength);break;case 3:printf("string desc:");if ((USB_request.r.wValue & 0xff) == 0) {printf("LangDes\n");VarSetupDescr = LangDes;mVarSetupLength = MIN(sizeof(LangDes), SetupMaxLength);} else if ((USB_request.r.wValue & 0xff) == 1) {printf("Manuf_Des id(%d)\n",req_index);VarSetupDescr = Manuf_Des;mVarSetupLength = MIN(sizeof(Manuf_Des), SetupMaxLength);} else if ((USB_request.r.wValue & 0xff) == 2) {printf("Prod_Des id(%d)\n",req_index);VarSetupDescr = Prod_Des;mVarSetupLength = MIN(sizeof(Prod_Des), SetupMaxLength);} else if ((USB_request.r.wValue & 0xff) == 3) {printf("SerDes id(%d)\n",req_index);VarSetupDescr = SerDes;mVarSetupLength = MIN(sizeof(SerDes), SetupMaxLength);} else {printf("other\n");}break;case 4:printf("interface desc id(%d)\n",req_index);break;case 5:printf("endpoint desc id(%d)\n",req_index);break;case 6:printf("qualifier desc id(%d)\n",req_index);VarSetupDescr = DeviceQualifierDesc;mVarSetupLength = MIN(sizeof(DeviceQualifierDesc), SetupMaxLength);break;case 0x22:printf("hid desc\n");VarSetupDescr = ReportDescriptor;mVarSetupLength = MIN(sizeof(ReportDescriptor), SetupMaxLength);break;default:printf("other desc\n");}UsbEp0Up();
}
- uart 模拟鼠标行为
void USART0_IRQHandler(void)
{uint8_t USART0_receive_ch;if (USART_GetITStatus(USART0, USART_STATUS_RXIP) == SET) {USART0_receive_ch = USART_ReceiveData(USART0);// 鼠标移动指令if (USART0_receive_ch == 'a') {buf_mouse[1] = -10; // X位移:左移buf_mouse[2] = 0; // Y位移:不变printf("left\n");need_release = 0; // 移动事件不需要释放,所以清除need_release(防止点击事件未完成)} else if (USART0_receive_ch == 'd') {buf_mouse[1] = 10; // X位移:右移buf_mouse[2] = 0; // Y位移:不变printf("right\n");need_release = 0;} else if (USART0_receive_ch == 'w') {buf_mouse[1] = 0; // X位移:不变buf_mouse[2] = -10; // Y位移:上移printf("up\n");need_release = 0;} else if (USART0_receive_ch == 's') {buf_mouse[1] = 0; // X位移:不变buf_mouse[2] = 10; // Y位移:下移printf("down\n");need_release = 0;}// 鼠标按键指令else if (USART0_receive_ch == 'c') {// 左键点击:设置按钮状态bit0为1buf_mouse[0] = 0x01; // 按钮状态:左键按下buf_mouse[1] = 0; // X位移:不变buf_mouse[2] = 0; // Y位移:不变printf("left click\n");need_release = 1; // 标记需要发送释放报告} else if (USART0_receive_ch == ' ') {// 右键点击:设置按钮状态bit1为1buf_mouse[0] = 0x02; // 按钮状态:右键按下buf_mouse[1] = 0; // X位移:不变buf_mouse[2] = 0; // Y位移:不变printf("right click\n");need_release = 1;} else if (USART0_receive_ch == 'x') {// 中键点击:设置按钮状态bit2为1buf_mouse[0] = 0x04; // 按钮状态:中键按下buf_mouse[1] = 0; // X位移:不变buf_mouse[2] = 0; // Y位移:不变printf("middle click\n");need_release = 1;} else if (USART0_receive_ch == 'r') {// 释放所有按键buf_mouse[0] = 0x00; // 按钮状态:所有键释放buf_mouse[1] = 0; // X位移:不变buf_mouse[2] = 0; // Y位移:不变printf("release all buttons\n");need_release = 0; // 不需要再释放}mouse_data_ready = 1;}
}
2.1.4 最终注册情况
- 打开windows设备管理器,可以看到注册成功的鼠标设备

2、window 端通过USB Device Tree查看注册信息如下
+++++++++++++++++ Device Information ++++++++++++++++++
Device Description : HID-compliant mouse
Device Path 1 : \\?\HID#VID_6666&PID_8888#7&ee10cea&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030} (GUID_DEVINTERFACE_HID)
Device Path 2 : \\?\HID#VID_6666&PID_8888#7&ee10cea&0&0000#{378de44c-56ef-11d1-bc8c-00a0c91405dd} (GUID_DEVINTERFACE_MOUSE)
Kernel Name : \Device\0000015b
Device ID : HID\VID_6666&PID_8888\7&EE10CEA&0&0000
Hardware IDs : HID\VID_6666&PID_8888&REV_0001 HID\VID_6666&PID_8888 HID\VID_6666&UP:0001_U:0002 HID_DEVICE_SYSTEM_MOUSE HID_DEVICE_UP:0001_U:0002 HID_DEVICE
Driver KeyName : {4d36e96f-e325-11ce-bfc1-08002be10318}\0002 (GUID_DEVCLASS_MOUSE)
Driver : \SystemRoot\System32\drivers\mouhid.sys (Version: 10.0.19041.1 Date: 2019-12-07 Company: Microsoft Corporation)
Driver Inf : C:\Windows\inf\msmouse.inf
Legacy BusType : PNPBus
Class : Mouse
Class GUID : {4d36e96f-e325-11ce-bfc1-08002be10318} (GUID_DEVCLASS_MOUSE)
Service : mouhid
Enumerator : HID
Location Info : -
Address : 1
Manufacturer Info : Microsoft
Capabilities : 0xA0 (SilentInstall, SurpriseRemovalOK)
Status : 0x0180200A (DN_DRIVER_LOADED, DN_STARTED, DN_DISABLEABLE, DN_NT_ENUMERATOR, DN_NT_DRIVER)
First Install Date : 2025-09-17 11:18:39
Last Arrival Date : 2025-10-09 17:53:44
EnhancedPowerMgmtEnabled : 0
Power State : D0 (supported: D0, D3, wake from D0)
+++++++++++++ Mouse Information ++++++++++++++
Input Data Queue Length : 2
Mouse Identifier : 256
Number of Buttons : 3
Sample Rate : 0
++++++++++++++ HID Information +++++++++++++++
Manufacturer : hezaizai
Product : Demo USB2.0 optical Mouse
UsagePage : 0x01 (Generic Desktop Controls)
Usage : 0x02 (Mouse)
Note:
USB Device Tree下载地址: https://www.uwe-sieber.de/files/UsbTreeView_Win32.zip
3、视频效果演示
uart-mouse test
3 枚举流程详细说明
当前市面上已有多种便捷的USB抓包分析工具,同时也可使用Wireshark等软件工具进行USB报文捕获与分析,用于排查连接过程中的问题。
除上述方法外,我们还可通过示波器或逻辑分析仪对USB通信进行底层信号采集与分析。本项目将采用逻辑分析仪对模拟鼠标设备的完整枚举流程进行抓包,并深入解析报文内容。
需要说明的是,实际枚举过程可能与其他技术文档描述存在差异,这主要源于主机端枚举策略的动态调整。为深入验证设备兼容性,后续计划还将开发定制化主机端测试程序,并移植常用USB协议栈以完善功能。
通过这种硬件级的报文分析方式,能够更深入地理解USB通信机制,为后续设备开发与问题排查提供有力支持。
另外,逻辑分析仪截图,前面章节比较详细,后面有一些删减(因为很多都差不多)。
3.0 总线复位

- device上电后,拉高dp(d+)总线(高速/全速上拉接在dp,低速接在dm)
- hub检测到电压变化并将信息反馈给host
- host之后会发送一个Get_Port_Status
- hub会将设备速度类型回复给host
- 在device拉高100ms稳定后,host会发出Set_Port_Feature请求让hub 复位其管理的端口
- hub通过驱动数据线到复位状态(D+和 D-全为低电平),并持续至少 10ms。当然,hub 不会把这样的复位信号发送给其他已有设备连接的端口,所以其他连在该 hub 上的设备看不到复位信号
另外USB热插拔的实现机制也正是基于此。
3.1 Get DevDesc控制传输
3.1.1 SETUP事务

- host->device SETUP令牌包
- host->device Data0数据包
80 06 00 01 00 00 40 00(小端格式)

0x80, // bmRequestType: Dir: D2H, Type: Standard, Recipient: Device
0x06, // bRequest (Get Descriptor)
0x00, // wValue[0:7] Desc Index: 0
0x01, // wValue[8:15] Desc Type: (Device)
0x00, 0x00, // wIndex Language ID: 0x00
0x40, 0x00, // wLength = 64
该报文表示,主机请求设备返回一个描述符(0x06),描述符类型为设备描述符(0x01),长度为64字节(0x40)
- device->host ACK握手包
3.1.2 IN事务

- host->device IN令牌包
- device->host Data1数据包
0x12, // bLength
0x01, // bDescriptorType (Device)
0x10, 0x01, // bcdUSB 1.10
0x00, // bDeviceClass (Use class information in the Interface Descriptors)
0x00, // bDeviceSubClass
0x00, // bDeviceProtocol
0x40, // bMaxPacketSize0 64
0x66, 0x66, // idVendor 0x6666
0x88, 0x88, // idProduct 0x8888
0x01, 0x00, // bcdDevice 0.01
0x01, // iManufacturer (String Index)
0x02, // iProduct (String Index)
0x00, // iSerialNumber (String Index)
0x01, // bNumConfigurations 1
- host->device ACK握手包
3.1.3 OUT事务

- host->device OUT令牌包
- host->device Data1数据包
- device->host ACK握手包
3.1.4 总结
这里会发现在host获取device描述符的过程分为三个阶段:
- 建立阶段
主机发送一个8字节的请求包(Setup Packet)。这个包包含了请求类型(GET_DESCRIPTOR)、请求值、索引以及最重要的 wLength(40) - 数据阶段
根据请求,设备向主机回复数据(18字节描述符) - 状态阶段
主机发送一个空数据包,表示数据传输完成
建立阶段一定是DATA0 数据包,之后如果有数据阶段,将进行翻转,变成 DATA1,并且在每次正确数据传输后都会进行一次翻转,这个机制用于保证数据被正确接收,而不是发送方发送的重复数据包(如果对方没有正确接收数据,DATAx不会翻转)。
在状态阶段,一律使用 DATA 1进行回复,状态阶段的数据包中的数据为空,也就是说不携带任何数据。
当完成了以上几个阶段,一次控制传输才算完成。
3.2 总线复位
当完成第一次的控制传输后,系统会要求 hub 对设备进行再一次的复位操作。再次复位的目的是使设备进入一个确定的状态。

3.3 Set_Address控制传输
3.3.1 SETUP事务

- host->device SETUP令牌包
- host->device Data0数据包
0x00, // bmRequestType: Dir: H2D, Type: Standard, Recipient: Device
0x05, // bRequest (Set Address)
0x02, 0x00, // wValue Device Addr: 2
0x00, 0x00, // wIndex = 0x00
0x00, 0x00, // wLength = 0
- device->host ACK握手包
3.3.2 IN事务

- host->device IN令牌包
- device->host Data1数据包
- host->device ACK握手包
Set_Address控制传输的信息通过SETUP令牌包以及传输完了,就不用额外的数据传输,只有建立阶段和状态阶段,而不需要数据阶段了。
device会将收到的addr配置到对应的寄存器,后续host也会将通过该addr来和device进行数据交互
3.4 Get DevDesc控制传输
再次获取device的描述信息,收发包同3.1一样。
不同的是这次发出读取18bytes数据SETUP令牌包(第一次请求读取64bytes的描述信息)
3.5 Get CfgDesc控制传输
这里是第一次获取device的配置信息,主要是获取配置描述符长度,后续会再发一次,获取完整配置描述符信息。
我用的windows主机第一次请求长度是256bytes,其他主机也有9bytes;但这里不关键,不影响配置描述符长度的获取,也不影响后续配置描述符的获取。
3.5.1 SETUP事务

- host->device SETUP令牌包
可以看到此时的Addr = 2 - host->device Data0数据包
0x80, // bmRequestType: Dir: D2H, Type: Standard, Recipient: Device
0x06, // bRequest (Get Descriptor)
0x00, // wValue[0:7] Desc Index: 0
0x02, // wValue[8:15] Desc Type: (Configuration)
0x00, 0x00, // wIndex Language ID: 0x00
0xFF, 0x00, // wLength = 255 - device->host ACK握手包
3.5.2 IN事务

- host->device IN令牌包
- device->host Data1数据包
0x09, // bLength: 描述符长度=9字节
0x02, // bDescriptorType: 配置描述符类型(0x02)
0x22, 0x00, // wTotalLength: 配置信息总长度=34字节(0x0022)
0x01, // bNumInterfaces: 接口数量=1
0x01, // bConfigurationValue: 配置值=1(用于SET_CONFIGURATION)
0x00, // iConfiguration: 配置字符串索引=0(无字符串)
0x80, // bmAttributes: 属性// - 位7: 1=总线供电// - 位6: 0=不支持远程唤醒// - 位5-0: 保留
0x32, // bMaxPower: 最大功耗=100mA(0x32=50, 单位2mA→100mA)0x09, // bLength: 描述符长度=9字节
0x04, // bDescriptorType: 接口描述符类型(0x04)
0x00, // bInterfaceNumber: 接口编号=0
0x00, // bAlternateSetting: 备用设置=0
0x01, // bNumEndpoints: 端点数量=1(除了默认控制端点0)
0x03, // bInterfaceClass: 接口类=0x03(HID类)
0x01, // bInterfaceSubClass: 接口子类=0x01(启动接口)
0x02, // bInterfaceProtocol: 接口协议=0x02(鼠标)
0x00, // iInterface: 接口字符串索引=0(无字符串)0x09, // bLength: 描述符长度=9字节
0x21, // bDescriptorType: HID描述符类型(0x21)
0x11, 0x01, // bcdHID: HID规范版本=1.11
0x00, // bCountryCode: 国家代码=0(不支持本地化)
0x01, // bNumDescriptors: 下级描述符数量=1
0x22, // bDescriptorType[0]: 报告描述符类型(0x22)
0x34, 0x00, // wDescriptorLength[0]: 报告描述符长度=52字节(0x0034)0x07, // bLength: 描述符长度=7字节
0x05, // bDescriptorType: 端点描述符类型(0x05)
0x81, // bEndpointAddress: 端点地址// - 位7: 1=IN方向(设备到主机)// - 位3-0: 0001=端点1
0x03, // bmAttributes: 端点属性// - 位1-0: 11=中断传输
0x04, 0x00, // wMaxPacketSize: 最大包大小=4字节
0x0A, // bInterval: 轮询间隔=10ms
- host->devece ACK握手包
3.5.3 OUT事务
- host->device OUT令牌包
- host->device Data1数据包
- device->host ACK握手包
本次的控制传输同样分为建立阶段,数据阶段和状态阶段
3.6 Get StrDesc控制传输
在host获取字符描述符前,一般会先获取下字符支持的语言,方便主机根据字符编码来解析显示。
这里获取的是厂商信息。
3.6.1 SETUP事务

0x80, // bmRequestType: Dir: D2H, Type: Standard, Recipient: Device
0x06, // bRequest (Get Descriptor)
0x00, // wValue[0:7] Desc Index: 0
0x03, // wValue[8:15] Desc Type: (String)
0x00, 0x00, // wIndex Language ID: 0x00
0xFF, 0x00, // wLength = 255
3.6.2 IN事务

0x04 //字符长度
0x03 //字符串描述符
0x09,0x04 //支持语言为英语
3.6.3 OUT事务
3.6.4 SETUP事务

0x80, // bmRequestType: Dir: D2H, Type: Standard, Recipient: Device
0x06, // bRequest (Get Descriptor)
0x02, // wValue[0:7] Desc Index: 2
0x03, // wValue[8:15] Desc Type: (String)
0x09, 0x04, // wIndex Language ID: 0x0409
0xFF, 0x00, // wLength = 255
3.6.5 IN事务

3.6.6 OUT事务

3.7 Get DevDesc控制传输
再次获取设备描述符,同3.4节。
3.8 Get CfgDesc控制传输
这里是第二次获取device的配置描述符,和第一次(3.5节)不一样支持在于,该次设备请求的数据长度为第一次上报的34bytes。
3.9 SET Configure
用于激活设备的一个指定配置,使device从"寻址状态"进入"配置状态",device可以启用相应的端点(端点可以开始接收/发送报告)
3.9.1 SETUP事务

0x00, // bmRequestType: Dir: H2D, Type: Standard, Recipient: Device
0x09, // bRequest (Set Config)
0x01, 0x00, // wValue Config Num: 1
0x00, 0x00, // wIndex = 0x00
0x00, 0x00, // wLength = 0
3.9.2 IN事务

3.10 SET IDLE
属于 HID类特定请求,当输入数据没有变化时,不要频繁报告,只有在数据发生变化时,或者达到指定的空闲时间后才发送报告
3.10.1 SETUP事务

0x21, // bmRequestType: Dir: H2D, Type: Class, Recipient: Interface
0x0A, // bRequest
0x00, 0x00, // wValue[0:15] = 0x00
0x00, 0x00, // wIndex = 0x00
0x00, 0x00, // wLength = 0

3.11 Get HID Report控制传输
3.11.1 SETUP事务

0x81, // bmRequestType: Dir: D2H, Type: Standard, Recipient: Interface
0x06, // bRequest (Get Descriptor)
0x00, // wValue[0:7] Desc Index: 0
0x22, // wValue[8:15] Desc Type: (HID Report)
0x00, 0x00, // wIndex Language ID: 0x00
0x74, 0x00, // wLength = 116
3.11.2 IN事务

0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x03, // Usage Maximum (0x03)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x03, // Report Count (3)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x05, // Report Size (5)
0x95, 0x01, // Report Count (1)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
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,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0xC0, // End Collection
3.11.6 OUT事务

3.12 Get StrDesc控制传输
后续可能还会获取其他的字符描述符,包括:厂商信息、设备信息等。
篇幅原因,就不再这里累述了,可以参考3.6。
4 中断传输
鼠标、键盘等人机交互设备(HID)的特点是数据量小,但要求主机能及时响应。中断传输正是为这种场景设计的:主机保证会以固定的时间间隔去主动询问(轮询)设备是否有数据要上报。
通过端点描述符可以获取到信息:允许鼠标设备以最高100Hz的频率,通过端点1向主机发送最多4字节的移动和按键数据。
下面简单抓取下执行上下左右时的波形
可以看出此时上报的ADDR=2,EP=1。
- 上

- 下

- 左

- 右

参考
HID 简介
HID规范
HID Usage Tables
