Chinaunix首页 | 论坛 | 博客
  • 博客访问: 8943
  • 博文数量: 4
  • 博客积分: 1400
  • 博客等级: 上尉
  • 技术积分: 50
  • 用 户 组: 普通用户
  • 注册时间: 2009-08-31 13:59
文章分类

全部博文(4)

文章存档

2011年(1)

2009年(3)

我的朋友
最近访客

分类: 嵌入式

2009-08-31 14:06:00

USB固件开发总结(一)

说明:

- 此文档包括四部分:

  - USB固件开发(通用部分)

  - USB固件开发(HID设备)

  - USB固件开发(Mass Storage设备)

  - USB固件开发(复合设备:HID+Mass Storage

- 由于不同的USB接口芯片在固件编写时会有不同的具体操作及特性,所以此文档不描述编程细节。

 

USB固件开发(通用部分)


1. 基本概念

1.1 工作机制

USB的通信是主从式通信。即USB主机发送请求,USB设备(从机)做出应答。设备无法主动请求主机。这样,设备的工作主要就是正确响应主机发来的请求。主机为了识别不同功能的设备,必然要求设备在投入具体使用前先报告一些特征数据给主机,这些特征数据的格式遵循着主机和设备之间制定的协议,所以如果设备正确返回了这些特征数据,那么主机就能够确定设备的功能。在此之后,主机就会按照设备的具体功能发出具体请求了,它们之间的正常工作由此展开。

以上表述中,所谓的“特征数据”就是“描述符”,主要包括设备描述符、配置描述符、接口描述符、端点描述符和字符串描述符,以及对应于设备具体功能的类描述符(譬如HID描述符)。


1.2  USB系统的层次结构

    如下图所示,主机与设备的通信分为3层,一般的开发只关心上两层。具体来讲,设备固件的开发会关心上两层,而主机还有可能只关心第一层,即客户软件的开发。从图上也可以开出,对具体“应用”来说,对设备来说只有“接口”是可见的,对主机客户软件来说,只有“管道”(图中的“通道”)可见。

 

 


2. 主机对设备的识别

这里的识别仅指设备插入后所进行的软件识别,不包括主机对设备硬件插入的识别。


主机检测到有USB设备插入后,会依次进行如下动作:

1、      复位设备。

2、      以缺省地址0向设备发出读取设备描述符的请求,以取得缺省控制管道所支持的最大数据包长度,此时主机只会读取描述符的前8个字节。

3、      向设备发出SetAddress请求,为其分配一个唯一的设备地址。

4、      使用新地址向设备发出读取设备描述符的请求,与第2步不同,此时是读取整个描述符。

5、      向设备循环发出读取配置描述符的请求,以读取全部配置信息(一个USB设备可能有多个配置,由设备描述符相关字段指出)。值得注意的是,这里的配置信息包括了配置描述符本身、接口描述符和端点描述符,以及相关类描述符。所以,每读取一个配置,主机会分为两步:首先发出请求读取配置描述符本身(9字节),以得到配置信息的长度;然后再次发出请求读取整个配置信息。这两步中的请求都是“读取配置描述符”,只是指明读取的数据长度不同。

6、      发出SetConfiguration请求为设备选择合适的配置。

 

在实际的开发的开发中,程序员很有可能觉察不到有这么多的请求,因为USB接口芯片很可能封装了一些控制管道的标准请求如SET_ADDRESS等,对这些请求芯片会自动做出反应,不需程序员干预。这是对开发过程的一种简化。譬如Sunplus公司MCU上集成的USB接口,一般就只将读取/设定描述符的请求暴露给程序员,而将GET_STATUSCLEAR_FEATURESET_FEATURESET_ADDRESSGET_CONFIGURATIONSET_CONFIGURATIONGET_INTERFACESET_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个字节。

 

注意:

字段bDeviceClassbDeviceSubClassUSB规范中被定义为“指明该设备所属的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         字段bInterfaceClassbInterfaceSubClassbInterfaceProtocol联合指明接口所属的设备类及协议,也就是接口的具体用途及使用方法。

l         字段iInterface指明描述此接口的字符串描述符的索引,0代表不使用。

 

端点描述符紧接着接口描述符,描述接口所使用的端点信息。这里是指明了两个端点:Bulk OUTBulk 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给主机。此例中,LANGID0x0409,表示在字符串描述符中字符的语言是US英语。其它3个字符串分别与设备描述符中/iManufactureiProductiSerialNumber字段对应。

3.2 固件在USB设备设别阶段的编程思路

一般地,USB设备接口芯片会产生一些中断来通知程序员特定事件的发生。譬如说,EP0(缺省控制端点) SETUP包的到达,EP0 INOUT事务的发生等等。控制传输是分三个阶段的:建立阶段,数据阶段,状态阶段。所以对于一次控制传输,设备固件必须要正确控制其执行流程,不能颠倒。当收到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)

{

       取得全局的发送数据的指针;

       利用指针读取描述符的数据并填充至端点0FIFO;

       通知芯片EP0 IN数据包已准备好;

}

 

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,那么数据字段的值与逻辑值是一一对应且相等的,即000b),101b),210b),311b)。


第三个例子:

再举一个例子:

逻辑最小值(-1

逻辑最大值(1

报告大小(2

这个例子的意思是,此报告中数据字段的大小是3位,逻辑值范围是-1~1,那么数据字段的值与逻辑值是按左对齐的方式部分对应的,即数据字段值000b)对应逻辑值-1,数据字段值101b)对应逻辑值0,数据字段值210b)对应逻辑值1,数据字段值311b)无效。

 

这里举一个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          //报告ID3),全局项,之前的报告ID项失效

    .dw 0xb1, 0x6                     //特征(数据,变量,相对值)

 

       //下面开始一个新的主项目,前面提到的全局项仍对这个主项目有效,譬如报告大小等

       .dw 0x09,  0x01     //用法(供应商用法1) ,局部项,修饰下面的“特征”  主项

    .dw 0x85,  0x02          //报告ID2),全局项,之前的报告ID项失效

    .dw 0xb1,  0x06          //特征(数据,变量,相对值)

   

       //下面开始一个新的主项目,前面提到的全局项仍对这个主项目有效,譬如报告大小等

    .dw 0x09,  0x01     //用法(供应商用法1) ,局部项,修饰下面的“输出”  主项

    .dw 0x85,  0x04          //报告ID4),全局项,之前的报告ID项失效

    .dw 0x91, 0x6                     //输出(数据,变量,相对值)

    .dw  0xc0            //结合结束

_ReportDescriptor_End:

 

以上描述符定义了4个不同的报告,用报告ID区分。HID设备定义文档上有讲,在一个报告ID之后而在下一个报告ID之前范围内的所有数据项都属于一个报告,发送报告时会把报告ID附在这个报告的前面义区分报告。

 

4. Windows HID编程接口

一般使用WriteFileHidD_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盘插入后发送一次,此后不再发送。

(待续)
阅读(910) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~