人机接口设备协议(HID)

本笔记为作者再学习蓝牙Host协议栈的一些心得体会,如有不对的地方,请包含与谅解!

​ ————by wsoz

人机接口设备协议(HID)

HID协议定义了人机接口设备中的协议、特征和使用规程,是在USB HID协议上进行的改进。

HID协议角色

HID协议定义了两种角色:

角色 名称 说明 典型设备
HID Host 主机 接收并处理HID数据的设备 电脑、手机、平板、游戏主机
HID Device 设备 产生并发送HID数据的设备 键盘、鼠标、游戏手柄、遥控器
角色关系
┌─────────────┐                    ┌─────────────┐
│  HID Device │ ── HID Reports ──→ │  HID Host   │
│   (键盘)    │ ←── Output/Feature │   (电脑)    │
└─────────────┘                    └─────────────┘
数据流向
方向 Report类型 说明
Device → Host Input Report 输入数据(按键、鼠标移动等)
Host → Device Output Report 输出数据(如键盘LED状态)
双向 Feature Report 配置/状态信息
与L2CAP的关系

蓝牙HID有两种实现方式:

方式 底层协议 说明
HID over L2CAP 直接基于L2CAP 经典蓝牙HID(BR/EDR),使用PSM 0x0011和0x0013
HOGP 基于GATT/ATT 低功耗蓝牙HID(BLE),HID over GATT Profile
HID SDP服务发现

在建立HID连接之前,Host需要通过SDP查询Device的HID服务记录,获取关键信息(如HID Descriptor、PSM等)。

SDP角色说明

SDP是服务发现协议,本质是"服务发布与查询":

角色 SDP行为 说明
HID Device 发布HID服务记录 告诉别人"我是HID设备,这是我的Report Descriptor"
HID Host 查询HID服务记录 问对方"你是HID设备吗?给我你的Report Descriptor"

重点:只有HID Device有HID SDP服务记录(被查询方),HID Host没有HID SDP记录但会查询Device的记录(查询方)。类似餐厅有菜单,顾客看菜单点菜。

HID服务UUID
UUID 名称 说明
0x1124 HumanInterfaceDeviceService HID服务类UUID
HID SDP属性

HID Device的SDP记录包含以下属性:

属性ID 名称 类型 说明
0x0200 HIDDeviceReleaseNumber uint16 设备版本号
0x0201 HIDParserVersion uint16 HID解析器版本(通常0x0111=1.1.1)
0x0202 HIDDeviceSubclass uint8 设备子类(键盘0x40、鼠标0x80、组合0xC0)
0x0203 HIDCountryCode uint8 国家码(键盘布局,0=不指定)
0x0204 HIDVirtualCable bool 是否支持虚拟线缆
0x0205 HIDReconnectInitiate bool 设备是否会主动重连
0x0206 HIDDescriptorList sequence 最重要,包含Report Descriptor
0x0207 HIDLANGIDBaseList sequence 语言ID列表
0x0208 HIDBatteryPower bool 是否电池供电
0x0209 HIDRemoteWake bool 是否支持远程唤醒
0x020B HIDProfileVersion uint16 HID Profile版本(0x0101=1.1)
0x020C HIDSupervisionTimeout uint16 监督超时(单位:基带slot)
0x020D HIDNormallyConnectable bool 是否通常可连接
0x020E HIDBootDevice bool 是否支持Boot Protocol
HIDDescriptorList结构

这是最关键的属性,Host从这里获取Report Descriptor:

HIDDescriptorList = sequence {
    sequence {
        uint8  descriptor_type,    // 0x22 = Report Descriptor
        string descriptor_data     // Report Descriptor二进制数据
    },
    ...  // 可以有多个描述符
}
descriptor_type 说明
0x22 Report Descriptor
0x23 Physical Descriptor(极少用)
Host查询HID服务流程
HID Host                                HID Device
    │                                       │
    │  ① SDP连接                            │
    │ ─────────────────────────────────────→│
    │                                       │
    │  ② ServiceSearchAttributeRequest     │  查询HID服务
    │     ServiceClassID = 0x1124          │
    │ ─────────────────────────────────────→│
    │                                       │
    │  ③ ServiceSearchAttributeResponse    │  返回服务记录
    │     - ProtocolDescriptorList         │  (PSM信息)
    │     - HIDDescriptorList              │  (Report Descriptor)
    │     - HIDDeviceSubclass              │  (设备类型)
    │ ←─────────────────────────────────────│
    │                                       │
    │  ④ 解析获取Report Descriptor          │
    │                                       │
    │  ⑤ 建立L2CAP连接...                   │

Host通过SDP获取Report Descriptor后,才能正确解析后续收到的HID Report数据。

HID逻辑链路

经典蓝牙HID在L2CAP层建立两条逻辑通道:

