第五篇 USB设备枚举过程(1)

总述

1. 设备枚举的整个过程

USB设备枚举过程,可大致分为下面的几个阶段:

一、获取设备描述符    

二、复位总线

三、设置地址阶段

四、再次获取设备描述符

五、获取配置描述符集合请求(第一次)

六、语言ID描述符和字符串描述符的请求

七、又获取了一次设备描述符

八、又再一次获取配置描述符

九、设置配置请求

十、设置接口请求

十一、重复两次请求产品字符串描述符

十二、Set_Idle请求

十三、获取报告描述符

十四、又一次获取产品描述符

十五、又一次设置接口

十六、一个未知的HID中断输出请求

十七、最后再获取一次配置描述符

2. 说明

(1)根据实例,分析枚举过程,使用的是实时操作系统Zephyr的设备协议栈架构,并且该实例是一个组合设备[ HID + UAC ]。

(2)USB设备枚举过程并不是固定的,比如,有多种因素会导致Host重复请求获取某个描述符,并且每次获取的长度是不一样的;不同的PC可能枚举过程也有差异;不同操作系统枚举过程也有差异(这是开发USB面临的最大问题之一,兼容性问题)。但是,过程大致是一样的,指令也是一样的,都遵循同一套USB协议。

(3)关于Zephyr操作系统的USB协议栈,后续有介绍。

(4)USB设备枚举过程比较复杂,因此篇幅比较长,这里差分为连续的几篇博文进行记录。

(5)分析枚举过程,又把前面四篇的内容给串起来了[对协议有粗略的解析]。

 

                                                                           设备枚举过程分析

USB检测到USB设备插入,会先对其进行复位。USB设备在总线复位后,地址为0。接下来,主机就使用地址0和设备进行通信。

一、Host获取设备描述符

主机向 地址为0的设备 的端点0发送获取设备描述符标准请求

1. USB的标准请求

USB协议定义了一个8字节的标准设备请求,标准请求发生在设备的枚举过程中。

(1) 标准请求的结构

偏移量/字节

大小/字节

取值

描述

0

bmRequestType

1

位图

见下表

1

bRequest

1

数值

见下表

2

wValue

2

数值

见下表

4

wIndex

2

索引或偏移量

见下表

6

wLength

2

字节数

见下表

对各个字段的描述:

请求的特性[bmRequestType]:

1)该字节数据的第七位D7:请求的数据传输方向

0:表示主机到设备  

1:表示设备到主机

2)该字节数据的第五和第六位D6~D5:表示请求的类型

0:表示这是一个标准请求

1:表示这是一个类请求

2:表示这是一个厂商请求

3:保留

3)第0到第4位D4~D0:表示该请求的接收者。

0:表示该请求的接收者是设备

1:表示该请求的接收者是接口

2:表示该请求的接收者是端点

3:表示该请求的接收者是其他

实例:如果是一个获取设备描述符的标准请求,那么这个字节数据就是0x80 = 1000 0000 B

[bRequest]: 请求代码

[wValue]: 意义由具体的请求来决定

[wIndex]: 意义由具体的请求来决定

[wLength]: 数据过程需要传输的字节数(可能为0,没有数据过程,那就是0)

 

2. USB获取设备描述符的标准请求

USB协议定义了11个标准请求(bRequest)。这11个标准请求的名字和对应的编号如下表。

bRequest

Value

bRequest

Value

GET_STATUS

0 (0x00)

GET_CONFIGURATIOIN

8 (0x08)

CLEAR_FEATURE

1 (0x01)

SET_CONFIGURATIOIN

9 (0x09)

SET_FEATURE

2 (0x02)

GET_INTERFACE

10 (0x0a)

SET_ADDRESS

5 (0x05)

SET_INTERFACE

11 (0x0b)

GET_DESCRIPTOR

6 (0x06)

SYNCH_FRAME

12 (0x0c)

SET_DESCRIPTOR

7 (0x07)

 

常用的几个标准请求为GET_DESCRIPTOR、SET_ADDRESS、GET_CONFIGURATIOIN、SET_CONFIGURATIOIN

(1) GET_DESCRIPTOR

