USB基础知识 – 枚举

一、USB通信过程

主机和USB设备可以相互传输数据,其具体过程如下(以主机箱设备传输为例):

  • step1:客户软件首先将要传输的数据放入缓冲区,同时向USB总线驱动程序发出IRPS,请求数据传输。(客户软件)
  • step2:USB总线驱动接收到程序接收请求,并对数据进行处理,转化为具有USB格式的事务处理。(USB系统软件)
  • step3:USB主控制器驱动程序将这些事务处理建立成事务列表,同时要求不能超过USB带宽。(USB系统软件)
  • step4:USB主控制器读取到事务列表并将事务转化为信息包,发送到USB总线上。(USB总线接口)
  • step5:USB设备收到这些信息后,SIE(USB总线接口是USB设备中的串行接口引擎)将其解包后放入指定端点的接收缓冲区内,由芯片固件对其进行处理。

二、USB枚举流程

  • step1:检测电压变化,报告主机
  • Step2:主机了解连接设备
  • Step3:Hub检测所插入的设备是高速还是低速
  • Step4:Hub复位设备
  • Step5:Host检测所连接的全速设备是否是支持高速模式
  • Step6:Hub建立设备和主机之间的信息通道
  • Step7:主机发送Get_Descriptor请求获取默认管道的最大包长度
  • Step8:主机给设备分配一个地址
  • Step9:主机获取设备的信息
  • Step10:主机给设备挂载驱动(复合设备除外)
  • Step11:设备驱动选择一个配置

三、详细的枚举过程

1、Hub检测各个端口数据线上的电压来判断插入的设备类型

在hub端,数据线D+和D-都有一个阻值在14.25k到24.8k的下拉电阻Rpd,而在设备端,D+(全速,高速)和D-(低速)上有一个1.5k的上拉电阻Rpu。当设备插入到hub端口时,有上拉电阻的一根数据线被拉高到幅值的90%的电压(大致是3V)。hub检测到它的一根数据线是高电平,就认为是有设备插入,并能根据是D+还是D-被拉高来判断到底是什么设备(全速/低速)插入端口(全速、高速设备的区分在我将来的文章中描述)。如下图。

2、Host了解连接的设备信息

每个hub利用它自己的中断端点向主机报告它的各个端口的状态(对于这个过程,设备是看不到的,也不必关心),报告的内容只是hub端口的设备连接/断开的事件。如果有连接/断开事件发生,那么host会发送一个 Get_Port_Status请求(request)给hub以了解此次状态改变的确切含义。Get_Port_Status等请求属于所有hub都要求支持的hub类标准请求(standard hub-class requests)。

3、Hub检测所插入的设备是高速还是低速设备

Hub通过检测USB总线空闲(Idle)时差分线的高低电压来判断所连接设备的速度类型,当host发来Get_Port_Status请求时,hub就可以将此设备的速度类型信息回复给host。USB 2.0规范要求速度检测要先于复位(Reset)操作。

4、Hub复位设备

主机一旦得知新设备已连上以后,它至少等待100ms以使得插入操作的完成以及设备电源稳定工作。然后主机控制器就向hub发出一个 Set_Port_Feature请求让hub复位其管理的端口(刚才设备插上的端口)。hub通过驱动数据线到复位状态(D+和D-全为低电平 ),并持续至少10ms。当然,hub不会把这样的复位信号发送给其他已有设备连接的端口,所以其他连在该hub上的设备自然看不到复位信号,不受影响。

5、Host检测所连接的全速设备是否是支持高速模式

  因为根据USB 2.0协议,高速(High Speed)设备在初始时是默认全速(Full Speed )状态运行,所以对于一个支持USB 2.0的高速hub,当它发现它的端口连接的是一个全速设备时,会进行高速检测,看看目前这个设备是否还支持高速传输,如果是,那就切到高速信号模式,否则就一直在全速状态下工作。
       同样的,从设备的角度来看,如果是一个高速设备,在刚连接bub或上电时只能用全速信号模式运行(根据USB 2.0协议,高速设备必须向下兼容USB 1.1的全速模式)。随后hub会进行高速检测,之后这个设备才会切换到高速模式下工作。假如所连接的hub不支持USB 2.0,即不是高速hub,不能进行高速检测,设备将一直以全速工作。

