USB固件开发(HID设备)
1. HID设备的识别
HID设备类除了有文档第一部分所述的一些标准描述符(包括设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符)外,还有自己的类专有描述符:
HID描述符
报告描述符
物理描述符
正确实现HID设备类专用描述符是主机成功识别HID设备的关键。HID描述符和报告描述符是必须要使用的,物理描述符一般不被使用。
1.1 HID描述符
HID描述符跟接口描述符、端点描述符类似,也是随配置信息一起返回给主机的,主机并不会单独发出请求来读取它。HID描述符在配置信息中的位置是紧接接口描述符。例如:
_Config_Descriptor:
.dw _Config_Descriptor_End-_Config_Descriptor //bLength: 0x09 byte
.dw 0x02 //bDescriptorType: CONFIGURATION
.dw _Config_Descriptor_Total-_Config_Descriptor //wTotalLength:
.dw 0x00
.dw 0x01 //bNumInterfaces: 1 interfaces
.dw 0x01 //bConfigurationValue: configuration 1
.dw 0x00 //iConfiguration: index of string
.dw 0xC0 //bmAttributes: self powered, Not Support Remote-Wakeup
.dw 0x32 //MaxPower: 100 mA
_Config_Descriptor_End:
_HID_Interface_Descriptor:
//Interface 1 (0x09 byte)
.dw 0x09 //bLength: 0x09 byte
.dw 0x04 //bDescriptorType: INTERFACE
.dw 0x01 //bInterfaceNumber: interface 0
.dw 0x00 //bAlternateSetting: alternate setting 0
.dw 0x01 //bNumEndpoints: 1 endpoints(EP1)
.dw 0x03 //bInterfaceClass: 人机接口设备(HID)类
.dw 0xff //bInterfaceSubClass: 供应商定义
.dw 0xff //bInterfaceProtocol 使用的协议:供应商定义
.dw 0x00 //iInterface: index of string
_HID_Interface_Descriptor_End:
_HID_Descriptor:
.dw 0x09 //bLength: 0x09 byte
.dw 0x21 //bDescriptorType: HID描述符类型编号
.dw 0x01, 0x10 //HID类协议版本号,为1.1
.dw 0x21 //固件的国家地区代号,0x21为美国
.dw 0x01 //下级描述符的数量
.dw 0x22 //下级描述符为报告描述符
.dw _ReportDescriptor_End-_ReportDescriptor, 0x00 //下级描述符的长度
_HID_Descriptor_End:
_Endpoint3:
.dw 0x07 //bLength: 0x07 byte
.dw 0x05 //bDescriptorType: ENDPOINT
.dw 0x83 //bEndpointAddress: IN endpoint 3
.dw 0x03 //bmAttributes: Interrupt
.dw 0x02, 0x00 //wMaxPacketSize: 2 byte
.dw 0x0A //bInterval: polling interval is 10 ms
_Config_Descriptor_Total:
HID描述符其实是为了提供下级描述符(如报告描述符)的信息。
下图更清楚地表述了各描述符之间的层次关系。
1.2 报告描述符
要解释报告描述符,首先得清楚什么是“报告”。“报告”是主机和HID设备之间进行数据交换的最小单位。也就是说,在主机完成对设备的识别之后,在具体应用上的数据交换就得以“报告”的方式进行。“报告”的类型有三种:输入报告、输出报告和特征报告。输入报告就是设备发给主机的报告,而输出报告就是主机发给设备的报告,特征报告是主机发给设备的报告,特征报告常在自定义HID设备中被用作主机向设备发送自定义数据。
报告描述符,顾名思义就是描述“报告”格式的,这个格式使主机和设备能遵循着同一个规则来解释一个报告中所含有的数据。与HID描述符不同,主机会发出单独的请求来读取报告描述符。关于报告描述符的组成,HID设备类定义文档中明确指出,一个报告描述符必须包含但不仅限于以下数据项:
输入(输出或特征)
用法(也可用“用法最小值与最大值”来定义一连串用法)
用法页
逻辑最小值
逻辑最大值
报告大小
报告计数
报告描述符看起来比较复杂,无论是HID设备类定义文档,还是其他参考书籍,都会花较大的篇幅来阐述它。要把它完全理解是需要一点时间的,而且就算是理解了也不一定能写出“像样”的报告描述符来。学习总有一个过程,入门才是最重要的,只要入了门,后面的事情就会慢慢变得简单,无需在一开始的时候就面面俱到。所以这里只对上面提到的必需的数据项进行解释及举例说明。
输入项(输出或特征)指明了报告的类型,其中隐含了报告的传输方向以及报告数据所具有的数学特性。
用法和用法页一起指明了数据项的用法,每个数据项都必须指明用法,否则主机端不能成功解析报告描述符。用法页是全局的,修饰列于其后的所有数据项,直到出现新的用法页为止;用法则是局部数据项,局部数据项只修饰列于其后的第一个主数据项内的数据项,一旦出现新的主数据项,那么用法必须重新指定。这其中隐含的意思是,每个主数据项前面都必须有修饰它的用法与用法页组合。(“用法”表示的是一个单独的用法,而“用法最小值”和“用法最大值”可以替代“用法”,代表某个范围的用法。)
逻辑最小值和逻辑最大值指明了报告所使用的数据值的范围,这个数据值是以逻辑单位为基础的,与报告大小有着对应关系。
报告大小指明数据项的位数。报告计数指明有多少个这样的数据项。
例如,定义以下数据项:
逻辑最小值(0)
逻辑最大值(0x7f)
报告大小(8)
那么它的意思就是,此报告中数据字段的大小是8位,本身可以表示0~255之间的任何数,但是逻辑值的范围被定义在0~127之间,所以实际上数据字段的数据不能超过127,否则视为无效报告。
再举一个例子:
逻辑最小值(0)
逻辑最大值(3)
报告大小(2)
这个例子的意思是,此报告中数据字段的大小是2位,逻辑值范围是0~3,那么数据字段的值与逻辑值是一一对应且相等的,即0(00b),1(01b),2(10b),3(11b)。
第三个例子:
再举一个例子:
逻辑最小值(-1)
逻辑最大值(1)
报告大小(2)
这个例子的意思是,此报告中数据字段的大小是3位,逻辑值范围是-1~1,那么数据字段的值与逻辑值是按左对齐的方式部分对应的,即数据字段值0(00b)对应逻辑值-1,数据字段值1(01b)对应逻辑值0,数据字段值2(10b)对应逻辑值1,数据字段值3(11b)无效。
这里举一个HID自定义设备的报告描述符的例子,这个例子比鼠标和键盘更简单。更具体的内容,譬如常用的鼠标和键盘,可以参看官方文档Device Class Definition for Human Interface Devices(HID).pdf 和HID Usage Tables.pdf。
_ReportDescriptor: //报告描述符
.dw 0x06, 0x00, 0xff //用法页,供应商自定义,修饰其下所有的主项
.dw 0x09, 0x01 //用法(供应商用法1),局部项,只修饰下面的“集合”主项。
.dw 0xa1, 0x01 //集合开始,主项
.dw 0x85, 0x1 //报告ID(1),全局项,可以修饰其下所有的主项,但是在这个报告描述中由于后面出现了新的报告ID,所以它只是修饰下面的“输入”主项。
.dw 0x9, 0x1 //用法(供应商用法1)
.dw 0x15, 0x0 //逻辑最小值(0),全局项,修饰下面所有的主项
.dw 0x26, 0xff, 0x0 //逻辑最大值(255),全局项,修饰下面所有的主项
.dw 0x75, 0x8 //报告大小(8),全局项,修饰下面所有的主项
.dw 0x95, 0x7 //报告计数(7),全局项,修饰下面所有的主项
.dw 0x81, 0x6 //输入(数据,变量,相对值),主项,说明此报告的属性
//下面开始一个新的主项目,前面提到的全局项仍对这个主项目有效,譬如报告大小等
.dw 0x09, 0x01 //用法(供应商用法1) ,局部项,修饰下面的“特征” 主项
.dw 0x85, 0x03 //报告ID(3),全局项,之前的报告ID项失效
.dw 0xb1, 0x6 //特征(数据,变量,相对值)
//下面开始一个新的主项目,前面提到的全局项仍对这个主项目有效,譬如报告大小等
.dw 0x09, 0x01 //用法(供应商用法1) ,局部项,修饰下面的“特征” 主项
.dw 0x85, 0x02 //报告ID(2),全局项,之前的报告ID项失效
.dw 0xb1, 0x06 //特征(数据,变量,相对值)
//下面开始一个新的主项目,前面提到的全局项仍对这个主项目有效,譬如报告大小等
.dw 0x09, 0x01 //用法(供应商用法1) ,局部项,修饰下面的“输出” 主项
.dw 0x85, 0x04 //报告ID(4),全局项,之前的报告ID项失效
.dw 0x91, 0x6 //输出(数据,变量,相对值)
.dw 0xc0 //结合结束
_ReportDescriptor_End:
以上描述符定义了4个不同的报告,用报告ID区分。HID设备定义文档上有讲,在一个报告ID之后而在下一个报告ID之前范围内的所有数据项都属于一个报告,发送报告时会把报告ID附在这个报告的前面义区分报告。
4. Windows HID编程接口
一般使用WriteFile或HidD_SetFeature来向设备发送数据(报告),使用ReadFile来读取设备发过来的数据(报告)。详情可以参考另一文章《Windows主机端与自定义USB HID设备通信详解》。
USB固件开发(Mass Storage设备)
Mass Storage设备,即大容量存储设备,最典型的莫过于U盘了,而U盘一般以Bulk Only传输方式实现。
1、USB Mass Storage设备的描述符及枚举过程描述符就是对应标准请求的那些描述符,与HID设备不同,Mass Storage设备没有自己的类描述符。描述符在USB Mass Storage Class Bulk-Only Transport文档中有详细的一对一的描述。所以此处不再赘述,仅举一例:
(设备描述符略,通用定义,与设备类无关)
(配置描述符略,通用定义,与设备类无关)
_Interface_Descriptor:
.dw 0x09 //bLength: 0x09 byte
.dw 0x04 //bDescriptorType: INTERFACE
.dw 0x00 //bInterfaceNumber: interface 0
.dw 0x00 //bAlternateSetting: alternate setting 0
.dw 0x02 //bNumEndpoints: 3 endpoints(EP0,EP1,EP2)
.dw 0x08 //bInterfaceClass: Mass Storage Devices Class
.dw 0x06 //bInterfaceSubClass:
.dw 0x50 //bInterfaceProtocol
.dw 0x02 //iInterface: index of string
_Interface_Descriptor_End:
_Endpoint1:
.dw 0x07 //bLength: 0x07 byte
.dw 0x05 //bDescriptorType: ENDPOINT
.dw 0x81 //bEndpointAddress: IN endpoint 1
.dw 0x02 //bmAttributes: Bulk
.dw 0x40, 0x00 //wMaxPacketSize: 64 byte
.dw 0x00 //bInterval: ignored
_Endpoint2:
//Endpoint 2 (0x07 byte)
.dw 0x07 //bLength: 0x07 byte
.dw 0x05 //bDescriptorType: ENDPOINT
.dw 0x02 //bEndpointAddress: OUT endpoint 2
.dw 0x02 //bmAttributes: Bulk
.dw 0x40, 0x00 //wMaxPacketSize: 64 byte
.dw 0x00 //bInterval: ignored
关于请求:
第一,主机首先会发出一系列标准请求。
第二,在标准请求完成之后,会发出两个类请求:Bulk-Only Mass Storage Reset请求和Get Max LUN请求。这两个请求的格式可以在USB Mass Storage Class Bulk-Only Transport文档中查询。
Bulk-Only Mass Storage Reset没有数据阶段,只在状态阶段告诉主机设备的Reset过程完成与否。如果在状态阶段返回ACK,那么主机就认为设备已经Reset完毕并准备好接收CBW了。
Get Max LUN要求设备返回一个字节的数据给主机,以表明此USB设备有多少个逻辑设备。返回的这个数据就是最大的设备逻辑号(Logic Unit Number),范围是0到15。例如,如果返回2,那么代表有0、1、2三个逻辑设备。
2、USB Mass Storage设备的Bulk数据交换流程通过bulk端点进行的数据传输,都遵循这样一个过程,即三个阶段:
CBW->DATA->CSW
CBW是一个数据块,携带主机发给设备的SCSI命令。接收了CBW后,设备就可以从中知道在接下来的DATA阶段中该干什么。
DATA阶段有三种情况:无数据需要传输,IN传输(设备到主机)或OUT传输(主机到设备)。
CSW阶段反馈这次传输的结果给主机。
其中值得注意的是:
- 在设备枚举完成之后,主机发出的第一个bulk OUT事务就是请求向设备发出CBW。所以设备可以通过这第一次的bulk OUT事务来判定第一次bulk数据传输的开始。此后的bulk数据传输就按照上述的三个阶段反复执行。也就是说,第一次传输CBW后,如果有数据要传输,那么就会经历DATA阶段,然后进入CSW阶段;如果没有数据要传输,则直接进入CSW阶段,就此一次传输结束。接下来,如果又有传输,那么再发出CBW。因此,设备可以认为CSW完成后收到的下一个bulk OUT事务就是主机请求传输新的CBW。
- CBW[12](CBW数据块的第13个字节)指明了传输方向,CBW[8-11]指明了传输的数据长度。实际上,CBW中的SCSI命令就暗含了数据要传输的方向和数据长度,因为SCSI规范中已明确规定这个命令所对应的数据格式。(在完整的应用中,要将CBW中的传输方向、数据长度与SCSI命令所表明的传输方向和数据长度做比较,不对应就要进行错误处理(Mass Storage Bulk-Only文档中有相关描述),不过正常情况下二者是匹配的,试验的时候可以暂时不理)。
- CSW[12](CSW数据块的第13个字节)这个字节很重要,它为0则表示此次传输成功,非0就是不成功。在DATA阶段的数据传完(或者无需数据传输)之后,主机会发出IN事务请求设备返回CSW。如果CSW传送的是不成功的信息,那么主机会接着发送另一个命令来获取失败的详细信息(即RequestSense命令)。
3、Mass Storage设备所使用的SCSI命令集0x00 TestUnitReady
0x03 RequestSense
0x12 Inquiry
0x1A ModeSense6
0x1B StartStop
0x1E MediumRemoval
0x23 ReadFormatCapacity
0x25 ReadCapacity
0x28 Read(10)
0x2A Write(10)
0x2F Verify
0x5A ModeSense10
其中,
- 主机首先发出Inquiry命令,响应了Inquiry之后就可以看到盘符.
- Inquiry之后会发出ReadFormatCapacity命令,这个命令在SCSI规范中是“厂家自定义命令”,可以参考UFI命令集文档(实际上,U盘所使用的所有SCSI命令集都可以参考UFI文档,它比SCSI标准文档更简洁明了)。注意这个命令在BusHound里是没有描述的,必须在“Device”选项页里勾选上这个U盘所对应的USB Mass Storage Device这个节点,才能看到这个命令的数据流。
- ReadFormatCapacity之后会发出ReadCapacity命令。
- U盘读数据(读扇区)时会发送Read(10)。ReadCapacity完成后就会发送Read(10)读取U盘的第一个扇区。
- U盘写数据时(写扇区)会发送Write(10)。
- TestUnitReady会在无其他数据传输时会定时发送,如果设备没有回应成功的CSW给主机,则主机认为设备已不存在。此时如果再双击磁盘图标,Windows会提示“请插入磁盘”。
- Verify在写数据时有用,表示核实数据,一般直接返回成功的CSW就可以了。一般来说,数据校验的工作在接收和向介质写数据时就已经顺带做了,如果发现错误,则直接告诉主机那次的数据传输有误,不会等到主机Verify时。当然,这不是一个必然的方案。
- RequestSense:如果CSW指示此次传输不成功,那么主机会发出此请求。
- StartStop暂时未发现大用处,一般直接返回成功的CSW。
- MediumRemoval在U盘被Eject的时候有用,处理不正确会Windows会弹出错误信息。
- ModeSense6/10这两个命令可以不支持(不支持不代表不反应,任何一个命令你都要做出反应,对于不支持的命令,可以通过STALL握手来向主机表明),暂时也未遇到过什么异常情况,而且我查看过一些U盘,有相当一部分就是随便回了几个数据给主机。这两个命令只会在U盘插入后发送一次,此后不再发送。
(待续)