(获取描述符)请求是枚举过程中,用得最多的一个请求。主机通过发送获取描述符请求,设备返回相应的数据,主机解析设备的各种描述符数据。也就是说,设备描述符还有分类,

USB协议规定,USB设备具有5种标准的描述符类型,如下表:

5种标准描述符类型

编号

设备描述符

1(0x01)

配置描述符

2(0x02)

字符串描述符

3(0x03)

接口描述符

4(0x04)

端点描述符

5(0x05)

GET_DESCRIPTOR的请求结构

bmRequestType

bRequest

wValue(2 Byte

wIndex(2 Byte)

wIndex (2 Byte)

1000 0000

(0x80

GET_DESCRIPTOR

(0x06

描述符的类型和索引值

0或者语言ID

描述符的长度

说明:

A.对于wValue这个域,第一字节(低字节)表示索引号,从0开始。第二字节(高字节)表示描述符的类型编号。

B除了获取字符串描述符之外,获取其他类型的描述符时,该域的值都为0。当获取的是字符串描述符中的语言ID字符串时(字符串描述符也有分类,下文会解析到),该域值就表示语言ID号。

C. wLength域是主机期望设备能返回的总数据字节数,那么,设备实际返回的字节数可以比该域指定的字节数少。比如:Host请求设备描述符的时候,只能返回18字节的数据(因为设备描述符只有18个字节)。

D. wValue、wIndex、wIndex的长度都是2字节,USB协议规定,使用小端模式进行数据传输,也就是低字节在前,高字节在后。

(2) 实例

Host请求设备描述符,则请求结构中各个域的为:

bmRequestType

bRequest

wValue

wIndex

wLength

1000 0000

(0x80

GET_DESCRIPTOR

(0x06

描述符的类型和索引值

0x01 00

0或者语言ID 0x00 00

长度

0x0040

那么Host下发的DATA0数据包的数据为[总线上的数据]:80 06  00 01  00 00  40 00

(3) 主机请求设备描述符的流程

在枚举阶段,USB使用的是控制传输。一次数据传输可认为是一个事务,事务一般由两三个包组成。事务=令牌包+数据包+握手包

控制传输分为三个过程:

第一过程:建立过程

第二过程:可选的数据过程

第三过程:状态过程

对于建立过程,使用一个建立事务一次数据传输过程[有发送方 和 接收方])。建立事务是一个数据输出的过程,也就是数据从主机到设备

令牌包只能使用SETUP令牌包;数据包只能使用DATA0数据包;最后是握手包,设备只能用ACK来应该(如果出错,则不应答)。使用USB协议分析仪分析建立事务,如下图:

第五篇 USB设备枚举过程(1)

 

                                                                                                                                                                      数据包的处理

    在枚举阶段,有很多的数据包以及复杂的传输过程,作为开发者,要怎么去处理呢?其实很多地方,USB接口芯片已经处理好了。我们只需要弄清楚过程就可以,区分哪部分是芯片完成的,哪部分是代码中需要处理的。

    芯片自动完成的(可认为是芯片内部的硬件自动处理的):CRC校验、数据包切换等。

    需要关注的:SETUP令牌包的中断、IN令牌包的中断、ACK握手包的回复等,对于程序来说,就是处理一系列的中断事件。Host下发的数据指令,比如上面这个例子中的DATA0数据包(80 06 00 01 00 00 40 00)。在程序中会收到这些数据,并且做逻辑上的处理(比如接下来就要把设备描述符的数据返回给主机,回应它的请求)。

    芯片自动完成的那些事情,数据是没法捕获到的,比如,使用Bus Hound这个上位机软件,只能捕获到DATA0数据包的数据,在Bus Hound中只能看到成功传输的数据,也就是只有ACK确认过的数据包。而使用比较高级的协议分析设备,比如专业的USB协议分析仪,就可以将整个过程都可视化。

数据过程是可选的,也就是一个控制传输,可以没有数据过程。一旦数据传输方向发生了改变,就会进行入状态过程。状态过程数据传输方向和前面的数据阶段相反,状态过程使用的是DATA1数据包。

继续接着分析下一个事务,也就是下一个数据传输。主机接着下发IN令牌包。设备切换到DATA1数据包,将设备描述符(18 Byte)发送给主机。主机收到数据,进行ACK回应。

设备返回描述符数据的过程如下图:

第五篇 USB设备枚举过程(1)

因为主机请求的是设备描述符,所以,这里返回的是USB设备的设备描述符数据。

 

3. 设备描述符

每个USB设备都必须并且只有一个设备描述符(在程序中有定义)。USB协议对设备描述符的定义如下:

(1) 设备描述符的结构

偏移量/字节

大小/字节

说明

0

bLength

1

描述符的长度 (18Byte=0x12)

1

bDescriptorType

1

描述符类型(设备描述符=0x01)

2

bcdUSB

2

本设备使用的USB协议版本

4

bDeviceClass

1

类代码

5

bDeviceSubClass

1

子类代码

6

bDeviceProtocol

1

设备所使用的协议

7

bMaxPacketSize0

1

端点0的最大包长(64 bytes)

8

idVendor

2

厂商ID

10

idProduct

2

产品ID

12

bcdDevice

2

设备版本号

14

iManufacturer

1

描述厂商字符串的索引

15

iProduct

1

描述产品字符串的索引

16

iSerialNumber

1

产品***字符串的索引

17

bNumConfigurations

1

可能的配置数

说明:

1)bcdUSB是该设备所使用的USB协议版本号,长度2字节。比如可以取2.0或者1.1等版本号。需要特别注意的是,协议规定使用BCD码来表示版本号,比如:USB2.0协议就是0x0200,USB1.1协议就是0x0110。对照USB协议分析仪来看的时候,要注意,USB协议中使用的是小端结构,也就是低字节在前。比如说,USB2.0协议拆分成两个字节就是0x00 0x02,那么对照协议分析仪里面的数据就是:00 02  ;USB1.1在协议分析仪里面的数据就是:10 01。

2)bDeviceClass是设备所使用的类代码(XX类接口描述符码)。常用的类如下(根据协议,进行C宏定义):

//HID设备类接口描述符码

#define HID_CLASS                    0x03

//音频类接口描述符码

#define Audio_CLASS                 0x01

//视频类接口描述符码

#define Vedio_CLASS                 0x0E

//大容量设备类接口描述符码

MASS_STORAGE_CLASS           0x08

//杂项类或者混合类接口描述符码

#define MISC_CLASS                           0xEF

//厂商自定义的设备类接口描述符码

#define CUSTOM_CLASS                    0xFF

//特定应用类接口描述符码

#define DFU_DEVICE_CLASS            0xFE

3)bDeviceSubClass设备所使用的子类代码。当类代码不为0也不是0xFF时,子类代码就得根据协议来进行赋值。当类代码为0的时候,子类代码也必须为0。

4)bDeviceProtocol是设备使用的协议。协议代码由USB协议规定。当该字段为0的时候,表示设备不使用类所定义的协议。该字段为0xFF的时候,表示使用的是厂商自定义的协议。也就是说,bDeviceProtocol要结合设备类和设备子类来进行赋值,如果设备类代码不为0,则子类代码肯定也不为0,进而bDeviceProtocol也就不为0(具体的取值,得深入研究USB协议)。如果类代码为0,则子类代码也就为0,进而bDeviceProtocol的值也就为0。综上所述,bDeviceClass、bDeviceSubClass、bDeviceProtocol这三者,要么都同时为0,要么都不为0。一般来说,设备类的定义放到接口里面,所以这三个字段一般都设置为0。