6、Hub建立设备和主机之间的信息通道

  主机不停地向hub发送Get_Port_Status请求,以查询设备是否复位成功。Hub返回的报告信息中有专门的一位用来标志设备的复位状态。
       当hub撤销了复位信号,设备就处于默认/空闲状态(Default state),准备接收主机发来的请求。设备和主机之间的通信通过控制传输,默认地址0,端点号0进行。此时,设备能从总线上得到的最大电流是100mA。(所有的USB设备在总线复位后其地址都为0,这样主机就可以跟那些刚刚插入的设备通过地址0通信。)

**真正的枚举从下面开始**

7、主机发送Get_Descriptor请求获取默认管道的最大包长度

  默认管道(Default Pipe)在设备一端来看就是端点0。主机此时发送的请求是默认地址0,端点0,虽然所有未分配地址的设备都是通过地址0来获取主机发来的请求,但由于枚举过程不是多个设备并行处理,而是一次枚举一个设备的方式进行,所以不会发生多个设备同时响应主机发来的请求。
      设备描述符的第8字节代表设备端点0的最大包大小。虽然说设备所返回的设备描述符(Device Descriptor)长度只有18字节,但系统也不在乎,此时,描述符的长度信息对它来说是最重要的,其他的瞄一眼就过了。当完成第一次的控制传输后,也就是完成控制传输的状态阶段,系统会要求hub对设备进行再一次的复位操作(USB规范里面可没这要求)。再次复位的目的是使设备进入一个确定的状态。

8、主机给设备分配一个地址

  主机控制器通过Set_Address请求向设备分配一个唯一的地址。在完成这次传输之后,设备进入地址状态(Address state),之后就启用新地址继续与主机通信。这个地址对于设备来说是终生制的,设备在,地址在;设备消失(被拔出,复位,系统重启),地址被收回。同一个设备当再次被枚举后得到的地址不一定是上次那个了。

9、主机获取设备的信息

  主机发送 Get_Descriptor请求到新地址读取设备描述符,这次主机发送Get_Descriptor请求可算是诚心,它会认真解析设备描述符的内容。设备描述符内信息包括端点0的最大包长度,设备所支持的配置(Configuration)个数,设备类型,VID(Vendor ID,由USB-IF分配), PID(Product ID,由厂商自己定制)等信息。Get_Descriptor请求(Device type)和设备描述符(VID,PID等信息)见下图:

请求数据为8个字节HEX:80 06 00 01 00 00 12 00

Get_description分析:

  • 第一个字节0x80拆分可以得到,这是一个主机发给设备(bit0~bit4)的一个标准(bit5~bit6)的请求命令,请求的结果是要求设备给Host返回(bit7 == 1)。
  • 第二个字节0x06查看表9-4可以得到这是一个GET_DESCRIPTOR,即获取描述符 的请求。
  • 第三四字节传的是0x0100 ,查看描述符表,得知高字节表示描述符类型,01表示设备,02表示配置;低字节表示索引。比如设备有多个配置,那需要读取不同配置的时候就通过低字节。或者一个配置下有多个接口,通过索引选择不同的接口。所以这里高字节的1代表 设备,低字节在本设备没用到。
  • 第四五字节为0x0,这个参数如果为0,则不关心;如果为非零,则表示Langurage ID,每一位都有对应的意义。
  • 第六七字节为0x12,即代表返回的数据不应该多于18个字节。

设备给主机回复的数据为18个字节HEX:12 01 00 02 00 00 00 08 64 0D 18 C0 01 43 01 02 00 01

设备描述符的分析如下

/* USB_DT_DEVICE: Device descriptor */
struct usb_device_descriptor {
	__u8  bLength             = 0x12;    //该描述符长度为18字节
	__u8  bDescriptorType     = 0x1;     //从16节的表9-5可知,1代表的描述符是设备描述符            
 
	__le16 bcdUSB             = 0x200;   //该位表示版本号,使用BCD码表示,即USB2.0版本        
	__u8  bDeviceClass        = 0x0;     //0代表由接口指出类信息
	__u8  bDeviceSubClass     = 0x0;     //子类,bDeviceClass 域为零,此域也须为零
	__u8  bDeviceProtocol     = 0x0;     //协议码,0代表没指定任何协议
	__u8  bMaxPacketSize0     = 0x8;     //端点0,最大包支持8个字节(低速8个字节)
	__le16 idVendor           = 0x046D;  //厂商标志(学习可以不用关心)
	__le16 idProduct          = 0xC010;  //产品标志(学习可以不用关心)
	__le16 bcdDevice          = 0x4301;  //设备发行号,用BCD码表示,43.01版本(学习可以不用关心)
	__u8  iManufacturer       = 0x1;     //描述厂商信息的字符串描述符的索引值
	__u8  iProduct            = 0x2;     //描述产品信息的字串描述符的索引值
	__u8  iSerialNumber       = 0x0;     //描述设备序列号信息的字串描述符的索引值
	__u8  bNumConfigurations  = 0x1;     //可能的配置描述符数目,这个鼠标设备就支持一个配置
} __attribute__ ((packed));