通道 PSM 名称 用途
HID Control 0x0011 控制通道 连接管理、GET/SET请求、握手响应
HID Interrupt 0x0013 中断通道 传输HID Report数据(Input/Output)
通道关系图
┌─────────────────────────────────────────────────────────┐
│                      HID Layer                          │
├────────────────────────┬────────────────────────────────┤
│    HID Control Channel │    HID Interrupt Channel       │
│      (PSM 0x0011)      │       (PSM 0x0013)             │
├────────────────────────┴────────────────────────────────┤
│                       L2CAP                             │
└─────────────────────────────────────────────────────────┘
两通道的区别
特性 Control Channel Interrupt Channel
连接顺序 先建立 后建立
数据类型 控制命令、握手 Report数据
传输方式 请求-响应模式 异步传输
典型操作 GET_REPORT、SET_REPORT INPUT报告、OUTPUT报告
连接建立流程
HID Device                              HID Host
    │                                       │
    │  ① L2CAP Connect (PSM=0x0011)        │  建立Control通道
    │ ←───────────────────────────────────→ │
    │                                       │
    │  ② L2CAP Connect (PSM=0x0013)        │  建立Interrupt通道
    │ ←───────────────────────────────────→ │
    │                                       │
    │  ③ HID Control: 获取HID描述符等       │  配置阶段
    │ ←───────────────────────────────────→ │
    │                                       │
    │  ④ HID Interrupt: Input Reports      │  数据传输阶段
    │ ─────────────────────────────────────→│

注意:必须先建立Control通道,再建立Interrupt通道。断开时顺序相反。

HID封包格式

HID封包由Header和可选的Payload组成:

┌──────────────────┬─────────────────────┐
│     Header       │      Payload        │
│     (1字节)      │     (0-N字节)       │
└──────────────────┴─────────────────────┘
Header字段结构
  Bit:  7    6    5    4    3    2    1    0
      ┌────┬────┬────┬────┬────┬────┬────┬────┐
      │  Message Type(4bit) │  Parameter(4bit) │
      └────┴────┴────┴────┴────┴────┴────┴────┘ 
字段 位置 说明
Message Type Bit 7-4 消息类型,定义操作类型
Parameter Bit 3-0 参数,含义随消息类型变化
消息类型列表
Message Type 名称 方向 通道 说明
HANDSHAKE 0x0 握手 Device→Host Control 响应Host的请求,返回结果码
HID_CONTROL 0x1 控制 双向 Control 控制命令(挂起、退出挂起、虚拟线缆拔出)
GET_REPORT 0x4 获取报告 Host→Device Control Host请求获取指定Report
SET_REPORT 0x5 设置报告 Host→Device Control Host发送Report给Device
GET_PROTOCOL 0x6 获取协议 Host→Device Control 查询当前协议模式
SET_PROTOCOL 0x7 设置协议 Host→Device Control 设置协议模式(Boot/Report)
DATA 0xA 数据 双向 Interrupt 传输Input/Output Report数据
HANDSHAKE响应码

HANDSHAKE是Device回复Host在Control通道上请求的机制:

Host请求 成功时回复 失败时回复
GET_REPORT DATA(直接返回数据) HANDSHAKE(错误码)
GET_PROTOCOL DATA(直接返回数据) HANDSHAKE(错误码)
SET_REPORT HANDSHAKE(成功码) HANDSHAKE(错误码)
SET_PROTOCOL HANDSHAKE(成功码) HANDSHAKE(错误码)

规律:GET类请求成功时用DATA返回数据,不发HANDSHAKE;SET类请求无论成功失败都用HANDSHAKE回复。

成功的GET_REPORT:
Host ──GET_REPORT──→ Device
Host ←──DATA────────  Device     (直接返回数据)

失败的GET_REPORT:
Host ──GET_REPORT──→ Device
Host ←──HANDSHAKE──  Device     (返回错误码)

SET_REPORT:
Host ──SET_REPORT──→ Device
Host ←──HANDSHAKE──  Device     (返回成功/失败)

HANDSHAKE的Parameter值定义:

Parameter值 名称 说明
0x0 SUCCESSFUL 成功
0x1 NOT_READY 设备未就绪
0x2 ERR_INVALID_REPORT_ID 无效的Report ID
0x3 ERR_UNSUPPORTED_REQUEST 不支持的请求
0x4 ERR_INVALID_PARAMETER 无效参数
0xE ERR_UNKNOWN 未知错误
0xF ERR_FATAL 致命错误

示例:HANDSHAKE成功响应

Header: 0x00
        ├── Message Type = 0x0 (HANDSHAKE)
        └── Parameter = 0x0 (SUCCESSFUL)
HID_CONTROL操作码
Parameter值 名称 说明
0x0 NOP 空操作(已弃用)
0x1 HARD_RESET 硬复位(已弃用)
0x2 SOFT_RESET 软复位(已弃用)
0x3 SUSPEND 挂起(进入低功耗)
0x4 EXIT_SUSPEND 退出挂起
0x5 VIRTUAL_CABLE_UNPLUG 虚拟线缆拔出(断开连接)

前三个从USB HID继承而来,在蓝牙HID中无实际意义,已弃用。实际只用后三个。

SUSPEND(挂起):Host一段时间不使用设备时发送,通知Device进入低功耗模式(如降低扫描频率、关闭LED等)。

EXIT_SUSPEND(退出挂起):恢复设备到正常工作状态。Host或Device都可以发起——例如用户按下键盘任意键唤醒时,Device主动发送EXIT_SUSPEND。

VIRTUAL_CABLE_UNPLUG(虚拟线缆拔出):模拟USB拔线动作,逻辑上断开HID连接。收到后双方断开L2CAP连接,效果等同于物理拔掉USB线。

GET_REPORT / SET_REPORT

Parameter字段的低2位表示Report类型:

