USB Human Interface Devices
USB人机接口设备(HID)是一种设备,正如其名称所暗示的那样,允许人类与计算机互动的接口。常见的例子包括USB鼠标、USB键盘、USB操纵杆和其他此类设备。USB HID设备使用的协议是在USB HID规范中定义的。一些芯片组支持将USB键盘和鼠标模拟成标准的PS/2设备,但许多芯片组不支持。因此,在一些可能根本没有PS/2端口的PC中,需要一个USB HID驱动程序。
准备知识
USB描述符是在USB设备插入后,主机将从USB设备读取其配置信息,这些配置信息就叫做USB Descriptor,是枚举USB设备成功所必须的。
在这些信息中,包含了Interface descriptor,叫做接口描述符,表明这个设备的接口相关信息。
接口描述符下,还有端口描述符,表示用来传数据的具体通道。比如Endpoint 0用来传输控制数据。
如下图:
1. 协议
USB HID设备主要基于两个协议:report protocol和boot protocol。
这两种协议都是用report来传输数据,不过是用两种不同类型的report。
report是一种数据结构,从设备发送给主机,也可以从主机发送给设备。当设备向主机发送报告时,它通常包含状态变化信息,如按键、鼠标移动等。当主机向设备发送报告时,它通常包含配置设备的命令,例如设置键盘上的LED。这个协议当然取决于标准的USB框架。USB HID设备使用中断传输进行通信,因为它们并不总是传输数据,但当它们传输时,需要软件做出非常快的响应,同时传输的数据通常也很小。
Report protocol是基于 "items"的概念,其结构在报告描述符(USB描述符里包含的)中定义。
boot protocol要简单得多,它遵循鼠标和键盘的标准结构。为了简单起见,本文至少在目前只讨论boot protocol。
boot protocol就是指在系统启动时就支持的协议类型,比如电脑只要上电,在BIOS阶段就可以使用键盘鼠标。这种协议是基本的简单的HID协议。
检测HID设备
HID设备在其设备描述符中的类/子类值都是0,而在其接口描述符中的类/子类值是有效的。请记住,接口描述符不能被手动请求,必须与配置和端点描述符一起获得。接口描述符中标识HID设备的类值是3,接口描述符中的子类值可以是1,表示设备支持boot protocol.,也可以是0,表示设备只支持report protocol。接口描述符中的协议字段也决定了它是一个鼠标还是一个键盘。具体来说,1表示HID设备是一个键盘,而2表示HID设备是一个鼠标。
"SetProtocol "请求
假设一个USB HID设备支持boot protocol,如上节所述,它的类值为3,子类值为1,驱动软件可以选择要使用的协议。它使用 "SetProtocol "请求来告诉设备它是想使用report protocol还是boot protocol。为了简单起见,本文暂时只描述boot protocol。为了发送 "SetProtocol "请求,软件向设备的控制端点0发送了一个常规的SETUP事务。SETUP数据包的请求类型包含0x21,"SetProtocol "的请求代码是0x0B,SETUP数据包的值字段应该包含0来表示boot protocol,或者1来表示report protocol。索引和长度字段都必须是0,因为索引是未使用的,这个请求没有数据阶段。这条命令只在完全支持引导协议的设备上支持。使用这个命令后,所有从设备发送到主机的报告都是boot reports或 regular reports,这取决于软件要求的类型。
"GetReport "请求
软件可以使用控制端点和常规的SETUP数据包向USB设备请求报告。SETUP数据包的请求类型将包含0xA1,"GetReport "的请求代码是1,SETUP数据包的 "值 "字段将包含0x0100,以请求一个ID为0的输入数据包,而长度字段是主机希望接收的数据阶段的长度。SETUP数据包应该发送到设备端点0(控制端点)。 对于键盘,数据阶段通常是8个字节,而对于鼠标,数据阶段的前3个字节是标准格式,而其余部分可能被设备的特定功能所使用。推荐只是为了测试设备初始化是否成功完成,或者类似的情况,才用这种方式接收报告。"GetReport "请求不应该被用来轮询HID设备的变化,因为SETUP和STATUS阶段浪费了太多的时间。相反,软件应该使用中断传输轮询HID,使用中断IN端点。
中断端点 / Interrupt endpoint
一般建议HID使用中断传输向软件报告,软件通常应避免使用上面提到的 "GetReport "请求。驱动软件应该请求HID设备的配置描述符。一个HID设备必须至少支持一种配置。然后,软件应该扫描端点描述符,寻找表示 "中断IN "类型的描述符,这是一个使用中断传输将设备数据发送给主机的端点。软件应该保存该端点的4位ID,以及该端点的8位间隔。间隔值以毫秒(ms)为单位编码时间,在这个时间空间内,软件应该轮询一次报告数据包。例如,如果间隔值是8,软件应该每8毫秒向设备请求一份报告。如果软件过早地请求报告,例如,在6毫秒之后,设备可能会发送与之前相同的数据包,或者它可能不发送任何东西,而是返回NAK。如果软件在时间范围之后要求报告,例如9毫秒,那么设备将发送新的数据包。软件使用这种描述的方法不断轮询USB HID设备。这也是优化你的USB代码的好机会,因为轮询功能将每秒运行数百次。比如有的USB鼠标的间隔值是10毫秒,所以软件每秒钟轮询USB设备100次。
------------------------------------------------
2. USB键盘
USB键盘使用报告与软件进行通信,就像其他HID设备一样。在接口描述符中,USB键盘的类别代码为3,协议值为1,可以被检测到。为了简单起见,在这里仅描述启动协议.
Report格式
这个报告必须由软件使用中断传输请求,每隔若干毫秒一次,间隔时间在USB键盘的中断IN描述符中定义。USB键盘报告的大小可以达到8个字节,尽管并不是所有这些字节都被使用,只用前三或四个字节实现一个适当的实现也是可以的。
Offset |
Size |
Description |
0 |
Byte |
Modifier keys status. |
1 |
Byte |
Reserved field. |
2 |
Byte |
Keypress #1. |
3 |
Byte |
Keypress #2. |
4 |
Byte |
Keypress #3. |
5 |
Byte |
Keypress #4. |
6 |
Byte |
Keypress #5. |
7 |
Byte |
Keypress #6. |
Modifier keys status:
这个字节是一个位域,每个位对应一个特定的modifier key。当一个位被设置为1时,相应的modifier key正在被按下。与PS/2键盘不同,USB键盘没有modifier keys的 "scancodes"。这个字节的位结构是:
Bit |
Bit Length |
Description |
0 |
1 |
Left Ctrl. |
1 |
1 |
Left Shift. |
2 |
1 |
Left Alt. |
3 |
1 |
Left GUI (Windows/Super key.) |
4 |
1 |
Right Ctrl. |
5 |
1 |
Right Shift. |
6 |
1 |
Right Alt. |
7 |
1 |
Right GUI (Windows/Super key.) |
当软件收到一个中断,例如,其中一个Shift modifier keys被设置为1时,软件应在scancode表中获取相应的shift modifier keys对应的scancode。
Reserved field:这个字节是由USB HID规范保留的,因此软件应该忽略它。
按键字段:一个键盘报告最多可以显示6个按键。所有这些值都是无符号的8位值(与PS/2扫描码不同,PS/2扫描码大多是7位),表示被按下的键。请参考USB编码到ASCII字符转换表。
按键机制
当按键被按下或释放时,USB键盘会发送中断,就像PS/2键盘一样。然而,与PS/2键盘不同,USB键盘没有 "make "和 "break"的scancodes概念。当用户按下一个键时,中断会在其中一个按键字段中出现一个扫描码值。当一个键被释放时,相应的按键字段在下一个数据包中被返回为零。为了更清楚地说明这一点,并说明为什么有一个以上的按键扫描码字段,让我们看一下下面的例子。假设用户按下了 "A "键,即0x04的scancode。返回的中断数据包会是这样的:
00 00 04 00 00 00 00 00
注意modifier keys是零,因为用户没有按任何modifier keys。保留字段也是零,正如USB HID规范所建议的。第一个按键字段包含0x04,对应于 "A "键。现在,让我们假设用户放开了 "A "键。发送的数据包是:
00 00 00 00 00 00 00 00
现在,让我们假设用户按了 "A "键,然后按了 "B "键(scancode 0x05)而没有放开 "A "键:
00 00 04 05 00 00 00 00
注意到一个中断数据包是如何能够将两个按键一起传输的。现在我们假设用户按下 "C "键(scancode 0x06),而没有放开 "A "或 "B "键:
00 00 04 05 06 00 00 00
现在,如果用户放开 "A "键,但继续按 "B "和 "C "键,会怎样?键盘将显示 "A "不再被按下,而 "B "和 "C "将向数据包的开头移动:
00 00 05 06 00 00 00 00
现在可能已经很明显了,USB键盘按照先按下哪个键的顺序返回扫描码。因此,如果第一个按键字段为零,就说明没有任何按键被按下发送扫描码。如果它不是零,软件可以检查接下来的字段,看看是否有另一个键也被按下。
Modifier keys 的概念可能是显而易见的,但为了完整起见,让我们假设用户同时按下了 "X "键(扫描码0x1B)和left shift键。 发送的中断数据包将包含:
02 00 1B 00 00 00 00 00
请注意,修改器字段的bit 1(值为0x02)被设置,以表明左移键正在被按下。
还有一个 "phantom condition",你可以把它看作是溢出。一个USB键盘数据包在一次传输中最多可以显示6个按键,但我们设想一下,有人掉了键盘,一次按了超过6个键。键盘将进入phantom condition状态,在这种情况下,所有报告的按键将是无效的scancode 0x01。然而,Modifier keys仍将被报告。想象一下,有8个键(或超过6个的任何随机数字)被按下,并且右移键也被按下。发送的数据包将看起来像:
20 00 01 01 01 01 01 01
请注意,modifier keys仍在被指示,但实际的代码都返回了phantom condition。除了phantom condition,还有其他特殊的代码。0x00表示没有编码,也没有按键被按下,0x01表示我们刚刚解释过的phantom condition,0x02表示键盘自检失败,0x03表示发生了一个未定义的错误。从0x04开始,扫描码是有效的,对应于 "真正的 "按键。如果发生phantom condition,设备驱动程序可以忽略它。
自动重复 / Auto-repeat
让USB键盘感到痛苦的一点是,在硬件中没有自动重复和自动重复延迟的机制,这必须完全在软件中实现,这与PS/2键盘不同。以自动重复延迟500毫秒和自动重复速度每秒10个字符为例,当软件意识到某个键被按下而一直没有被释放时,它就会在500毫秒后(或任何你喜欢的自动重复延迟)忽略除了这个按下超过500ms的其他按键,之后如果这个键还保持按下,驱动程序就会报告每1秒按10次键,或任何你想要的自动重复速度。这将使用户感到自然,就像你按住一个键一样;第一个键出现在屏幕上,计算机等待一个短暂的延迟,然后键就会不断出现。
LED灯
LED灯也是在软件中处理的,根据硬件,NumLock、CapsLock和ScrollLock是正常的按键,可以发送正常的scancode。当这些键被按下时,驱动程序负责操作LED灯。当按下CpasLock按键,发送给PC,然后PC上的驱动程序再向键盘发送设置Led灯的指令。所以有时判断PC是否死机,只需按下这几个和LED灯显示有关的按键,如果不能操作,则操作系统一般就是挂了,没法再通过驱动程序来控制LED灯。
为了设置LED灯,驱动程序使用标准的USB setup事务向设备发送一个SetReport请求,其中有一个字节的data stage。设置包的请求类型应该包含0x21,SetReport的请求代码是0x09。设置数据包的值域在低字节中包含报告ID,它应该是0。高字节包含报告类型,应该是0x02,表示一个输出报告,或者一个从软件发送到硬件的报告。
索引字段应包含USB键盘的接口号,这是在接口描述符中存在的数字,表明这个设备是一个USB键盘。数据阶段应该是1个字节,是一个位域。这个设置事务应该被传输到控制端点0,这在所有的硬件上都适用。其他硬件可能支持也可能不支持可选的中断OUT端点。如果硬件支持中断输出端点,你可以只将1字节的数据stage传输到中断输出端点,而不需要SETUP阶段和STATUS阶段的额外开销。如果硬件支持中断输出端点,你应该尽可能避免使用控制端点,因为中断输出端点速度更快,可以用中断传输来编程,而不是setup传输。1字节的数据stage(用于SETUP事务)或1字节的中断OUT传输的格式如下所示。当一个位被设置为1时,相应的LED被打开。
Bit |
Bit Length |
Description |
0 |
1 |
Num Lock. |
1 |
1 |
Caps Lock. |
2 |
1 |
Scroll Lock. |
3 |
1 |
Compose. |
4 |
1 |
Kana. |
5 |
3 |
Reserved, must be zero. |
------------------------------------------------
3. USB鼠标
USB鼠标,就像其他HID设备一样,使用reports与软件进行通信,reports通过中断端点发送,或者可以通过 "GetReport "请求手动请求。USB鼠标在接口描述符中的协议值为2。
报告格式
该报告必须由主机使用中断传输来请求,每隔若干毫秒一次。间隔是在USB鼠标设备的中断IN描述符中定义的。USB鼠标报告中只有前三个字节被定义。其余的字节,如果存在,可用于设备特定的功能。软件可以在中断传输中只请求三个字节,即使实际数据包较大,也不会造成错误。下表定义了使用boot protocol操作的USB鼠标的报告格式。
Offset |
Size |
Description |
0 |
Byte |
Button status. |
1 |
Byte |
X movement. |
2 |
Byte |
Y movement. |
Button status:这个字节是一个位域,其中最低的三个位是标准格式。其余的5位可用于设备的特定用途。
Bit |
Bit Length |
Description |
0 |
1 |
When set to 1, indicates the left mouse button is being clicked. |
1 |
1 |
When set to 1, indicates the right mouse button is being clicked. |
2 |
1 |
When set to 1, indicates the middle mouse button is being clicked. |
3 |
5 |
These bits are reserved for device-specific features. |
X movement:
这是一个有符号的8位整数,代表X运动。第7位(值为0x80)决定了该值的符号。当这个值为负数时,鼠标被向左移动。当这个值为正数时,鼠标被向右移动。注意,与PS/2鼠标不同,USB鼠标的移动值是8位有符号整数,而不是9位整数。
Y movement:
这也是一个有符号的8位整数,代表Y移动。当这个值为负数时,鼠标被向上移动。当该值为正时,鼠标被向下移动(朝向用户)。
------------------------------------------------
附录:
USB规范为键盘位置规定了16位的按键代码(但实际上就是用一个字节,其他范围的保留或未定义),并为通常的美国布局确定了按键说明。下面的值是以十进制给出的。0-3是协议值,分别是NoEvent、ErrorRollOver、POSTFail、ErrorUndefined。224-231是modifier keys的值。
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
- |
err |
err |
err |
A |
B |
C |
D |
E |
F |
G |
H |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
I |
J |
K |
L |
M |
N |
O |
P |
Q |
R |
S |
T |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
U |
V |
W |
X |
Y |
Z |
1 |
2 |
3 |
4 |
5 |
6 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
7 |
8 |
9 |
0 |
Enter |
Esc |
BSp |
Tab |
Space |
- / _ |
= / + |
[ / { |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
] / } |
\ / | |
... |
; / : |
' / " |
` / ~ |
, / < |
. / > |
/ / ? |
Caps Lock |
F1 |
F2 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
F3 |
F4 |
F5 |
F6 |
F7 |
F8 |
F9 |
F10 |
F11 |
F12 |
PrtScr |
Scroll Lock |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
Pause |
Insert |
Home |
PgUp |
Delete |
End |
PgDn |
Right |
Left |
Down |
Up |
Num Lock |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
KP / |
KP * |
KP - |
KP + |
KP Enter |
KP 1 / End |
KP 2 / Down |
KP 3 / PgDn |
KP 4 / Left |
KP 5 |
KP 6 / Right |
KP 7 / Home |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
KP 8 / Up |
KP 9 / PgUp |
KP 0 / Ins |
KP . / Del |
... |
Applic |
Power |
KP = |
F13 |
F14 |
F15 |
F16 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
F17 |
F18 |
F19 |
F20 |
F21 |
F22 |
F23 |
F24 |
Execute |
Help |
Menu |
Select |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
Stop |
Again |
Undo |
Cut |
Copy |
Paste |
Find |
Mute |
Volume Up |
Volume Down |
Locking Caps Lock |
Locking Num Lock |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
Locking Scroll Lock |
KP , |
KP = |
Internat |
Internat |
Internat |
Internat |
Internat |
Internat |
Internat |
Internat |
Internat |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
LANG |
LANG |
LANG |
LANG |
LANG |
LANG |
LANG |
LANG |
LANG |
Alt Erase |
SysRq |
Cancel |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
Clear |
Prior |
Return |
Separ |
Out |
Oper |
Clear / Again |
CrSel / Props |
ExSel |
|||
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
||||
LCtrl |
LShift |
LAlt |
LGUI |
RCtrl |
RShift |
RAlt |
RGUI |
参考:
USB Human Interface Devices - OSDev Wikihttps://wiki.osdev.org/USB_Human_Interface_Devices
Keyboard scancodes: USBhttps://www.win.tue.nl/~aeb/linux/kbd/scancodes-14.html