5)端点0的最大包长,取值可以是:8、16、32、64字节。注意,对应的十六进制数分别就是:0x08、0x10、0x20、0x40(分析源码和协议分析仪里面的数据,注意进行转换)。

6)关于厂商ID (2Byte),在开发中,可以随意设定一个值。真正做产品,要使用公司的ID(向USB协会申请),避免侵权。对于插入的设备,主机是依靠厂商ID号、产品ID号、产品***来安装驱动的。

7)产品ID是生产厂商自己定义的,比较*。

8)bcdDevice设备版本号。同一个产品,升级之后(比如固件修改,新增功能),可以通过修改设备版本号来进行区别。

9)iManufacturer是描述厂商字符串的索引值。如果设为0,则表示该USB设备没有厂商字符串。主机单独获取厂商字符串的时候,下发的标准请求数据包中,wValue域的第一个字节【低字节】就是厂商字符串的索引值,而高字节就是描述符的类型(字符串描述符0x03)。

厂商字符串就是一串普通的字符串,在设备描述符中,有三个非0的索引值:厂商字符串的索引值为1;产品字符串的索引值为2;产品***字符串的索引为3。设备在收到主机的字符串描述符请求之后,根据索引值,将对应的字符串数据返回给主机。所以,如果解析到主机字符串描述符请求的数据包,如果wValue=0x0301,则表示主机请求获得厂商字符串