Parameter[1:0] Report类型
0x1 Input Report
0x2 Output Report
0x3 Feature Report
Report ID说明

当一个HID设备支持多种Report时,需要用Report ID区分:

场景 Report ID字段
设备只有1种Report 省略(不传,节省1字节)
设备有多种Report 必须传(1字节,值由HID Descriptor定义)

典型多Report设备示例(多功能键盘)

Report ID 用途 说明
0x01 标准按键 A-Z、0-9、F1-F12等
0x02 媒体键 音量+/-、播放、暂停、下一曲
0x03 系统控制 电源、睡眠、唤醒

Report ID的值和含义由设备的HID Descriptor定义,Host通过SDP获取。

GET_REPORT请求格式
┌────────────┬─────────────┬────────────────┐
│   Header   │  Report ID  │  Buffer Size   │
│  (1字节)   │ (0或1字节)  │   (2字节)      │
└────────────┴─────────────┴────────────────┘
字段 说明
Header 0x4x,低2位表示Report类型
Report ID 可选,多Report设备必须指定
Buffer Size 2字节小端序,Host告诉Device缓冲区大小

示例:GET_REPORT请求获取键盘状态

Header:      0x41       → GET_REPORT + Input Report
Report ID:   0x01       → 键盘Report
Buffer Size: 0x08 0x00  → 8字节缓冲区(小端序)
SET_REPORT请求格式
┌────────────┬─────────────┬────────────────┐
│   Header   │  Report ID  │  Report Data   │
│  (1字节)   │ (0或1字节)  │   (N字节)      │
└────────────┴─────────────┴────────────────┘
字段 说明
Header 0x5x,低2位表示Report类型
Report ID 可选,多Report设备必须指定
Report Data 要设置的Report数据

示例:SET_REPORT设置键盘LED状态(Caps Lock亮)

Header:      0x52       → SET_REPORT + Output Report
Report ID:   0x01       → 键盘Report
Report Data: 0x02       → Bit1=1 表示Caps Lock LED亮
GET_PROTOCOL / SET_PROTOCOL

HID定义了两种协议模式:

模式 说明
Boot Protocol 0x00 固定格式,用于BIOS/启动阶段
Report Protocol 0x01 完整格式,由HID Descriptor定义(默认模式)
为什么需要Boot Protocol?
┌─────────────────────────────────────────────────────────────┐
│  BIOS启动阶段                                               │
│  ┌─────────────┐                                            │
│  │ 没有完整的  │  无法解析复杂的HID Descriptor              │
│  │ HID驱动程序 │  ────→  使用固定格式的Boot Protocol        │
│  └─────────────┘                                            │
├─────────────────────────────────────────────────────────────┤
│  操作系统启动后                                             │
│  ┌─────────────┐                                            │
│  │ 完整HID驱动 │  可以解析HID Descriptor                    │
│  │   已加载    │  ────→  切换到Report Protocol              │
│  └─────────────┘                                            │
└─────────────────────────────────────────────────────────────┘
封包格式
操作 Header Payload
GET_PROTOCOL 0x60
SET_PROTOCOL (Boot) 0x70
SET_PROTOCOL (Report) 0x71
协议切换流程
HID Host                                HID Device
    │                                       │
    │  GET_PROTOCOL (Header: 0x60)         │  查询当前协议
    │ ─────────────────────────────────────→│
    │                                       │
    │  DATA (Header: 0xA0, Data: 0x01)     │  返回:Report Protocol
    │ ←─────────────────────────────────────│
    │                                       │
    │  SET_PROTOCOL (Header: 0x70)         │  切换到Boot Protocol
    │ ─────────────────────────────────────→│
    │                                       │
    │  HANDSHAKE (Header: 0x00)            │  确认成功
    │ ←─────────────────────────────────────│

注意:Boot Protocol只支持键盘和鼠标,其他HID设备(如游戏手柄)没有Boot Protocol。

DATA

DATA消息用于在Interrupt通道上传输Report数据:

┌────────────┬─────────────┬────────────────┐
│   Header   │  Report ID  │  Report Data   │
│   0xAx     │ (0或1字节)  │   (N字节)      │
└────────────┴─────────────┴────────────────┘
Header值 含义
0xA1 Input Report(Device→Host)
0xA2 Output Report(Host→Device)
0xA3 Feature Report
Report Data格式

Report Data的具体格式由**HID Descriptor(HID描述符)**定义。以下是常见设备的Boot Protocol标准格式:

键盘Boot Protocol Report(8字节)

  字节:   0        1        2    3    4    5    6    7
      ┌────────┬────────┬────┬────┬────┬────┬────┬────┐
      │Modifier│Reserved│Key0│Key1│Key2│Key3│Key4│Key5│
      └────────┴────────┴────┴────┴────┴────┴────┴────┘
字段 字节 说明
Modifier 0 修饰键位图(Ctrl/Shift/Alt/GUI)
Reserved 1 保留,固定为0x00
Key0-Key5 2-7 最多同时按下6个普通键的键码

Modifier位定义

Bit Bit
0 Left Ctrl 4 Right Ctrl
1 Left Shift 5 Right Shift
2 Left Alt 6 Right Alt
3 Left GUI(Win) 7 Right GUI