获取配置描述符:

请求数据为8个字节HEX:80 06 00 02 00 00 09 00

  • 第一个字节0x80拆分可以得到,这是一个主机发给设备(bit0~bit4)的一个标准(bit5~bit6)的请求命令,请求的结果是要求设备给Host返回(bit7 == 1)。
  • 第二个字节0x06查看表9-4可以得到这是一个GET_DESCRIPTOR,即获取描述符 的请求。
  • 第三四字节传的是0x0100 ,查看描述符表,得知高字节表示描述符类型,01表示设备,02表示配置;低字节表示索引。比如设备有多个配置,那需要读取不同配置的时候就通过低字节。或者一个配置下有多个接口,通过索引选择不同的接口。所以这里高字节的2代表配置,低字节在本设备没用到。
  • 第四五字节为0x0,这个参数如果为0,则不关心;如果为非零,则表示Langurage ID,每一位都有对应的意义。
  • 第六七字节为0x9,即代表返回的数据不应该多于9个字节。

设备给主机回复配置描述符:

设备给主机回复的数据为9个字节HEX:09 02 22 00 01 01 00 A0 32

 struct usb_config_descriptor {
	__u8  bLength            = 0x09;    //此描述符长度为9
	__u8  bDescriptorType    = 0x02;    ///从16节的表9-5可知,2代表的配置描述符 
 
	__le16 wTotalLength      = 0x22;    //此配置信息的总长为34字节(包括配置,接口,端点和设备类及厂商定义的描述符)
	__u8  bNumInterfaces     = 0x01;    //表示有一个接口描述符
	__u8  bConfigurationValue= 0x01;    //在SetConfiguration()请求中用1作参数来选定此配置
	__u8  iConfiguration     = 0x00;    //描述此配置的字串描述表索引
	__u8  bmAttributes       = 0xA0;    //D7: 保留(设为一)D6: 自给电源 D5: 远程唤醒 D4..0:保留(设为一)   表示这是一个由总线供电,并支持远程唤醒功能(可以睡眠节约电)
	__u8  bMaxPower          = 0x32;    //在此配置下的总线电源耗费量。以 2mA 为一个单位 即2 * 50 = 100ms
} __attribute__ ((packed));

获取其他描述:

上一步在配置描述符中已经知道了配置描述符+接口描述符+端点描述符+设备类描述符+厂商定义的描述符的总字节长度为34个字节。

请求数据为8个字节HEX:80 06 00 02 00 00 22 00

  • 第一个字节0x80拆分可以得到,这是一个主机发给设备(bit0~bit4)的一个标准(bit5~bit6)的请求命令,请求的结果是要求设备给Host返回(bit7 == 1)。
  • 第二个字节0x06查看表9-4可以得到这是一个GET_DESCRIPTOR,即获取描述符 的请求。
  • 第三四字节传的是0x0100 ,查看描述符表,得知高字节表示描述符类型,01表示设备,02表示配置;低字节表示索引。比如设备有多个配置,那需要读取不同配置的时候就通过低字节。或者一个配置下有多个接口,通过索引选择不同的接口。所以这里高字节的2代表配置,低字节在本设备没用到。
  • 第四五字节为0x0,这个参数如果为0,则不关心;如果为非零,则表示Langurage ID,每一位都有对应的意义。
  • 第六七字节为0x22,即代表返回的数据不应该多于34个字节(因为实际上次读取配置描述符已经知道多个描述符总和为34字节了)。

⑥ 设备给主机回复的数据为34个字节HEX:

09 02 22 00 01 01 00 A0 32 09 04 00 00 01 03 01 02 00 09 21 11 01 00 01 22 34 00 07 05 81 03 05 00 0A

拆分后:

  1. 09 02 22 00 01 01 00 A0 32 配置描述符
  2. 09 04 00 00 01 03 01 02 00 接口描述符
  3. 09 21 11 01 00 01 22 34 00 HID类描述符
  4. 07 05 81 03 05 00 0A 端点描述符

接口描述符

struct usb_interface_descriptor {
	__u8  bLength              = 0x09;    //该描述符的字节数
	__u8  bDescriptorType      = 0x04;    //描述符类型,4代表接口描述符
 
