一、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
拆分后:
- 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 HID类描述符
- 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)配置给不同的驱动,因此,需要等到当设备被配置并把接口使能后才可以把驱动挂载上去。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
暂无评论...