目录
一、HID类设备相关概念
1. USB-HID名词解释
2. HID类设备数据传输特性
3. 按照传输速度对USB设备进行分类
二、USB设备描述符
1.USB标准描述符
1.1 各个描述符之间的关系
三、HID类设备特有的描述符
1. 设备描述符
2. 配置描述符
3. 接口描述符
4. 端点描述符
5. HID描述符
5.1 HID描述符的结构
5.2 HID描述符定义示例
6. 报告描述符
6.1 报告描述符的结构
6.2 示例
7.zephyr设备栈中报告描述符的注册和返回
7.1 报告描述符的注册示例(可忽略本小节)
7.2 注册回调函数
7.3 中断回调流程
7.3.1 控制器驱动层中断
7.3.2 核心层接口被回调
7.3.3 设备类驱动层接口被回调
7.4 报告描述符数据指针的更新以及数据的返回
7.5 总结
四、HID用途表文档简述—按键的用途ID
1. 用途表文档
2. 解析键盘按键值上报
2.1 按键的用途ID(查表可知)
2.2 输入报告的分析
2.3 实例1(单个按键按下)
3. 对比实验数据
3.1 勾选要监听的设备
3.2 开始捕获
3.3 实例2(组合按键按下)
五、解析程序代码(可忽略本小节)
1. HID类设备接口描述符的定义
2. 端点描述符在程序中的定义
3. HID报告描述符在程序中的定义
4. Shell命令行模拟键值的上报和数据的捕获
4.1 shell_hid_keyboard()函数
4.2 USB Hound捕获数据确认
Human Interface Deviece。指的是直接与人进行交互的设备,软硬件架构图如下(Layer Diagram):
一些典型的HID类设备包括:USB鼠标(Keyboards)、USB键盘(mouse)、游戏操纵杆(joysticks.)。这些都是标准设备,而HID协议还允许用户开发自定义的USB HID设备。
(1)交换的数据存储在被称为报告(report)的数据结构内。
(2)每一笔事务都能携带小量或者中量的数据:
低速设备:每一笔事务最大是8字节 全速设备:每一笔事务最大是64字节 高速设备:每一笔事务最大是1024字节 |
(3)设备可以在未预定的时间里【取决于用户的随机行为】传数据给主机(因此主机要定时轮询,以获得最新的数据)。
(1)三种传输速度 低速设备:Low Speed Device (LS) 全速设备:Full Speed Device (FS) 高速设备:Hight Speed Deveice (HS) (2)USB类(USB Class)和USB速度(USB Speed)的关系: USB Class是属于Application的。 USB Speed是属于Physical层的。 (3)市面上的HID产品中,绝大多数都是低速设备。[原因:低速设备成本和技术难度都比较低] |
USB是个通用的总线,硬件端口是统一的,但是USB设备却是多种多样的,那么,USB主机如何区分出所接入的不同的设备的呢?答案:依赖于不同设备各自的描述符。
当插入USB设备之后,主机会向从机发送一系列的命令,从机收到命令之后,会返回特定的描述符信息。主机通过解析收到的描述符,来识别到从机设备的相关信息。这个过程,就是设备枚举过程。
描述符的作用:给主机传达信息,从而让主机知道设备有什么功能、属于哪种类别、占用多少带宽、使用什么类别的传输方式、数据量的大小等等。主机确认这一系类的信息之后,双方才会开始进行数据通信。
USB有5种标准设备描述符,分别是:设备描述符、配置描述符、字符串描述符、接口描述符、端点描述符。
另外还有:HID描述符、报告描述符等各个类特有的描述符。
一个设备,只有一个设备描述符;一个设备描述符可以包含多个配置描述符;一个配置描述符可以包含多个接口描述符;一个接口描述符可以包含多个端点描述符。
包含关系如下图:
USB描述符之间的关系是一层一层的,所以,在枚举阶段,获取描述符的时候,应该从最顶层开始,顺序:设备描述符---------->配置描述符-------->……
英文对照
设备:Device 设备描述符:Device Descriptor 配置:Configuration 配置描述符:Configuration Descriptor 字符串:String 字符串描述符:String Descriptor 接口:Interface 接口描述符:Interface Descriptor 端点:Endpoint 端点描述符:Endpoint Descriptor 报告:Report 报告描述符:Report Descriptor |
每种描述符都有自己独立的编号(枚举阶段文档里面有详细的说明),使用C宏定义如下:
#define DEVICE_DESCRIPTOR 0x01 //设备描述符 #define CONFIGURATION_DESCRIPTOR 0x02 //配置描述符 #define STRING_DESCRIPTOR 0x03 //字符串描述符 #define INTERFACE_DESCRIPTOR 0x04 //接口描述符 #define ENDPOINT_DESCRIPTOR 0x05 //端点描述符 |
一个usb设备可以是单一类类型,也可以由多个类组合或者复合而成。类的定义是在接口描述符中指定的,而不是在设备描述符中设定的。
比如,假定某个设备为HID类设备,那么在定义接口描述符的时候,接口描述符的bInterfaceClass域就要赋值为0x03。
协议文档描述如下:
那么,怎么定义一个设备所用的类?一般来说,在设备描述符中,bDeviceClass、bDeviceSubClass、bDeviceProtocol这三个字段都设置为0,然后在接口描述符中,定义具体的类代码、子类代码、协议代码。--------------->单一的类设备定义!如果是组合设备,则设备描述符的这三个字段的值是固定的,必须设置如下:
.bDeviceClass = MISC_CLASS, //0xEF .bDeviceSubClass = 0x02, .bDeviceProtocol = 0x01, |
HID类设备相关的描述符,总共有:设备描述符、配置描述符、接口描述符、HID描述符(HID描述符的下级描述符有:报告描述符 和 物理描述符)、端点描述符。
如下图所示:
略,跟其他设备类一样,通用。
略,跟其他设备类一样,通用。
略,跟其他设备类一样,通用。
注意bInterval这个字段的设定。
偏移量/字节 |
域 |
大小/字节 |
说明 |
0 |
bLength |
1 |
描述符的总长度(取决于下级描述符的个数) |
1 |
bDescriptorType |
1 |
描述符类型(HID描述符 = 0x21) |
2 |
bcdHID |
2 |
HID协议版本 |
4 |
bCountyCode |
1 |
国家代码 |
5 |
bNumDescriptors |
1 |
下级描述符的数量 |
6 |
bDescriptorType |
1 |
下级描述符的类型 |
7 |
wDescriptorLenght |
2 |
下级描述符的长度 |
9 |
bDescriptorType |
1 |
下级描述符的类型 |
10 |
wDescriptorLenght |
2 |
下级描述符的长度 |
… |
… |
…(可选) |
说明:
(1) bLength:大小为1字节,是该描述符的总长度。比如,当只有一个下级描述符的时候,总长度就是:1 + 1 + 2 + 1 + 1 + 1 + 2 = 9 Byte
(2) bDescriptorType是描述符类型编号,HID描述符编号为0x21
(3) bcdHID:2字节,是设备所使用的HID协议的版本号。如果是HID1.1协议,则值为0x0110
(4) bCountyCode:设备所适用的国家。通常使用的键盘是美式键盘,代码为33,即0x21
(5) bNumDescriptors:下级描述符的数量。这个值至少为1,也就是HID类设备至少要有一个报告描述符。下级描述符可以是报告描述符或者物理描述符。
(6) bDescriptorType:下级描述符的类型。报告描述符的编号是0x22,物理描述符编号是0x23
(7) wDescriptorLenght:下级描述符的长度。
/* HID Descriptor */ sizeof(struct usb_hid_descriptor), /* bLength */ USB_HID_DESC, /* bDescriptorType */ LOW_BYTE(USB_1_1), /* bcdHID */ HIGH_BYTE(USB_1_1), 0x00, /* bCountryCode */ 0x01, /* bNumDescriptors */ USB_HID_REPORT_DESC, /* bDescriptorType */ LOW_BYTE(sizeof(hid_report_desc)), /* wDescriptorLength */ HIGH_BYTE(sizeof(hid_report_desc)), |
报告描述符和普通描述符一样,都是通过控制输入端点0来返回,主机下发获取报告描述符指令来获取设备的报告描述符。而且,这个请求是发送到接口的,并不是发送到设备(这是与获取其他标准描述符描述符的区别)。
报告描述符是最复杂的、结构最特异的一种描述符。关于HID用图表,在另一篇文章有介绍。
报告描述符包含多个报告,不同的报告通过报告ID来识别,报告的第一个字节就是报告ID。当报告描述符中没有定义报告ID时,开始就是数据。
报告描述符没有固定的长度,也没有固定的数据类型。而是由条目(item)来组成,一个条目占据一行。HID协议规定了两种条目:短条目和长条目,常用的是短条目。
短条目的构成:一字节的前缀 + 可选的数据字节。可选的数据字节可以是0、1、2、4字节。实际中所使用的条目,大部分是1字节的可选数据。
一字节前缀的结构(各个bit都有不同的含义):
7 6 5 4 3 2 1 0 |
||
bTag |
bType |
bSize |
说明:
1)最低两位D1~D0表示后面紧跟着的数据的字节数
0—0字节、1—1字节、2—2字节、4—4字节 |
2)D3~D2表示该条目的类型
0—主条目(main item)、1—全局条目(global item)、2—局部条目(local item)、3—保留 |
3)bTag表示该条目的功能(对照HID用图表[下文])。
主条目用来划分数据域,主条目共有5个
Input(输入) 、Output(输出) 、Feature(特性)、Collection(集合)、End Collection(闭集合) |
4)全局条目用来选择用途页。常用的全局条目有
Usage Page(用途页)、Logical Mininum(逻辑最小值) 、Logical Maxmum(逻辑最大值) 、Physical Mininum(物理最小值) 、Physical Maxmum(物理最大值)、Report Size(数据域大小)、 Report Count(数据域数量)、Report ID(报告ID) |
5)局部条目用来定义控制的属性。局部条目只在局部有效,遇到一个主条目之后,有效范围就被终止。常用的局部条目有
Usage(用途)、Usage Minmum(用途最小值)、Usage Maxmum(用途最大值) |
关于键盘的报告描述符、鼠标的报告描述符,可以用官方网站提供的HID描述符工具(HID Descriptor tool)生成(文末有共享链接);还可以使用现成的报告描述符进行修改;HID协议和用途表文档中,也有很多现成的例子。
键盘的报告描述符(只实现了几个按键)
const uint8 KeyBoardReportDescriptor[63] = { //(1)键盘上的Ctrl、Shift、Win、Alt 8个按键[在HID用图表文章中有详细说明] //(2)这个字节是全0数据,凑上去的,可以不要 //(3)LED灯用途页,用到了5个bit //(4)和上面的5个bit凑成一字节数据,所以这3个bit全0,没任何意义。 //(5)对应键盘上的所有的键值 |
这五部分的解释如下:
(1)这部分实际上为键盘的八个控制键,包括:左/右CTL,在/右ALT,在/右SHIFT,左/右WIN键盘,所以其范围为如下所示(HID_Usage_Tables.pdf从53页开始,展示了所有的keyborad page) 0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl) 八个键一个键对应于一个位所以: 0x75, 0x01, // REPORT_SIZE (1) report size单位为bit,report count为8,所以1*8共占用一个字节; 由于按键的值要么为1(按下),要么为0(松开),所以逻辑最大值为1,最小值为0 0x15, 0x00, // LOGICAL_MINIMUM (0) 由于按键为输入(对主机来说),所以为INPUT,并且为数据(Data),变量(var),绝对值(Abs) 0x81, 0x02, // INPUT (Data,Var,Abs) (2)这部分由于键盘数据的八个字节的第二个字节是保留的(第一个字节就是上面所描述的控制键部分),所以 0x95, 0x01, // REPORT_COUNT (1) 1bit*8 = 1 byte ,前且为常量。 (3)该部分LED输出,即如键盘上的大小灯,数字锁定灯等,只用了五个,所以report count为5. (4)由于(3)部分只用了5个bits,但发送肯定是一字节一字节地发送,所以要把不用的3个bits也要凑起来,但其又是没有实际意义的,所以定义为常量。 0x95, 0x01, // REPORT_COUNT (1) (5)这部分主要就是六个字节的键盘键值了(一个字节表示一个键),所以一次最多可以发送六个键值,即六个按键按下(当然有没有效,操作系统说了算,一般三个键同时按下系统就报错),所以这一整个报告描述符,包括一组输入的键,一组输出的LED。键一共有八个字节,即一起发送要发八个字节的数据,第1个字节是八个控制键,第2个字节是保留,第三至第八个字节为普通按键键值,没有固定位置,只需要往上填上HID Usage Tables上的键值系统即会确认为该键按下。输出的LED只有一个字节,一个位对应一个LED灯,只使用了五个位。 0x95, 0x06, // REPORT_COUNT (6) report size为8bits,有6个,所以刚好是六个字节。 |
参考:HID协议文档、HID用途表文档
报告描述符的注册和其他描述符的注册流程是一样的,由应用层向下注册,中断向上回调。通过更新数据指针来返回主机所要获取的描述符数据。
示例基于zephyr操作系统(非原生,实际上公司内部的设备栈已经基本上全部重构,唯一相似的就是调用调用框架还保留着)。
报告描述符单独定义在一个文件中,属于应用层。
报告描述符的注册是在应用层中完成的,示例如下:
文件:\samples\actions_sdk_demo\usb_audio_dongle\src\usb_handler.c
实质上是把定义好的描述符数据赋值给一个全局变量hid_device,返回给主机的数据从全局变量中取,更新指针的指向即可。
跳转到设备类驱动层分析:
在usb_hid_init()中,注册了整个报告描述符的长度信息:
注册一个HID设备实际上是创建一个HID设备,使用的是基本框架。在应用层/设备类驱动层中定义类请求处理句柄函数、厂商请求处理句柄函数、自定义请求处理句柄函数等,通过调用核心层提供的接口usb_set_config()向设备栈注册中断回调函数,最后调用usb_enable()创建一个类设备。
文件:subsys\usb\class\hid\core.c(设备类驱动层)
设备配置结构体如下
指向报告描述符的指针的更新是在自定义请求处理句柄函数中完成,函数如下:
所以,接下来就要分析设备类驱动层里面这个回调函数什么时候会被回调。
控制器驱动层:\drivers\usb\device\aotg_udc.c,从控制器中断服务函数usb_aotg_isr_handler()开始分析
建立事务的令牌包和数据包都会引起中断,DATA0数据包是8字节的标准请求(端点0上的数据包中断)。
8字节标准请求:81 06 00 22 00 00 51 01
结合串口打印的信息:
[general] [WRN] usb_handle_control_transfer: bmRequestType:0x81 [general] [WRN] usb_handle_control_transfer: bRequest:0x06 [general] [WRN] usb_handle_control_transfer: wValue:0x2200 [general] [WRN] usb_handle_control_transfer: wIndex:0x0000 [general] [WRN] usb_handle_control_transfer: wLength:0x0151 |
标准请求分析如下:
0x81:这是一个标准请求,请求的接收者是接口。 0x06:这是一个获取描述符的标准请求。 0x2200:HID协议定义的报告描述符。 0x0151:主机请求337 Byte的报告描述符数据。 |
首先,USB控制器产生中断事件,核心层的usb_handle_control_transfer()最先被回调。在处理SETUP令牌包中断时,调用usb_handle_request()。
在usb_handle_request()中,根据请求的类型(标准请求[type=0]、类请求[type=1]、厂商[type=2]、自定义)再调用核心层的接口。
这是来自主机的标准请求。而对于来自主机的标准请求、类请求、厂商请求或者是自定义的请求,在配置USB设备的时候,都注册了一系列的处理句柄函数。
所以,接下来在核心层被调用的是usb_handle_standard_request(),传入的是8字节请求数据和数据指针。
首先,都会先查询应用层是否注册了自定义请求的处理句柄,如果应用层向设备栈中注册了自定义请求处理句柄函数,则自定义请求处理句柄函数就会被回调。上文7.2小节分析到,在core.c中,HID类设备向设备栈中注册了自定义请求处理句柄函数,所以指针usb_dev.custom_req_handler不为空,于是就进行回调,回传的参数是8字节的数据字以及数据指针。
hid_custom_handle_req()被回调
数据指针在设备类驱动层的hid_custom_handle_req()中被更新,指针指向最初注册报告描述符时,保存报告描述符数据的全局变量hid_device。
数据指针得到更新之后,回到核心层中调用usb_data_to_host()将报告描述符的数据返回给主机。
(1)报告描述符是单独分开进行注册的,注册描述符的数据信息和长度信息。
(2)设备类驱动层在函数hid_custom_handle_req()中处理主机获取报告描述符的请求。
(3)对于主机的标准请求,接收者可以是设备、端点、接口。而在设备栈中最先判断的是自定义请求处理句柄函数是否有定义。所以,在设备类驱动层,一般都会定义自定义请求处理句柄函数,直接返回0即可。
(4)可以注册shell子系统,模拟按键。
(5)主机请求HID报告描述符时,请求的数据长度一般都会比实际长度大得多。且报告描述符是通过端点0返回的。
HID_Usage_Tables.pdf从53页开始。定义了各个按键的用途ID。
…………………………………………….
内容太多,等实际用上,再去查HID用途表。
以小键盘上的数字键为例,Keypad指的是小键盘。
Keypad 1 and End: 小键盘上的数字键1 Keypad 2 and Down: 小键盘上的数字键2 Keypad 3 and PageDown: 小键盘上的数字键3 …… |
对应的键盘实物图如下:
从用图表中,我们可以查到这小键盘上几个数字按键的用途ID,当按下时,用途ID就会被上报,如下:
第一列:十进制数 第二列:十六进制数
需要关注的是,第二列的数值及其对应的按键。
数据从设备返回到主机,对于HID类设备,数据的返回通过报告来实现。端点发送报告数据。
通过分析BusHound上捕获到的报告描述符数据可知:
键盘返回8字节的输入报告。通过报告描述符和HID用途表文档可知:
1)Buf[0]这个字节的位0 D0:左Ctrl键
位1 D1:左Shift键
位2 D2:左Alt键
位3 D3:左win键
位4 D4:右Ctrl键
位5 D5:右Shift键
位6 D6:右Alt键
位7 D7:右win键
按下,则对应的位为1。
2)Buf[1]:保留,即0000 0000(HEX)=0(DEC)
3)Buf[2]~ Buf[7]是按键值。普通键最多只有5个,因此不会超过6个。对于实际的键盘,如果同时按下几个按键,则后面的六个字节Buf[2]~ Buf[7]都为0xFF。表示按下的键太多,无法正确返回。
以小键盘上的数字键为例,并且操作是:一次只按一个按键!下表就是理论上,键盘返回的8个字节的数据。
按键 |
Buf[0] |
Buf[1] |
Buf[2] |
Buf[3] |
Buf[4] |
Buf[5] |
Buf[6] |
Buf[7] |
1 |
0x00 |
0x00 |
0x59 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
2 |
0x00 |
0x00 |
0x5a |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
3 |
0x00 |
0x00 |
0x5b |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
4 |
0x00 |
0x00 |
0x5c |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
5 |
0x00 |
0x00 |
0x5d |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
6 |
0x00 |
0x00 |
0x5e |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
7 |
0x00 |
0x00 |
0x5f |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
8 |
0x00 |
0x00 |
0x60 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
9 |
0x00 |
0x00 |
0x61 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
注意:Buf[2]的值和用图表上用途ID
使用USB Hound这个软件来捕获键盘的数据。
以按下数字键1为例,进行实际捕获到的数据和理论数据的对比。
其他按键可依次类推,进行验证,这个例子是验证单个按键按下的。
比如:Ctrl+v ,这里的Ctrl指的是左边的Ctrl。
8字节的理论值:
按键 |
Buf[0] |
Buf[1] |
Buf[2] |
Buf[3] |
Buf[4] |
Buf[5] |
Buf[6] |
Buf[7] |
Ctrl+v |
0x01 |
0x00 |
0x19 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
首先,左边的Ctrl按下,那么Buf[0]的第一位就是1,所以字节数据就是0x01;经过查表可知,v键的用途ID为0x19。
按下Ctrl不放,再按v键,数据捕获如下:
可以看见实验值和理论值对应。
在捕获数据进行分析的过程中,还可以发现,按键按下上报一次数据,按键松开上报一次。这是因为开发键盘的人,在程序里,逻辑是这样的:在判断按键事件时,只有当按键情况发生改变的时候(要么按下、要么松开),才需要返回报告,按键一直按住,是不需要返回报告的。
如:按下小键盘数据1键不放:
然后松开,发现多出了一次上报的数据:
因此,按下一个按键再松开,应该是有两次数据的。
弄清按键上报的值(Buf[0]~Buf[7])是至关重要的,接下来就可以实现标准的USB键盘设备(只要能使用USB芯片将这些数据发给主机,即可实现一模一样的键盘)。
USB HID设备是通过报告来传送数据的。报告分为:输入报告 和 输出报告。
输入报告:USB设备发给主机的。比如:USB鼠标将移动和点击等信息返回给电脑。
输出报告:是主机发送数据给USB设备的。比如:数字键盘锁定灯和大写字母锁定灯。即:输入和输出,数据流的方向是相对主机而言的。
报告是一个数据包,数据包里面包含着所要传送的数据(比如:按键值Buf[0]~Buf[7])。
报告的传输方式
1)对于输入报告,则一定是通过中断输入端点输入的。
2)对于输出报告,如果没有中断输出端点时,就是用控制输出端点0输出;如果有中断输出端点,则通过中断输出端点发出数据包。
下面的分析基于zephyr操作系统。
解析
(1)信息1:在程序中,定义了两个端点描述符。其中,他们的都是:“中断传输类型”的端点。
.bmAttributes = USB_DC_EP_INTERRUPT 在源码中,USB_DC_EP_INTERRUPT的定义处: 由此可知,USB_DC_EP_INTERRUPT的值为3,也就是0x03。而对于端点描述符的bmAttributes字段,在枚举阶段解析文档中,有讲过其含义。 bmAttributes描述端点的属性
即:目前该USB设备定义的两个端点,使用的数据传输类型为:中断传输类型。 |
(2)信息2:关于端点地址bEndpointAddress
从枚举阶段,主机请求配置描述符集合的130个字节数据中,提取出来(如果是捕获其他厂商的产品)。
主机请求配置描述符集合数据。
定义配置描述符集合时,各个描述符的先后顺序如下:
配置描述符----->接口描述符---->HID描述符----->端点描述符---->……
把定义的两个端点描述符数据提取出来,如下:
07 05 81 03 40 00 01 07 05 01 03 40 00 01 |
D7 D6~D4(保留,都为0) D3~D1
第一个端点描述符的地址数据为:0x81= 1 000 0001
第一个端点描述符的地址数据为:0x01= 0 000 0001
D7:表示数据的传输方向,输入为1,输出为0(输入或者输出相对于主机而言)
D3~D1:表示端点号,两个端点的端点号都为1。
中断输出端点1 中断输入端点1 |
该USB设备的非0端点就有两个,并且设备的类为HID类。这两项重要的信息,被定义在接口描述符中(根据描述符之间的包含关系,一个接口描述符可以包含多个端点描述符,所以一些信息,得返回它的上一级描述符里面去确认),回顾一下接口描述符的定义:
那么,对于这个HID设备,输入报告通过中断输入端点发送到主机。而输出报告,由中断输出端点将数据发给设备。
HID类设备模拟键值上报的所有函数,都在这个文件里:
ATS350B\samples\actions_sdk_demo\usb_audio_dongle\src\hid_handler.c
该函数完成了8个字节键值的上报。定义如下:
模拟上报小键盘数据1键,并使用USB Hound捕获
下一篇:HID设备实例 USB_HID设备实例_卡卡的博客-CSDN博客