码农的自我修养 - USB键盘和鼠标的数据包格式
USB Human Interface DevicesUSB人机接口设备(HID)是一种设备,正如其名称所暗示的那样,允许人类与计算机互动的接口。常见的例子包括USB鼠标、USB键盘、USB操纵杆和其他此类设备。USB HID设备使用的协议是在USB HID规范中定义的。一些芯片组支持将USB键盘和鼠标模拟成标准的PS/2设备,但许多芯片组不支持。因此,在一些可能根本没有PS/2端口的PC中,需要
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
![Logo](https://devpress.csdnimg.cn/79de2bf0b7994defa4242ef90d5513fa.jpg)
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)