鼠标Boot Protocol Report(3字节)

  字节:   0        1        2
      ┌────────┬────────┬────────┐
      │Buttons │ X移动  │ Y移动  │
      └────────┴────────┴────────┘
字段 字节 说明
Buttons 0 按键位图(Bit0=左键,Bit1=右键,Bit2=中键)
X移动 1 X轴相对移动量(有符号,-127~+127)
Y移动 2 Y轴相对移动量(有符号,-127~+127)

Boot Protocol格式是固定的,Report Protocol格式由HID Descriptor定义,可以更复杂(如支持滚轮、更高精度等)。

示例

键盘按下’A’键的Input Report

Header:    0xA1      → DATA + Input Report
Report ID: 0x01      → 键盘Report ID
Data:      00 00 04 00 00 00 00 00
           │     │
           │     └── 按键码 0x04 = 'A'
           └── Modifier = 0 (无修饰键)

键盘按下Ctrl+C的Input Report

Header:    0xA1
Report ID: 0x01
Data:      01 00 06 00 00 00 00 00
           │     │
           │     └── 按键码 0x06 = 'C'
           └── Modifier = 0x01 (Left Ctrl)

HID描述符

HID描述符(HID Descriptor)是HID协议的核心,它告诉Host设备能产生什么样的数据、数据的格式是什么。Host通过SDP获取HID描述符后,才能正确解析Report数据。

描述符类型

蓝牙HID中涉及三种描述符(继承自USB HID):

描述符 说明
HID Descriptor 顶层描述符,包含版本号、国家码、以及下级描述符的数量和长度
Report Descriptor 最重要,定义Report的具体格式(有哪些字段、每个字段多少位、含义是什么)
Physical Descriptor 可选,描述物理设备信息(极少使用)

实际开发中最关键的是Report Descriptor,它决定了Report Data的解析方式。

Report Descriptor结构

Report Descriptor由一系列**Item(项)**组成,每个Item描述Report的一个属性:

┌────────┬────────┬────────┬─── ─── ───┬────────┐
│ Item 1 │ Item 2 │ Item 3 │    ...    │ Item N │
└────────┴────────┴────────┴─── ─── ───┴────────┘
Item格式

每个Item由前缀字节和可选的数据字节组成:

短Item格式(最常用):
  Bit:  7    6    5    4    3    2    1    0
      ┌────┬────┬────┬────┬────┬────┬────┬────┐
      │    Tag (4bit)     │ Type(2bit)│Size(2bit)│
      └────┴────┴────┴────┴────┴────┴────┴────┘
      后跟 0/1/2/4 字节数据
字段 位置 说明
Size Bit 1-0 后续数据长度:0=0字节,1=1字节,2=2字节,3=4字节
Type Bit 3-2 Item类型:0=Main,1=Global,2=Local
Tag Bit 7-4 具体功能标识
Item类型
Type 名称 说明
Main 主项 定义Report的数据结构(Input/Output/Feature/Collection/End Collection)
Global 全局项 设置全局属性(Usage Page、Logical Min/Max、Report Size/Count等)
Local 局部项 设置局部属性(Usage、Usage Min/Max等),仅对下一个Main Item有效

三者区别

Global Local Main
作用 设置属性 设置属性 定义字段
有效期 一直有效,直到被覆盖 仅对下一个Main有效 -

简单理解:Global和Local都是"准备工作"(设置属性),Main才是"真正干活"(定义数据字段)。Global设置后一直保持,Local用一次就失效。

示例

75 08     → [Global] Report Size = 8     // 设置:每个字段8位(一直有效)
95 06     → [Global] Report Count = 6    // 设置:共6个字段(一直有效)
19 00     → [Local] Usage Min = 0        // 临时:用途从0开始(只对下一个Main有效)
29 65     → [Local] Usage Max = 0x65     // 临时:用途到0x65(只对下一个Main有效)
81 00     → [Main] Input                 // 定义:6×8bit=6字节Input字段
                                         // Local失效,Global保持
常用Item标签

注意:Tag的含义取决于Type,同样的Tag值在不同Type下含义不同。例如Tag=0x0在Global下是Usage Page,在Local下是Usage。所以下面按Type分类列出。

Main Item(Type=0x0)
Tag 名称 说明
0x8 Input 定义Input Report的数据字段
0x9 Output 定义Output Report的数据字段
0xB Feature 定义Feature Report的数据字段
0xA Collection 开始一个数据集合
0xC End Collection 结束一个数据集合

后续数据字节Input/Output/Feature的数据位含义:

Bit 值=0 值=1
0 Data(数据) Constant(常量)
1 Array(数组) Variable(变量)
2 Absolute(绝对值) Relative(相对值)

各属性区别

属性 区别 典型应用
Data vs Constant Data=实际有意义的数据;Constant=填充/保留位,Host忽略 Reserved字段用Constant
Array vs Variable Array=字段值是"哪个被选中"(如键码);Variable=每个bit/字段是独立开关 键码用Array,Modifier用Variable
Absolute vs Relative Absolute=当前绝对值;Relative=相对上次的变化量 鼠标移动用Relative,摇杆位置用Absolute

示例对比

键盘Modifier(8个独立开关):
  81 02 → Input (Data, Variable, Absolute)
  每个bit独立表示一个修饰键是否按下:Bit0=LeftCtrl, Bit1=LeftShift...

