http://blog.chinaunix.net/u3/98810/index.html
USB固件开发总结(一)<?xml:namespace prefix = o />
说明:
- 此文档包括四部分:
- USB固件开发(通用部分)
- USB固件开发(HID设备)
- USB固件开发(Mass Storage设备)
- USB固件开发(复合设备:HID+Mass Storage)
- 由于不同的USB接口芯片在固件编写时会有不同的具体操作及特性,所以此文档不描述编程细节。
USB固件开发(通用部分)
<!--[if !supportLineBreakNewLine]-->
<!--[endif]-->
1. 基本概念
1.1 工作机制
USB的通信是主从式通信。即USB主机发送请求,USB设 备(从机)做出应答。设备无法主动请求主机。这样,设备的工作主要就是正确响应主机发来的请求。主机为了识别不同功能的设备,必然要求设备在投入具体使用前先报告一些特征数据给主机,这些特征数据的格式遵循着主机和设备之间制定的协议,所以如果设备正确返回了这些特征数据,那么主机就能够确定设备的功能。 在此之后,主机就会按照设备的具体功能发出具体请求了,它们之间的正常工作由此展开。
以上表述中,所谓的“特征数据”就是“描述符”,主要包括设备描述符、配置描述符、接口描述符、端点描述符和字符串描述符,以及对应于设备具体功能的类描述符(譬如HID描述符)。
1.2 USB系统的层次结构
如下图所示,主机与设备的通信分为3层,一般的开发只关心上两层。具体来讲,设备固件的开发会关心上两层,而主机还有可能只关心第一层,即客户软件的开发。从图上也可以开出,对具体“应用”来说,对设备来说只有“接口”是可见的,对主机客户软件来说,只有“管道”(图中的“通道”)可见。
<?xml:namespace prefix = v /><!--[if !vml]--><!--[endif]-->
<!--[if !supportLineBreakNewLine]-->
<!--[endif]-->
2. 主机对设备的识别
这里的识别仅指设备插入后所进行的软件识别,不包括主机对设备硬件插入的识别。
主机检测到有USB设备插入后,会依次进行如下动作:
1、 复位设备。
2、 以缺省地址0向设备发出读取设备描述符的请求,以取得缺省控制管道所支持的最大数据包长度,此时主机只会读取描述符的前8个字节。
3、 向设备发出SetAddress请求,为其分配一个唯一的设备地址。
4、 使用新地址向设备发出读取设备描述符的请求,与第2步不同,此时是读取整个描述符。
5、 向设备循环发出读取配置描述符的请求,以读取全部配置信息(一个USB设备可能有多个配置,由设备描述符相关字段指出)。值得注意的是,这里的配置信息包括了配置描述符本身、接口描述符和端点描述符,以及相关类描述符。所以,每读取一个配置,主机会分为两步:首先发出请求读取配置描述符本身(9字节),以得到配置信息的长度;然后再次发出请求读取整个配置信息。这两步中的请求都是“读取配置描述符”,只是指明读取的数据长度不同。
6、 发出SetConfiguration请求为设备选择合适的配置。
在实际的开发的开发中,程序员很有可能觉察不到有这么多的请求,因为USB接口芯片很可能封装了一些控制管道的标准请求如SET_ADDRESS等,对这些请求芯片会自动做出反应,不需程序员干预。这是对开发过程的一种简化。譬如Sunplus公司MCU上集成的USB接口,一般就只将读取/设定描述符的请求暴露给程序员,而将GET_STATUS,CLEAR_FEATURE,SET_FEATURE,SET_ADDRESS,GET_CONFIGURATION,SET_CONFIGURATION,GET_INTERFACE,SET_INTERFACE这些请求全部封装了。
在以上表述中,有几处提到主机在发送同一个请求时,要求读取的数据长度会不一样。如读取设备描述符时,是先读8字节,而后再读取整个;读取配置信息时,是先读取9字节,然后读取整个。所以,固件程序必须处理这些情况。有的时候,设备要求读取的数据长度比实际描述符的长度要长,例如读取字符串描述符时,它一律发出0xFF这样的长度,对这种情况固件也必须处理。一个通用的做法就是,对于所有的请求,都把描述符长度与主机请求读取的长度做个比较,取其中较小者为实际处理的长度。
3. 开发中的细节问题
3.1各描述符详解
各描述符字段的具体意义,可以参考USB规范第9章“设备架构”。这里主要是举例说明各描述符的用法和它们在实际编程中的表现形式,以及一些注意事项。
总的一点:不管是何种描述符,其前两个字节必定是指明该描述符的长度及描述符类型。
设备描述符:
_Device_Descriptor:
.dw _Device_Descriptor_End-_Device_Descriptor //bLength
.dw 0x01 //bDescriptorType : Device
.dw 0x10, 0x01 //bcdUSB : version 1.10
.dw 0x00 //bDeviceClass
.dw 0x00 //bDeviceSubClass
.dw 0x00 //bDeviceProtocol
.dw 0x08 //bMaxPacketSize0
.dw 0x55, 0x0f //idVendor : 0x0F55
.dw 0x8A, 0x02 //idProduct : 0x028A
.dw 0x00, 0x01 //bcdDevice
.dw 0x01 //iManufacturer
.dw 0x02 //iProduct
.dw 0x03 //iSerialNumber
.dw 0x01 //bNumConfigurations
_Device_Descriptor_End:
此设备描述符描述了一个支持USB1.1协议的,只有一个配置的设备,指出控制管道的最大数据包长度为8个字节。
注意:
字段bDeviceClass与bDeviceSubClass在USB规范中被定义为“指明该设备所属的USB设备类与设备子类”,但一般的应用都会将此两个字段置0,表示各个接口相互独立工作,其所属的设备类将在接口描述符中指出。这一点就暗示了主机端的客户软件是以接口为基本操作对象的,与1.2节中的USB层次结构描述相呼应。
配置信息描述符集合:
_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:
_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 0x00 //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:
.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
_Config_Descriptor_Total:
配置信息包括其配置描述符本身,外加端点描述符、接口描述符以及可能存在的类描述符。
关于配置描述符:
l 字段wTotalLength指明了配置信息的总长度。
l 字段bNumInterfaces指明了接口数目,这里是1,代表此设备只有一个接口。实际应用中,很可能有两个以上的接口,譬如说两个接口,那么这里应填为2,并且在配置信息中要增加一个接口描述符及相配套的描述符,如端点描述符等。文档的第四部分“USB固件开发(复合设备:HID+Mass Storage)”将会举例说明。
l 字段bConfigurationValue指明了配置号,主机在SetConfiguration请求时用此值来选定此配置。如果有两个以上的配置,那么配置号将各不相同。
l 字段iConfiguration指明了描述该配置的字符串描述的索引,0代表不使用。
接口描述符紧接着配置描述符,描述具体的接口信息。接口指明了此设备具体有什么用途。
关于接口描述符:
l 字段bInterfaceNumber指明接口号。如果有多个不同种类的接口,则每种接口的接口号各不相同。
l 字段bAlternateSetting指明可选设置的索引值。意思是如果此设备只有一种接口,但是有两个可选设置,那么这两个设置的bInterfaceNumber值必须相同,且bAlternateSetting值必须不同。文档的第四部分“USB固件开发(复合设备:HID+Mass Storage)”将会举例说明。
l 字段bNumEndpoints指明接口使用的端点数目,不包括端点0(端点0是必须有的且被用在缺省的控制管道上)。
l 字段bInterfaceClass、bInterfaceSubClass和bInterfaceProtocol联合指明接口所属的设备类及协议,也就是接口的具体用途及使用方法。
l 字段iInterface指明描述此接口的字符串描述符的索引,0代表不使用。
端点描述符紧接着接口描述符,描述接口所使用的端点信息。这里是指明了两个端点:Bulk OUT与Bulk IN端点。
关于端点描述符:
l 字段bEndpointAddress指明了端点号与端点的传输方向。这必须与USB接口芯片中的端点信息一致。譬如说,_Endpoint1指明了以Bulk IN方式使用端点1,那么在固件编程的时候,也必须设置USB接口芯片的相关寄存器,指明端点1的使用方式为Bulk IN。
l 字段wMaxPacketSize指明了端点所支持的最大数据包长度。这和USB接口芯片的特性及USB传输方式和传输速度有关。在块(bulk)传输方式下,如果是低速/全速传输,那最大只能为64字节;如果是高速传输,那么最大为512字节。如果接口芯片支持的最大值小于上述最大值,那么当然要以接口芯片的为准。
l 字段bInterval只对用作中断传输的断点起作用,表述主机轮询此端点的最大时间间隔。
字符串描述符:
_String0_Descriptor:
.dw 0x04 //bLength
.dw 0x03 //bDescriptorType
.dw 0x09, 0x04 //bString
_String0_Descriptor_End:
_String1_Descriptor:
.dw _String1_Descriptor_End-_String1_Descriptor //bLength
.dw 0x03 //bDescriptorType
.dw 'S', 0x00 //bString
.dw 'T', 0x00
.dw 'R', 0x00
.dw 'I', 0x00
.dw 'N', 0x00
.dw 'G', 0x00
.dw '1', 0x00
_String1_Descriptor_End:
_String2_Descriptor:
.dw _String2_Descriptor_End-_String2_Descriptor //bLength
.dw 0x03 //bDescriptorType
.dw 'S', 0x00 //bString
.dw 'T', 0x00
.dw 'R', 0x00
.dw 'I', 0x00
.dw 'N', 0x00
.dw 'G', 0x00
.dw '2, 0x00
_String2_Descriptor_End:
_String3_Descriptor:
.dw _String3_Descriptor_End-_String3_Descriptor //bLength
.dw 0x03 //bDescriptorType
.dw 'S', 0x00 //bString
.dw 'T', 0x00
.dw 'R', 0x00
.dw 'I', 0x00
.dw 'N', 0x00
.dw 'G', 0x00
.dw '3', 0x00
_String3_Descriptor_End:
字符串描述符所遵循的字符编码标准是UNICODE,即使用两字节来表示一个字符。唯一要注意的是字符串0有着特殊意义:它返回字符的语言信息即LANGID给主机。此例中,LANGID为0x0409,表示在字符串描述符中字符的语言是US英语。其它3个字符串分别与设备描述符中/iManufacture、iProduct、iSerialNumber字段对应。
USB固件开发总结(二)
3.2 固件在USB设备设别阶段的编程思路
一般地,USB设备接口芯片会产生一些中断来通知程序员特定事件的发生。譬如说,EP0(缺省控制端点) SETUP包的到达,EP0 IN或OUT事务的发生等等。控制传输是分三个阶段的:建立阶段,数据阶段,状态阶段。所以对于一次控制传输,设备固件必须要正确控制其执行流程,不能颠倒。当收到EP0 SETUP包 到达的信息之后,固件要分析其请求的具体内容,这里假定为读取描述符,然后进入数据阶段向主机发送相应描述符的具体内容,发送完成后,进入状态阶段。状态阶段结束后,一次控制传输就此完成。要注意的是,就算是进入各个阶段,也要等待主机发送事务请求后才能响应具体操作。也就是,假定固件分析了EP0 SETUP包得到主机的请求是读取某个描述符,固件随后应该进入数据阶段,但只是流程逻辑上的进入,具体的操作还要等待主机的控制IN令牌到达后,才能开始数据阶段真正的数据传输,之后进入状态阶段。一般地,状态阶段只需设定一个寄存器通知芯片开始状态阶段即可,无需干预其细节。主机对设备的识别最初是通过控制管道来进行的,一系列控制传输(主机识别设备的请求)完成之后,主机就能识别到USB设备了,在设备管理器中会显示出来(但是不一定能完全正常地使用设备,因为可能还有一些协议并未完成,例如Mass Storage设备还需对SCSI命令正确响应,文档的第3部分会有具体讲述)。下面举一个例子说明固件处理控制传输的思路,当然实际应用中并不限于这样的思路。
这个例子的思路是,在响应USB产生的中断时,会用全局变量记录下中断的发生,然后在主循环里面进行具体处理。
/* USB服务程序伪代码 */
void USB_Service(void)
{
/*处理控制传输的3个阶段*/
switch (EP.EP0.Stage)
{
case C_STAGE_EP0_SETUP: /*处在建立阶段*/
if (!USB_Setup ()) /*如果请求是被支持的*/
EP.EP0.Stage = C_STAGE_EP0_DATA; /*转入数据阶段*/
break;
case C_STAGE_EP0_DATA: /*处在数据阶段*/
if (EP.EP0.Status == C_STATUS_EP0_IN_NACK) /*收到了IN令牌*/
{
USB_WriteEP0FIFO(); /*通过控制端点发送数据给主机*/
EP.EP0.Status = C_STATUS_RESET; /*已处理完毕,所以复位此状态*/
EP.EP0.Stage = C_STAGE_EP0_STATUS; /*转入状态阶段*/
重新使能EP0_IN_NACK中断; /*在ISR中会关掉此中断*/
}
break;
case C_STAGE_EP0_STATUS:
使能EP0_STATUS寄存器;
break;
default:
break;
}
}
/*USB中断服务程序伪代码(部分)*/
void USB_ISR(void)
{
if (SETUP包到达)
{
清中断;
EP.EP0.Stage = C_STAGE_EP0_SETUP;
EP.EP0.Status = C_STATUS_EP0_SETUP_ARRIVAL;
}
else if (EP0 IN令牌到达但是芯片自动回复了NAK)
{
清中断;
关闭此中断;
EP.EP0.Status = C_STATUS_EP0_IN_NACK;
}
}
/*USB控制传输的建立阶段处理程序伪代码*/
int USB_Setup (void)
{
通过各寄存器的值来得到请求的类型和相关数据;
if (请求类型是读取描述符)
{
switch (描述符值)
{
case 设备描述符值:
将全局的发送数据的指针指向设备描述符buffer;
break;
......
default: break;
}
}
else if (设备还需处理的其他请求)
{
处理;
}
else /*不支持的请求*/
{
发送STALL信号;
return 1; /*返回错误*/
}
return 0; /*返回正确*/
}
/*通过端点0发送数据*/
void USB_WriteEP0FIFO(void)
{
取得全局的发送数据的指针;
利用指针读取描述符的数据并填充至端点0的FIFO;
通知芯片EP0 IN数据包已准备好;
}
USB固件开发总结(三)
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描述符其实是为了提供下级描述符(如报告描述符)的信息。
下图更清楚地表述了各描述符之间的层次关系。
<!--[if !vml]--><!--[endif]-->
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设备通信详解》。
Windows主机端与自定义USB HID设备通信详解
说明:
- 以下结论都是基于Windows XP系统所得出的,不保证在其他系统的适用性。
- 在此讨论的是HID自定义设备,对于标准设备,譬如USB鼠标和键盘,由于操作系统对其独占,许多操作未必能正确执行。
1. 所使用的典型Windows API
CreateFile
ReadFile
WriteFile
以下函数是DDK的内容:
HidD_SetFeature
HidD_GetFeature
HidD_SetOutputReport
HidD_GetInputReport
其中,CreateFile用于打开设备;ReadFile、HidD_GetFeature、HidD_GetInputReport用于设备到主机方向的数据通信;WriteFile、HidD_SetFeature、HidD_SetOutputReport用于主机到设备方向的数据通信。鉴于实际应用,后文主要讨论CreateFile,WriteFile,ReadFile,HidD_SetFeature四个函数,明白了这四个函数,其它的可以类推之。
2. 几个常见错误
当使用以上API时,如果操作失败,调用GetLastError()会得到以下常见错误:
6: 句柄无效
23: 数据错误(循环冗余码检查)
87: 参数错误
1784: 用户提供的buffer无效
后文将会详细说明这些错误情况。
3. 主机端设备枚举程序流程
<!--[if !vml]--><!--[endif]-->
4. 函数使用说明
CreateFile(devDetail->DevicePath, //设备路径
GENERIC_READ | GENERIC_WRITE, //访问方式
FILE_SHARE_READ | FILE_SHARE_WRITE, //共享模式
NULL,
OPEN_EXISTING, //文件不存在时,返回失败
FILE_FLAG_OVERLAPPED, //以重叠(异步)模式打开
NULL);
在这里,CreateFile用于打开HID设备,其中设备路径通过函数SetupDiGetInterfaceDeviceDetail取得。CreateFile有以下几点需要注意:
- 访问方式: 如果是系统独占设备,例如鼠标、键盘等等,应将此参数设置为0,否则后续函数操作将失败(譬如HidD_GetAttributes);也就是说,不能对独占设备进行除了查询以外的任何操作,所以能够使用的函数也是很有限的,下文的一些函数并不一定适合这些设备。在此顺便列出MSDN上关于此参数的说明:
If this parameter is zero, the application can query file and device attributes without accessing the device. This is useful if an application wants to determine the size of a floppy disk drive and the formats it supports without requiring a floppy in the drive. It can also be used to test for the file's or directory's existence without opening it for read or write access。
- 重叠(异步)模式:此参数并不会在此处表现出明显的意义,它主要是对后续的WriteFile,ReadFile有影响。如果这里设置为重叠(异步)模式,那么在使用WriteFile,ReadFile时也应该使用重叠(异步)模式,反之亦然。这首先要求WriteFile,ReadFile的最后一个参数不能为空(NULL)。否则,便会返回87(参数错误)错误号。当然,87号错误并不代表就是此参数不正确,更多的信息将在具体讲述这两个函数时指出。此参数为0时,代表同步模式,即WriteFile,ReadFile操作会在数据处理完成之后才返回,否则阻塞在函数内部。
ReadFile(hDev, //设备句柄,即CreateFile的返回值
recvBuffer, //用于接收数据的buffer
IN_REPORT_LEN, //要读取数据的长度
&recvBytes, //实际收到的数据的字节数
&ol); //异步模式
在这里,ReadFile用于读取HID设备通过中断IN传输发来的输入报告。有以下几点要注意:
1、ReadFile的调用不会引起设备的任何反应,即HID设备与主机之间的中断IN传输不与ReadFile打交道。实际上主机会在最大间隔时间(由设备的端点描述符来指定)内轮询设备,发出中断IN传输的请求。“读取”即意味着从某个buffer里面取回数据,实际上这个buffer就是HID设备驱动中的buffer。这个buffer的大小可以通过HidD_SetNumInputBuffers来改变。在XP上缺省值是32(个报告)。
2、读取的数据对象是输入报告,也即通过中断输入管道传入的数据。所以,如果设备不支持中断IN传输,那么是无法使用此函数来得到预期结果的。实际上这种情况不可能在HID中出现,因为协议指明了至少要有一个中断IN端点。
3、IN_REPORT_LEN代表要读取的数据的长度(实际的数据正文+一个byte的报告ID),这里是一个常数,主要是因为设备固件的信息我是完全知道的,当然知道要读取多少数据(也就是报告的长度);不过也可以通过另外的函数(HidD_GetPreparsedData)来事先取得报告的长度,这里不做详细讨论。因为很难想象在不了解固件信息的情况下来做自定义设备的HID通信,在实际应用中一般来说就是固件与PC程序匹配着来开发。此参数如果设置过大,不会有实质性的错误,在recvBytes参数中会输出实际读到的长度;如果设置过小,即小于报告的长度,会返回1784号错误(用户提供的buffer无效)。
4、关于异步模式。前面已经提过,此参数的设置必须与CreateFile时的设置相对应,否则会返回87号错误(参数错误)。如果不需要异步模式,此参数需置为NULL。在这种情况下,ReadFile会一直等待直到数据读取成功,所以会阻塞住程序的当前过程。
WriteFile(hDev, //设备句柄,即CreateFile的返回值
reportBuf, //存有待发送数据的buffer
OUT_REPORT_LEN, //待发送数据的长度
&sendBytes, //实际收到的数据的字节数
&ol); //异步模式
在这里,WriteFile用于传输一个输出报告给HID设备。有以下几点要注意:
1、 与ReadFile不同,WriteFile函数被调用后,虽然也是经过驱动程序,但是最终会反映到设备中。也就是说,调用WriteFile后,设备会接收到输出报告的请求。如果设备使用了中断OUT传输,则WriteFile会通过中断OUT管道来进行传输;否则会使用SetReport请求通过控制管道来传输。
2、 OUT_REPORT_LEN代表要写入的数据长度(实际的数据正文+一个byte的报告ID)。如果大于实际报告的长度,则使用实际报告长度;如果小于实际报告长度,会返回1784号错误(用户提供的buffer无效)。
3、 reportBuf[0]必须存有待发送报告的ID,并且此报告ID指示的必须是输出报告,否则会返回87号错误(参数错误)。这种情况可能容易被程序员忽略,结果不知错误号所反映的是什么,网上也经常有类似疑问的帖子。顺便指出,输入报告、输入报告、特征报告这些报告类型,是反映在HID设备的报告描述符中。后文将做举例讨论。
4、 关于异步模式。前面已经提过,此参数的设置必须与CreateFile时的设置相对应,否则会返回87号错误(参数错误)。如果不需要异步模式,此参数需置为NULL。在这种情况下,WriteFile会一直等待直到数据读取成功,所以会阻塞住程序的当前过程。
HidD_SetFeature(hDev, //设备句柄,即CreateFile的返回值
reportBuf, //存有待发送数据的buffer
FEATURE_REPORT_LEN); //buffer的长度
HidD_SetOutputReport(hDev, //设备句柄,即CreateFile的返回值
reportBuf, //存有待发送数据的buffer
OUT_REPORT_LEN); //buffer的长度
HidD_SetFeature发送一个特征报告给设备,HidD_ SetOutputReport发送一个输出报告给设备。注意以下几点:
1、 跟WriteFile类似,必须在reportBuf[0]中指明要发送的报告的ID,并且和各自适合的类型相对应。也就是说,HidD_SetFeature只能发送特征报告,因此报告ID必须是特征报告的ID;HidD_SetOutputReport只能发送输出报告,因此报告ID只能是输出报告的ID。
2、 这两个函数最常返回的错误代码是23(数据错误)。包括但不仅限于以下情况:
- 报告ID与固件描述的不符。
- 传入的buffer长度少于固件描述的报告的长度。
据有关资料反映(非官方文档),只要是驱动程序对请求无反应,都会产生此错误。
5. 常见错误汇总
- HID ReadFile
- Error Code 6 (handle is invalid)
传入的句柄无效
- Error Code 87 (参数错误)
很可能是createfile时声明了异步方式,但是读取时按同步读取。
- Error Code 1784 (用户提供的buffer无效):
传参时传入的“读取buffer长度”与实际的报告长度不符。
- HID WriteFile
- Error Code 6 (handle is invalid)
传入的句柄无效
- Error Code 87(参数错误)
- CreateFile时声明的同步/异步方式与实际调用WriteFile时传入的不同。
- 报告ID与固件中定义的不一致(buffer的首字节是报告ID)
- Error Code 1784 (用户提供的buffer无效)
传参时传入的“写入buffer长度”与实际的报告长度不符。
- HidD_SetFeature
- HidD_SetOutputReport
- Error Code 1 (incorrect function)
不支持此函数,很可能是设备的报告描述符中未定义这样的报告类型(输入、输出、特征)
- Error Code 6 (handle is invalid)
传入的句柄无效
- Error Code 23(数据错误(循环冗余码检查))
- 报告ID与固件中定义的不相符(buffer的首字节是报告ID)
- 传入的buffer长度少于固件定义的报告长度(报告正文+1byte, 1byte为报告ID)
- 据相关资料反映(非官方文档),只要是驱动程序不接受此请求(对请求无反应),都会产生此错误
6. 报告描述符及数据通信程序示例
报告描述符(由于是汇编代码,所以不必留意其语法,仅需注意表中的每个数据都占1个字节):
_ReportDescriptor: //报告描述符
.dw 0x06, 0x00, 0xff //用法页
.dw 0x09, 0x01 //用法(供应商用法1)
.dw 0xa1, 0x01 //集合开始
.dw 0x85, 0x01 //报告ID(1)
.dw 0x09, 0x01 //用法(供应商用法1)
.dw 0x15, 0x00 //逻辑最小值(0)
.dw 0x26, 0xff, 0x0 //逻辑最大值(255)
.dw 0x75, 0x08 //报告大小(8)
.dw 0x95, 0x07 //报告计数(7)
.dw 0x81, 0x06 //输入(数据,变量,相对值)
.dw 0x09, 0x01 //用法(供应商用法1)
.dw 0x85, 0x03 //报告ID(3)
.dw 0xb1, 0x06 //特征(数据,变量,相对值)
.dw 0x09, 0x01 //用法(供应商用法1)
.dw 0x85, 0x02 //报告ID(2)
.dw 0xb1, 0x06 //特征(数据,变量,相对值)
.dw 0x09, 0x01 //用法(供应商用法1)
.dw 0x85, 0x04 //报告ID(4)
.dw 0x91, 0x06 //输出(数据,变量,相对值)
.dw 0xc0 //结合结束
_ReportDescriptor_End:
这个报告描述符,定义了4个不同的报告:输入报告1,特征报告2,特征报告3,输出报告4(数字代表其报告ID)。为了简化,每个报告都是7个字节(加上报告ID就是8个字节)。下面用一个简单的示例来描述PC端与USB HID设备进行通信的一般方法。
|
文章评论(0条评论)
登录后参与讨论