第五篇 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:请求的数据传输方向
2)该字节数据的第五和第六位D6~D5:表示请求的类型
3)第0到第4位D4~D0:表示该请求的接收者。
实例:如果是一个获取设备描述符的标准请求,那么这个字节数据就是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接口芯片已经处理好了。我们只需要弄清楚过程就可以,区分哪部分是芯片完成的,哪部分是代码中需要处理的。 芯片自动完成的(可认为是芯片内部的硬件自动处理的):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设备的设备描述符数据。
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的原生代码中,设备描述符的定义处如下:
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是处理控制传输的),原型定义如下:
(2) 一个标准请求处理的流程
首先一个标准请求会使得usb_handle_standard_request()被回调[暂时不分析底层控制器]。然后在函数体内,usb_handle_std_device_req()会被调用,真正地去处理标准请求指令是在usb_handle_std_device_req()里面。也就是前者是被触发,进行回调,后者是被调用。这两个函数定义的地方分别如下:
请求获得设备描述符、请求设置地址、请求获得配置描述符等标准请求,都要从下面这个函数开始进行分析。
在这个阶段,以Host请求获得设备描述符为例,接着进行分析。
分析usb_get_descriptor()这个函数,可以大概知道:先提取描述符的类型,判断是属于哪一种描述符,比如是设备描述符,然后,到描述符空间[一个地址,里面存放的是描述符的数据,设备描述符定义好之后,会进行注册]进行遍历,根据偏移量进行遍历进行寻找,该函数的定义处:
看函数体里面的注释,结合USB协议,可以知道:对于全速模式和低速模式的设备,获取描述符的标准请求只有三种。单独请求获取设备描述符、单独请求获取配置描述符、单独请求获取字符串描述符。接口描述符和端点描述符是跟着配置描述符一起返回的,不可单独返回,根据他们之间的包含关系(一个配置描述符可以有很多个接口,一个接口可以有很多个端点……),如果单独返回,主机没法解析(不符合协议)。实际上,主机也不会单独请求获取。
找到设备描述符之后,通过usb_data_to_host()将数据发送到主机。一层层剖析,可以发现,数据的发送是通过端点0进行发送的(符合协议)。usb_data_to_host()里面调用的是usb_dc_ep_write(),进入usb_dc_ep_write()可以发现,该函数完成数据的发送,还可以获取到发送成功的数据字节数。
发送的大概过程(结合代码和协议): 将设备描述符数据写入到端点0的输入缓冲区中,并使能端点发送。主机下发IN令牌包之后,设备的USB控制器就将缓冲区中的数据返回给主机,主机收到数据,则下发ACK握手包到设备,设备收到握手包就可以确认数据已经成功传输到主机了(这部分也是硬件自动完成的,对用户不可见)。
两个函数的定义处,分别如下:
主机在成功获取第一个数据包的设备描述符并确认无误之后,就会返回一个0长度的确认数据包(状态过程,使用的是DATA1数据包),设备收到确认数据包,就知道发送的数据是无误的,接下来设备发送握手包给主机,结束一个控制传输过程。控制传输过程比较复杂,几次交互,有可见的数据交互,也有不可见的数据交互,虽然复杂,但是能保证数据的可靠性。
到此,一个控制传输过程结束【建立过程(主机下发请求)、状态过程(设备返回数据) 状态过程(主机返回0长度的数据包)】。设备描述符总共有18个字节。
串口跟踪打印如下:
总结大概的流程: 有请求----->解析请求----->返回数据
需要注意的是,端点状态的回调(能反映数据的通信过程是否完整,开始或者结束)。
USB协议分析仪上捕获到的完整的总线数据:
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插入,第一次请求设备描述符的过程。