键盘Key Codes(6个键码槽位):
  81 00 → Input (Data, Array)
  每个字节的值表示"哪个键被按下":0x04=A, 0x05=B...

鼠标移动(相对位移):
  81 06 → Input (Data, Variable, Relative)
  值表示相对上次的移动量:+5表示向右移动5个单位
Global Item(Type=0x1)
Tag 名称 说明
0x0 Usage Page 用途页(如:通用桌面、键盘、LED等)
0x1 Logical Minimum 逻辑最小值
0x2 Logical Maximum 逻辑最大值
0x7 Report Size 每个数据字段的位数
0x9 Report Count 数据字段的个数
0x8 Report ID Report ID值
Local Item(Type=0x2)
Tag 名称 说明
0x0 Usage 具体用途(如:键盘、鼠标、X轴、Y轴等)
0x1 Usage Minimum 用途范围最小值
0x2 Usage Maximum 用途范围最大值

对比:Tag=0x0时,Global是Usage Page(用途页),Local是Usage(具体用途);Tag=0x1时,Global是Logical Minimum,Local是Usage Minimum。

常用Usage Page
Usage Page 说明
0x01 Generic Desktop 通用桌面(键盘、鼠标、游戏手柄等设备类型)
0x07 Keyboard/Keypad 键盘按键码
0x08 LED LED指示灯(Caps Lock、Num Lock等)
0x09 Button 按钮(鼠标按键等)
0x0C Consumer 消费类设备(媒体键、音量等)
Report Descriptor编写模板
第1步:声明设备类型
  ├── Usage Page (这是什么类型的设备页)
  └── Usage (具体是什么设备)

第2步:开始集合
  └── Collection (Application)     ←── 开始

第3步:定义每个字段(重复N次)
  │
  ├── 【准备属性 - Global】
  │     Usage Page = ?          // 这个字段属于哪类
  │     Logical Min/Max = ?     // 值的范围
  │     Report Size = ?         // 每个字段多少位
  │     Report Count = ?        // 几个这样的字段
  │
  ├── 【准备用途 - Local】
  │     Usage 或 Usage Min/Max  // 具体什么用途
  │
  └── 【定义字段 - Main】
        Input/Output/Feature    // 真正创建字段!

第4步:结束集合
  └── End Collection              ←── 结束

编写步骤总结

步骤 做什么 用什么Item
1 声明设备类型 Global(Usage Page) + Local(Usage)
2 开始集合 Main(Collection)
3 设置属性 Global(Size/Count/Min/Max)
4 设置用途 Local(Usage)
5 创建字段 Main(Input/Output/Feature)
6 重复3-5定义更多字段
7 结束集合 Main(End Collection)

记住:Global和Local都是"准备工作",只有遇到Main(Input/Output/Feature)时才真正创建字段!

简单示例:2个按钮的设备
05 09     → Usage Page (Button)           // 第1步:按钮类型
09 01     → Usage (Button 1)              // 这是按钮设备
A1 01     → Collection (Application)      // 第2步:开始集合

  15 00   → Logical Minimum (0)           // 准备Global:值范围0-1
  25 01   → Logical Maximum (1)
  75 01   → Report Size (1)               // 每个字段1位
  95 02   → Report Count (2)              // 2个字段

  09 01   → Usage (Button 1)              // 准备Local:按钮1
  09 02   → Usage (Button 2)              // 准备Local:按钮2

  81 02   → Input (Data, Variable)        // 定义!创建2×1bit的Input

C0        → End Collection                // 第4步:结束

结果:Report Data = 1字节,低2位分别表示按钮1和按钮2的状态。

Report Descriptor实例

以标准键盘的Boot Protocol Report为例,解读其Report Descriptor:

05 01       → Usage Page (Generic Desktop)        // 用途页:通用桌面
09 06       → Usage (Keyboard)                    // 用途:键盘
A1 01       → Collection (Application)            // 开始应用集合

  // ===== Modifier Keys(修饰键,1字节) =====
  05 07     → Usage Page (Keyboard/Keypad)        // 用途页:键盘键码
  19 E0     → Usage Minimum (0xE0 = Left Ctrl)    // 起始:Left Ctrl
  29 E7     → Usage Maximum (0xE7 = Right GUI)    // 结束:Right GUI
  15 00     → Logical Minimum (0)                 // 逻辑最小值:0
  25 01     → Logical Maximum (1)                 // 逻辑最大值:1
  75 01     → Report Size (1)                     // 每个字段1位
  95 08     → Report Count (8)                    // 共8个字段
  81 02     → Input (Data, Variable, Absolute)    // 定义为Input:8×1bit=1字节

  // ===== Reserved(保留,1字节) =====
  95 01     → Report Count (1)                    // 1个字段
  75 08     → Report Size (8)                     // 每个字段8位
  81 01     → Input (Constant)                    // 定义为常量Input:1×8bit=1字节

  // ===== LED Output(LED状态,1字节) =====
  05 08     → Usage Page (LED)                    // 用途页:LED
  19 01     → Usage Minimum (Num Lock)            // 起始:Num Lock
  29 05     → Usage Maximum (Kana)                // 结束:Kana
  95 05     → Report Count (5)                    // 5个LED
  75 01     → Report Size (1)                     // 每个1位
  91 02     → Output (Data, Variable, Absolute)   // 定义为Output:5bit
  95 01     → Report Count (1)                    // 填充
  75 03     → Report Size (3)                     // 3位补齐到1字节
  91 01     → Output (Constant)                   // 填充位

  // ===== Key Codes(按键码,6字节) =====
  05 07     → Usage Page (Keyboard/Keypad)        // 用途页:键盘键码
  19 00     → Usage Minimum (0)                   // 起始:0
  29 65     → Usage Maximum (0x65)                // 结束:0x65
  15 00     → Logical Minimum (0)                 // 逻辑最小值
  25 65     → Logical Maximum (0x65)              // 逻辑最大值
  75 08     → Report Size (8)                     // 每个字段8位
  95 06     → Report Count (6)                    // 共6个字段
  81 00     → Input (Data, Array)                 // 定义为Input数组:6×8bit=6字节