10)iProduct是描述产品的字符串的索引值。同样的,如果设置为0,则表示该USB设备没有产品字符串。第一次插上设备时,提示发现新硬件,并显示设备的名称,其实这里显示的信息就是从产品字符串中获取的。实验:可通过修改产品字符串,再编译固件烧录,插入设备,就可以看到提示信息了。

同理,当主机请求产品字符串的时候,wValue这个域的值应该是:0x0302

11)iSerialNumber是设备的***字符串的索引值。同样的,如果设置为0,则表示该USB设备没有设备***。最好一个产品指定一个唯一的***,因为有可能主机会结合产品***和VID、PID来进行设备的区分和加载对应的驱动。

同理,当主机请求***字符串的时候,wValue这个域的值应该是:0x0303

注:

   厂商ID 、产品ID和***字符串是不一样的。


下文对源码的分析,仅供参考。

(2) 设备描述符在源码中的定义

对于设备描述符这个数据结构,可以抽象定义成一个结构体。zephyr的原生代码中,设备描述符的定义处如下:

第五篇 USB设备枚举过程(1)

 

4. 代码流程分析

(1) usb_handle_control_transfer()函数

这是设备核心层中的API,端点0有数据(数据输入/输出事务)的时候,usb_handle_control_transfer()会被回调。假如是一个标准请求,那么可以根据bmRequest域的值来进行判断,接下来数据的传输方向,只有两种情况:

A. 设备---->主机[bmRequest=0x80],也就是设备收到了一个请求,主机要求它接下来要把特定的请求的数据发给主机。

B. 主机---->设备[bmRequest=0x00],也就是设备收到了一个请求,通过解析请求,它知道接下来主机要下发数据。

在USB通信中,数据的收发是通过端点来进行的。各种描述符之间的关系,大概如下:

设备描述符(一个设备有且只有一个设备描述符)【up】

--------------------------------------------------------------------------------------

配置描述符

接口描述符

---------------------------------------------------------------------------------------

端点描述符 【bottom】

 

端点存在这几种状态,在源码中,定义如下:

文件:usb_dc.h

/**

 * USB Endpoint Callback Status Codes

 * 端点回调状态码定义

 */

enum usb_dc_ep_cb_status_code {

   /*已经收到SETUP数据包 */

   /*如果是标准请求,那么SETUP数据包就是完整的8字节标准请求数据包 */

USB_DC_EP_SETUP,    /* SETUP received =0*/

        

         /* Out transaction on this EP, data is available for read =1*/

/* Host通知设备,在该端点上进行的是:数据输出事务 */   

/* 并且,数据已经可以进行读取 */

         USB_DC_EP_DATA_OUT,

 

   //在该端点上,一个数据输入事务已经完成。

   //一个事务,就是一次数据传输,

   USB_DC_EP_DATA_IN,  /* In transaction done on this EP =2*/

};

回到usb_handle_control_transfer()的分析,该函数主要是进行数据传输方向的判断和端点状态的检测,对于枚举过程,首先从这个函数入手进行分析(整个API是处理控制传输的),原型定义如下:

第五篇 USB设备枚举过程(1)

(2) 一个标准请求处理的流程

首先一个标准请求会使得usb_handle_standard_request()被回调[暂时不分析底层控制器]。然后在函数体内,usb_handle_std_device_req()会被调用,真正地去处理标准请求指令是在usb_handle_std_device_req()里面。也就是前者是被触发,进行回调,后者是被调用。这两个函数定义的地方分别如下:

第五篇 USB设备枚举过程(1)

