HID设备规范精讲:从协议到枚举

HID设备,说白了就是人机交互设备——键盘、鼠标、游戏手柄、触摸屏都归它管。USB HID协议的好处是通用,你不用为每种输入设备单独写驱动。今天我就把这套规范的底细给你讲透。

HID类协议概述

HID类协议是USB设备类协议里最常用的一类。它定义了一套标准的数据交换方式,让主机不用知道设备具体是什么,就能读懂你的按键、移动、摇杆数据。

我个人习惯把HID协议拆成三层理解:

  • 协议层:定义数据怎么传(中断传输、报告协议)
  • 描述符层:定义数据长什么样(报告描述符)
  • 应用层:定义数据怎么用(鼠标、键盘等用途)

嗯,这里要注意:HID设备不一定非得是USB的。蓝牙HID、I2C HID也都有,但USB HID是最成熟的。我在项目中遇到过客户拿蓝牙HID方案来问USB问题,结果发现是报告描述符写错了——这个坑后面会细说。

报告描述符:HID设备的灵魂

报告描述符(Report Descriptor)是HID设备最核心的东西。它告诉主机:你的数据包里每个bit代表什么。没有它,主机就是个瞎子。

描述符的语法结构

报告描述符由一系列Item组成。每个Item包含一个字节的头部和若干字节的数据。头部的高位决定Item类型,低位决定数据长度。

Item类型 Tag值 作用
Main 0x80-0xFF 定义输入、输出、功能、集合
Global 0x00-0x7F 设置全局状态(用途页、逻辑最小值等)
Local 0x00-0x7F 设置局部状态(用途、物理最小值等)

你想想看,这其实就是一个状态机。Global项设置全局参数,Local项设置局部参数,Main项用这些参数生成一个数据字段。我刚开始学的时候,被这个嵌套关系绕晕过。

一个简单的鼠标报告描述符

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 (Button)
0x19, 0x01,        //     Usage Minimum (Button 1)
0x29, 0x03,        //     Usage Maximum (Button 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,Var,Abs)
0x95, 0x01,        //     Report Count (1)
0x75, 0x05,        //     Report Size (5)
0x81, 0x01,        //     Input (Const,Var,Abs)
0x05, 0x01,        //     Usage Page (Generic Desktop)
0x09, 0x30,        //     Usage (X)
0x09, 0x31,        //     Usage (Y)
0x15, 0x81,        //     Logical Minimum (-127)
0x25, 0x7F,        //     Logical Maximum (127)
0x75, 0x08,        //     Report Size (8)
0x95, 0x02,        //     Report Count (2)
0x81, 0x06,        //     Input (Data,Var,Rel)
0xC0,              //   End Collection
0xC0               // End Collection

这段描述符定义了一个标准三键鼠标:3个bit的按键,5个bit的填充,2个字节的X/Y相对位移。总共3个字节的报告长度。

关键点:Report Count和Report Size的乘积决定了这个字段占多少bit。所有字段的bit数加起来必须是8的倍数,否则要补填充位。

解析报告描述符的步骤

我建议你按这个流程来解析:

  1. 读第一个字节,判断Item类型和长度
  2. 根据类型决定是Main、Global还是Local
  3. Global项会改变后续Main项的行为
  4. Local项只影响紧跟着的Main项
  5. 遇到Main项时,用当前Global和Local状态生成一个数据字段
  6. 遇到Collection时,递归解析内部内容

调试技巧:用USBlyzer或Wireshark抓包时,可以直接看到解析后的报告描述符树形结构。我经常拿它跟自己的解析结果对比,找差异。

HID设备枚举流程

枚举过程就是主机认识你的过程。HID设备在标准USB枚举基础上,多了几个特殊步骤。

枚举的完整流程

  1. 获取设备描述符:主机知道这是个USB设备
  2. 获取配置描述符:主机知道设备有哪些配置
  3. 获取接口描述符:主机发现接口类是HID(0x03)
  4. 获取HID描述符:主机知道报告描述符的长度和版本
  5. 获取报告描述符:主机解析你的数据格式
  6. 获取端点描述符:主机知道用哪个端点通信

我曾经在一个项目里踩过坑:HID描述符里的报告描述符长度写错了,导致主机只读了前半段描述符,后半段被截断了。鼠标能识别但按键全乱套。查了两天才找到原因。

接口描述符

接口描述符告诉主机:这个接口是HID类的。关键字段如下:

字段 值 说明
bInterfaceClass 0x03 HID类
bInterfaceSubClass 0x00 / 0x01 0x00=无启动协议,0x01=支持启动协议
bInterfaceProtocol 0x00-0x02 0x00=无,0x01=键盘,0x02=鼠标

注意:如果你做的是键盘或鼠标,建议把SubClass设为0x01,Protocol设为对应的值。这样BIOS下也能用,否则进系统前可能没反应。

HID描述符

HID描述符是HID设备特有的。它紧跟在接口描述符后面,结构如下:

typedef struct {
    uint8_t  bDescriptorType;   // 0x21
    uint16_t bcdHID;            // HID版本号,通常是0x0111
    uint8_t  bCountryCode;      // 国家代码,键盘布局用
    uint8_t  bNumDescriptors;   // 后续描述符数量,通常是1
    uint8_t  bDescriptorType2;  // 0x22(报告描述符)
    uint16_t wDescriptorLength; // 报告描述符长度
} HID_DESCRIPTOR;

嗯,这里有个细节:bcdHID字段是BCD码格式。0x0111表示版本1.11,不是1.11。我见过有人写成0x0101,结果主机不认。

端点描述符

HID设备通常使用中断端点来传输数据。端点描述符定义了这个端点的属性:

字段 典型值 说明
bEndpointAddress 0x81 IN端点1
bmAttributes 0x03 中断传输
wMaxPacketSize 0x0040 64字节(全速)
bInterval 0x0A 10ms轮询间隔

你想想看,bInterval这个值很关键。全速设备单位是ms,高速设备单位是125μs的倍数。鼠标一般设1-10ms,键盘设10-20ms就够了。设太短浪费带宽,设太长手感延迟。

知识结构总览

下面这张图把HID设备规范的核心脉络串起来了:

HID设备规范知识结构 HID类协议 报告描述符 枚举流程 数据传输 Item语法 Main/Global/Local 接口描述符 HID描述符 端点描述符 报告描述符定义数据格式 枚举流程建立通信通道 中断传输承载实际数据

避坑指南

做HID设备这么多年,我总结了几条血泪教训:

  • 报告描述符长度必须准确:HID描述符里的wDescriptorLength一定要跟实际长度一致。我曾经少算了2个字节,主机解析到一半就停了。
  • 中断端点间隔别设太短:全速设备bInterval最小1ms,但设1ms意味着每秒1000次中断。USB总线会扛不住的。
  • 启动协议要谨慎:如果你设了支持启动协议,BIOS下会用简化版的报告格式。你的固件要能同时处理两种格式。
  • 报告ID不是必须的:只有一个报告时不需要报告ID。加了反而多一个字节的开销。

调试建议:用USB分析仪抓包看枚举过程。重点关注主机请求报告描述符时,设备返回的数据是否完整。我习惯在固件里加一个调试串口,把每次枚举的请求和响应都打印出来。