C0          → End Collection                      // 结束集合

这个Descriptor定义的Report格式正好对应前面介绍的键盘Boot Protocol Report:1字节Modifier + 1字节Reserved + 6字节Key Codes,同时还定义了1字节LED Output Report。

实际Report数据示例

根据上面的Descriptor,定义了两种Report:

注意:Header由协议栈自动填充,调用 hid_device_interupt_report() 时只需传入Data部分。

Input Report(Device→Host,8字节):

[Modifier] [Reserved] [KeyCode0] [KeyCode1] [KeyCode2] [KeyCode3] [KeyCode4] [KeyCode5]

Output Report(Host→Device,1字节):

[LED]

场景1:按下 ‘A’ 键

Header:    0xA1
Data:      00 00 04 00 00 00 00 00
           │    │   │
           │    │   └── 0x04 = 'A'
           │    └── Reserved 固定0
           └── Modifier = 0(无修饰键)

场景2:按下 Shift+A(大写A)

Header:    0xA1
Data:      02 00 04 00 00 00 00 00
           │        │
           │        └── 0x04 = 'A'
           └── 0x02 = Bit1=1 = Left Shift

场景3:同时按下 A、B、C

Header:    0xA1
Data:      00 00 04 05 06 00 00 00
                  │  │   │
                  │  │   └── 0x06 = 'C'
                  │  └── 0x05 = 'B'
                  └── 0x04 = 'A'

场景4:松开所有键

Header:    0xA1
Data:      00 00 00 00 00 00 00 00

每次按键必须发两包:按下报文 + 松开报文,否则Host会认为持续按着。

Modifier位定义:

Bit0=Left Ctrl   Bit4=Right Ctrl
Bit1=Left Shift  Bit5=Right Shift
Bit2=Left Alt    Bit6=Right Alt
Bit3=Left GUI    Bit7=Right GUI

该Descriptor同时定义了1字节LED Output Report(Host→Device),用于控制键盘指示灯:

场景5:Host点亮 Caps Lock

Header:    0xA2
Data:      02
           │
           └── Bit1=1 = Caps Lock亮

LED位定义(低5位有效,高3位为填充):

Bit0 = Num Lock
Bit1 = Caps Lock
Bit2 = Scroll Lock
Bit3 = Compose
Bit4 = Kana

流程总结

本节总结HID Host与Device之间的完整交互流程。

整体流程概览
┌──────────────────────────────────────────────────────────────────────────┐
│                         HID 连接完整流程                                  │
├──────────────────────────────────────────────────────────────────────────┤
│  ① 底层连接    │  Inquiry → Paging → LMP → Baseband连接建立              │
├──────────────────────────────────────────────────────────────────────────┤
│  ② 服务发现    │  SDP查询 → 获取HID服务记录 → 解析Report Descriptor       │
├──────────────────────────────────────────────────────────────────────────┤
│  ③ 通道建立    │  L2CAP Control(0x0011) → L2CAP Interrupt(0x0013)        │
├──────────────────────────────────────────────────────────────────────────┤
│  ④ HID配置     │  SET_PROTOCOL(可选) → 准备就绪                          │
├──────────────────────────────────────────────────────────────────────────┤
│  ⑤ 数据传输    │  Input Report ←→ Output Report                          │
├──────────────────────────────────────────────────────────────────────────┤
│  ⑥ 断开连接    │  VIRTUAL_CABLE_UNPLUG 或 L2CAP断开                       │
└──────────────────────────────────────────────────────────────────────────┘
详细交互流程

蓝牙键盘连接电脑为例:

    电脑 (HID Host)                              蓝牙键盘 (HID Device)
         │                                              │
═══════════════════════════════ 阶段①:底层连接 ═══════════════════════════════
         │                                              │
         │  Inquiry (查找设备)                          │
         │ ────────────────────────────────────────────→│
         │                                              │
         │  Inquiry Response (我是键盘,地址XX:XX:XX)   │
         │ ←────────────────────────────────────────────│
         │                                              │
         │  Paging + LMP + Baseband连接                 │
         │ ←──────────────────────────────────────────→ │
         │                                              │