请求获得设备描述符、请求设置地址、请求获得配置描述符等标准请求,都要从下面这个函数开始进行分析。

第五篇 USB设备枚举过程(1)

在这个阶段,以Host请求获得设备描述符为例,接着进行分析。

第五篇 USB设备枚举过程(1)

分析usb_get_descriptor()这个函数,可以大概知道:先提取描述符的类型,判断是属于哪一种描述符,比如是设备描述符,然后,到描述符空间[一个地址,里面存放的是描述符的数据,设备描述符定义好之后,会进行注册]进行遍历,根据偏移量进行遍历进行寻找,该函数的定义处:

第五篇 USB设备枚举过程(1)

看函数体里面的注释,结合USB协议,可以知道:对于全速模式和低速模式的设备,获取描述符的标准请求只有三种。单独请求获取设备描述符、单独请求获取配置描述符、单独请求获取字符串描述符。接口描述符和端点描述符是跟着配置描述符一起返回的,不可单独返回,根据他们之间的包含关系(一个配置描述符可以有很多个接口,一个接口可以有很多个端点……),如果单独返回,主机没法解析(不符合协议)。实际上,主机也不会单独请求获取。

找到设备描述符之后,通过usb_data_to_host()将数据发送到主机。一层层剖析,可以发现,数据的发送是通过端点0进行发送的(符合协议)。usb_data_to_host()里面调用的是usb_dc_ep_write(),进入usb_dc_ep_write()可以发现,该函数完成数据的发送,还可以获取到发送成功的数据字节数。

发送的大概过程(结合代码和协议): 将设备描述符数据写入到端点0的输入缓冲区中,并使能端点发送。主机下发IN令牌包之后,设备的USB控制器就将缓冲区中的数据返回给主机,主机收到数据,则下发ACK握手包到设备,设备收到握手包就可以确认数据已经成功传输到主机了(这部分也是硬件自动完成的,对用户不可见)。

两个函数的定义处,分别如下:

第五篇 USB设备枚举过程(1)

第五篇 USB设备枚举过程(1)

主机在成功获取第一个数据包的设备描述符并确认无误之后,就会返回一个0长度的确认数据包(状态过程,使用的是DATA1数据包),设备收到确认数据包,就知道发送的数据是无误的,接下来设备发送握手包给主机,结束一个控制传输过程。控制传输过程比较复杂,几次交互,有可见的数据交互,也有不可见的数据交互,虽然复杂,但是能保证数据的可靠性。

到此,一个控制传输过程结束【建立过程(主机下发请求)、状态过程(设备返回数据) 状态过程(主机返回0长度的数据包)】。设备描述符总共有18个字节。

串口跟踪打印如下:

第五篇 USB设备枚举过程(1)

总结大概的流程: 有请求----->解析请求----->返回数据

需要注意的是,端点状态的回调(能反映数据的通信过程是否完整,开始或者结束)。

USB协议分析仪上捕获到的完整的总线数据:

第五篇 USB设备枚举过程(1)

Packet: 包         Transation: 事务        Transfer: 传输

 

5. 总结

(1) Host请求返回64个字节(0x40)的数据,但是实际上,设备描述符只有18字节。即返回的字节数可以小于或者等于wLength(协议规定)。

(2) 芯片自动处理的令牌包: ACK握手包(对用户不可见,一般的USB上位机捕获软件也没能捕获到,只有USB协议分析仪可以)。

(3) 这个过程总共有三个事务,每个事务的构成 = 令牌包 + 数据包 + 回应包,这三个事务又构成一次传输。

(4) 主机请求设备描述符这个阶段,使用的是控制传输类型,数据包在DATA0和DATA1之间进行切换。

(5) USB协议分析仪能够图形化分析每个流程。注意结合: USB协议分析仪 + 协议文档 + 源码。

(6) 主机在这个阶段【还没有复位总线】是和地址为0的USB设备的端点0进行数据通信。

(7) 都是Host在主动

Host检测到DP/DM电平有变化,说明有device接入;

紧接着就获取设备描述符(发出SETUP 和 IN_TOKEN包);

...........................................................................................

以上是Host检测到Device插入,第一次请求设备描述符的过程。