	__u8  bInterfaceNumber     = 0x0;     //接口号,当前配置支持的接口数组索引(从零开始)
	__u8  bAlternateSetting    = 0x0;     //可选设置的索引值,这里无
	__u8  bNumEndpoints        = 0x01;    //端点描述符数量,1个    
	__u8  bInterfaceClass      = 0x03;    //接口所属的类值,由USB说明保留,	3代表人机接口类(HID)
	__u8  bInterfaceSubClass   = 0x01;    //子类码 这些值的定义视bInterfaceClass域而定
	__u8  bInterfaceProtocol   = 0x02;    //协议码:bInterfaceClass 和bInterfaceSubClass 域的值而定
	__u8  iInterface           = 0x00;    //描述此接口的字串描述表的索引值
} __attribute__ ((packed));

这里看一下接口描述的分类:

Base Class Descriptor Usage Description
00h Device Use class information in the Interface Descriptors
01h Interface Audio 
02h Both Communications and CDC Control
03h Interface HID (Human Interface Device)
05h Interface Physical
06h Interface Image
07h Interface Printer
08h Interface Mass Storage
09h Device Hub
0Ah Interface CDC-Data
0Bh Interface Smart Card
0Dh Interface Content Security
0Eh Interface Video
0Fh Interface Personal Healthcare
10h Interface Audio/Video Devices
11h Device Billboard Device Class
12h Interface USB Type-C Bridge Class
DCh Both Diagnostic Device
E0h Interface Wireless Controller
EFh Both Miscellaneous
FEh Interface Application Specific
FFh Both Vendor Specific

  类-子类-协议码 最终得到我们这个是一个鼠标设备。有一个端点,可想而知就是输入端点,比较鼠标就是一个输入类设备

端点描述符

struct usb_endpoint_descriptor {
	__u8  bLength            = 0x07;    //该描述符占用7个字节
	__u8  bDescriptorType    = 0x05;    //是一个端点描述符
 
        //Bit3..0:端点号 Bit6..4:保留,为零 Bit7:方向,如果控制端点则略。 
        //0:输出端点(主机到设备) 1:输入端点(设备到主机) 所以在这是一个端点号为1的输入端点
	__u8  bEndpointAddress   = 0x81;    
	__u8  bmAttributes       = 0x3;     //00=控制传送 01=同步传送 10=批传送 11=中断传送   这是一个中断端点            
	__le16 wMaxPacketSize    = 0x0005;  //最大包大小为5个字节
	__u8  bInterval          = 0x0A;    //周期数据传输端点的时间间隙。1ms * 10 = 10ms
 
	/* NOTE:  these two are _only_ in audio endpoints. */
	/* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */
        /* 下面两个只有在图像视频类中才会有 */
	__u8  bRefresh;
	__u8  bSynchAddress;
} __attribute__ ((packed));

  之后主机发送Get_Descriptor请求,读取配置描述符(Configuration Descriptor),字符串等,逐一了解设备更详细的信息。事实上,对于配置描述符的标准请求中,有时wLength一项会大于实际配置描述符的长度(9字节),比如255。这样的效果便是:主机发送了一个Get_Descriptor_Configuration 的请求,设备会把接口描述符,端点描述符等后续描述符一并回给主机,主机则根据描述符头部的标志判断送上来的具体是何种描述符。
      接下来,主机就会获取配置描述符。配置描述符总共为9字节。主机在获取到配置描述符后,根据里面的配置集合总长度,再获取配置集合。配置集合包括配置描述符,接口描述符,端点描符等等。
     如果有字符串描述符的话,还要获取字符串描述符。另外HID设备还有HID描述符等。

10、主机给设备挂载驱动(复合设备除外)

  主机通过解析描述符后对设备有了足够的了解,会选择一个最合适的驱动给设备。  然后tell the world(announce_device)说明设备已经找到了,最后调用设备模型提供的接口device_add将设备添加到 usb 总线的设备列表里,然后 usb总线会遍历驱动列表里的每个驱动,调用自己的 match(usb_device_match) 函数看它们和你的设备或接口是否匹配,匹配的话调用device_bind_driver函数,现在就将控制权交到设备驱动了。   

     对于复合设备,通常应该是不同的接口(Interface)配置给不同的驱动,因此,需要等到当设备被配置并把接口使能后才可以把驱动挂载上去。

You may also like...

发表评论

电子邮件地址不会被公开。