═══════════════════════════════ 阶段②:服务发现 ═══════════════════════════════
         │                                              │
         │  L2CAP Connect (PSM=0x0001, SDP)            │
         │ ────────────────────────────────────────────→│
         │  L2CAP Connect Response                      │
         │ ←────────────────────────────────────────────│
         │                                              │
         │  SDP ServiceSearchAttributeRequest          │
         │     查询: UUID=0x1124 (HID)                 │
         │ ────────────────────────────────────────────→│
         │                                              │
         │  SDP ServiceSearchAttributeResponse         │
         │     返回: HIDDescriptorList (Report Desc)   │
         │            HIDDeviceSubclass = 0x40 (键盘)  │
         │            HIDBootDevice = true              │
         │ ←────────────────────────────────────────────│
         │                                              │
         │  L2CAP Disconnect (SDP通道)                 │
         │ ←──────────────────────────────────────────→ │
         │                                              │
         │  【Host解析Report Descriptor,了解键盘Report格式】
         │                                              │
═══════════════════════════════ 阶段③:HID通道建立 ═════════════════════════════
         │                                              │
         │  L2CAP Connect (PSM=0x0011, Control)        │  建立控制通道
         │ ────────────────────────────────────────────→│
         │  L2CAP Connect Response                      │
         │ ←────────────────────────────────────────────│
         │  L2CAP Config Request/Response              │
         │ ←──────────────────────────────────────────→ │
         │                                              │
         │  L2CAP Connect (PSM=0x0013, Interrupt)      │  建立中断通道
         │ ────────────────────────────────────────────→│
         │  L2CAP Connect Response                      │
         │ ←────────────────────────────────────────────│
         │  L2CAP Config Request/Response              │
         │ ←──────────────────────────────────────────→ │
         │                                              │
═══════════════════════════════ 阶段④:HID配置(可选) ═══════════════════════════
         │                                              │
         │  【如果在BIOS阶段,可能需要切换到Boot Protocol】
         │                                              │
         │  SET_PROTOCOL (Header=0x70, Boot Protocol)  │  Control通道
         │ ────────────────────────────────────────────→│
         │  HANDSHAKE (Header=0x00, 成功)              │
         │ ←────────────────────────────────────────────│
         │                                              │
         │  【正常OS环境下使用默认的Report Protocol,无需SET_PROTOCOL】
         │                                              │
═══════════════════════════════ 阶段⑤:数据传输 ════════════════════════════════
         │                                              │
         │     ══════ 用户按下 'A' 键 ══════           │
         │                                              │
         │  DATA Input Report (Header=0xA1)            │  Interrupt通道
         │     Data: 00 00 04 00 00 00 00 00           │  按下A键
         │ ←────────────────────────────────────────────│
         │                                              │
         │  DATA Input Report (Header=0xA1)            │
         │     Data: 00 00 00 00 00 00 00 00           │  释放A键
         │ ←────────────────────────────────────────────│
         │                                              │
         │     ══════ 用户按下 Caps Lock ══════        │
         │                                              │
         │  DATA Input Report (Header=0xA1)            │
         │     Data: 00 00 39 00 00 00 00 00           │  按下CapsLock
         │ ←────────────────────────────────────────────│
         │                                              │
         │  DATA Output Report (Header=0xA2)           │  Interrupt通道
         │     Data: 02                                 │  点亮CapsLock LED
         │ ────────────────────────────────────────────→│
         │                                              │
         │     ══════ 一段时间无操作 ══════            │
         │                                              │
         │  HID_CONTROL (Header=0x13, SUSPEND)         │  Control通道
         │ ────────────────────────────────────────────→│  通知键盘进入低功耗
         │                                              │
         │     ══════ 用户按任意键唤醒 ══════          │
         │                                              │
         │  HID_CONTROL (Header=0x14, EXIT_SUSPEND)    │  Control通道
         │ ←────────────────────────────────────────────│  键盘请求退出挂起
         │                                              │
═══════════════════════════════ 阶段⑥:断开连接 ════════════════════════════════
         │                                              │
         │  【方式A:虚拟线缆拔出】                     │
         │  HID_CONTROL (Header=0x15, VIRTUAL_CABLE_UNPLUG)
         │ ────────────────────────────────────────────→│
         │                                              │
         │  【方式B:直接断开L2CAP】                    │
         │  L2CAP Disconnect (Interrupt通道)           │  先断Interrupt
         │ ←──────────────────────────────────────────→ │
         │  L2CAP Disconnect (Control通道)             │  再断Control
         │ ←──────────────────────────────────────────→ │
         │                                              │
各阶段要点总结
阶段 关键操作 涉及协议/通道 说明
①底层连接 Inquiry、Paging、LMP Baseband 建立蓝牙物理连接
②服务发现 SDP查询HID服务 L2CAP PSM=0x0001 获取Report Descriptor
③通道建立 建立Control→Interrupt L2CAP PSM=0x0011/0x0013 必须先Control后Interrupt
④HID配置 SET_PROTOCOL(可选) HID Control通道 BIOS阶段可能需要Boot Protocol
⑤数据传输 Input/Output Report HID Interrupt通道 主要数据交互阶段
⑥断开连接 VIRTUAL_CABLE_UNPLUG或L2CAP断开 Control通道/L2CAP 先断Interrupt后断Control
实际抓包示例

以下是一个简化的HID键盘连接抓包示例(仅展示HID层):

# 1. Control通道建立后,Host可能查询当前协议模式
[Host → Device] GET_PROTOCOL
    Header: 0x60

[Device → Host] DATA (返回当前协议)
    Header: 0xA0
    Data: 0x01 (Report Protocol)

# 2. 用户按下 Ctrl+C(复制)
[Device → Host] DATA Input Report
    Header: 0xA1
    Data: 01 00 06 00 00 00 00 00
          │     │
          │     └── Key Code 0x06 = 'C'
          └── Modifier 0x01 = Left Ctrl

# 3. 用户释放按键
[Device → Host] DATA Input Report
    Header: 0xA1
    Data: 00 00 00 00 00 00 00 00  (全0表示无按键)

# 4. Host设置键盘LED(Num Lock + Caps Lock亮)
[Host → Device] DATA Output Report
    Header: 0xA2
    Data: 03  (Bit0=NumLock, Bit1=CapsLock)
常见场景处理
场景 Host行为 Device行为
首次配对连接 SDP查询→保存Report Descriptor→建立通道 响应SDP→等待连接
重新连接(已配对) 可跳过SDP(使用缓存的Descriptor)→直接建立通道 主动发起连接或等待连接
BIOS/启动阶段 SET_PROTOCOL切换到Boot Protocol 切换到固定8字节格式
休眠唤醒 发送EXIT_SUSPEND或等待Device唤醒 发送EXIT_SUSPEND唤醒Host
设备断开 清理连接状态,等待重连 可主动重连(如果HIDReconnectInitiate=true)

HID应用理解

首先对应HID键盘等,我们就通过HID描述符对我们的report字段进行定义

static uint8_t hid_device_descriptor[] =
{
    0x05, 0x01,              /* Usage Page (Generic Desktop) */
    0x09, 0x06,              /* Usage (Keyboard) */
    0xA1, 0x01,              /* Collection (Application) */

    0x05, 0x07,              /*   Usage Page (Keyboard/Keypad) */
    0x19, 0xE0,              /*   Usage Minimum (Keyboard LeftControl) */
    0x29, 0xE7,              /*   Usage Maximum (Keyboard Right GUI) */
    0x15, 0x00,              /*   Logical Minimum (0) */
    0x25, 0x01,              /*   Logical Maximum (1) */
    0x75, 0x01,              /*   Report Size (1) */
    0x95, 0x08,              /*   Report Count (8) */
    0x81, 0x02,              /*   Input (Data,Var,Abs) - modifier */

    0x95, 0x01,              /*   Report Count (1) */
    0x75, 0x08,              /*   Report Size (8) */
    0x81, 0x01,              /*   Input (Const,Array,Abs) - reserved */

    0x05, 0x08,              /*   Usage Page (LEDs) */
    0x19, 0x01,              /*   Usage Minimum (Num Lock) */
    0x29, 0x05,              /*   Usage Maximum (Kana) */
    0x95, 0x05,              /*   Report Count (5) */
    0x75, 0x01,              /*   Report Size (1) */
    0x91, 0x02,              /*   Output (Data,Var,Abs) */

    0x95, 0x01,              /*   Report Count (1) */
    0x75, 0x03,              /*   Report Size (3) */
    0x91, 0x01,              /*   Output (Const,Array,Abs) - LED padding */

    0x05, 0x07,              /*   Usage Page (Keyboard/Keypad) */
    0x19, 0x00,              /*   Usage Minimum (Reserved) */
    0x29, 0x65,              /*   Usage Maximum (Keyboard Application) */
    0x15, 0x00,              /*   Logical Minimum (0) */
    0x25, 0x65,              /*   Logical Maximum (101) */
    0x75, 0x08,              /*   Report Size (8) */
    0x95, 0x06,              /*   Report Count (6) */
    0x81, 0x00,              /*   Input (Data,Array,Abs) - key array */

    0xC0,                    /* End Collection */
};

此HID描述符定义的数据格式就是

  typedef struct __attribute__((packed)) {
      uint8_t modifier;    // 8bit: E0~E7 (LCTRL, LSHIFT, LALT, LGUI, RCTRL, RSHIFT, RALT, RGUI)
      uint8_t reserved;    // 保留
      uint8_t keys[6];     // 6个普通按键keycode(00~65),最多同时6键
  } hid_kbd_input_report_t; // 共8字节

因此我们此时就要区分修饰键普通按键的区别:

  • 按照HID的标准协议,键盘的按键标识就是通过一个字节来进行标识的
  • 同一个字节可以标识不同的按键,主要看此按键的位置是放在描述符定义的修饰按键字节处还是普通按键处
  • 比如:0X04在修饰按键中表示左ALT,在普通按键中表示按键A。

对于HID描述符,我们通常采用DT来辅助我们快速生成

在这里插入图片描述

在这里插入图片描述

之后就是利用HID描述符定义的report格式进行我们的数据发送即可。

最后再次以HID键盘获取按键为例子:

PC 是这样“知道哪个键”的:

1. 先读你的 HID 描述符
   知道输入报告是 8 字节,byte0 是修饰键、byte2..7 是键值数组。
2. 再按 HID 标准码表解释 keycode
   例如 0x04 解释为 A,0x1E 解释为 1,这是 USB HID Usage Tables 的标准,不是你自定义的。
3. 结合修饰键位一起算最终字符
   比如 byte0 的 Shift 位=1 且 keycode=0x04,PC 就当成 A(大写)而不是 a。
4. 靠“按下/松开”的报告变化判定状态
   按下时报告里有 keycode;松开时把对应 keycode 清掉(常见是发全 0),PC 就知道键释放了。
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